diff --git a/qgroundcontrol.pro b/qgroundcontrol.pro index 9dfad863f037d1c97aaa31f5a6a1e1ce778c5e52..a070e4a2d0f331b2dd572181769a8f9d2962c447 100644 --- a/qgroundcontrol.pro +++ b/qgroundcontrol.pro @@ -334,6 +334,7 @@ HEADERS += \ src/uas/UAS.h \ src/uas/UASInterface.h \ src/uas/UASMessageHandler.h \ + src/Vehicle/MavlinkLogManager.h \ src/ui/toolbar/MainToolBarController.h \ src/AutoPilotPlugins/PX4/PX4AirframeLoader.h \ src/AutoPilotPlugins/APM/APMAirframeLoader.h \ @@ -498,6 +499,7 @@ SOURCES += \ src/QmlControls/QmlObjectListModel.cc \ src/uas/UAS.cc \ src/uas/UASMessageHandler.cc \ + src/Vehicle/MavlinkLogManager.cc \ src/ui/toolbar/MainToolBarController.cc \ src/AutoPilotPlugins/PX4/PX4AirframeLoader.cc \ src/AutoPilotPlugins/APM/APMAirframeLoader.cc \ diff --git a/src/QGCToolbox.cc b/src/QGCToolbox.cc index 5c63154bed9d133066c588a4cf4f52803b46efe1..fd569b7b5abd236a377f744d1a6ae3f540640ad4 100644 --- a/src/QGCToolbox.cc +++ b/src/QGCToolbox.cc @@ -28,6 +28,7 @@ #include "FollowMe.h" #include "PositionManager.h" #include "VideoManager.h" +#include "MavlinkLogManager.h" QGCToolbox::QGCToolbox(QGCApplication* app) : _audioOutput(NULL) @@ -50,6 +51,7 @@ QGCToolbox::QGCToolbox(QGCApplication* app) , _followMe(NULL) , _qgcPositionManager(NULL) , _videoManager(NULL) + , _mavlinkLogManager(NULL) { _audioOutput = new GAudioOutput(app); _autopilotPluginManager = new AutoPilotPluginManager(app); @@ -71,6 +73,7 @@ QGCToolbox::QGCToolbox(QGCApplication* app) _qgcPositionManager = new QGCPositionManager(app); _followMe = new FollowMe(app); _videoManager = new VideoManager(app); + _mavlinkLogManager = new MavlinkLogManager(app); } void QGCToolbox::setChildToolboxes(void) @@ -95,11 +98,13 @@ void QGCToolbox::setChildToolboxes(void) _followMe->setToolbox(this); _qgcPositionManager->setToolbox(this); _videoManager->setToolbox(this); + _mavlinkLogManager->setToolbox(this); } QGCToolbox::~QGCToolbox() { delete _videoManager; + delete _mavlinkLogManager; delete _audioOutput; delete _autopilotPluginManager; delete _factSystem; diff --git a/src/QGCToolbox.h b/src/QGCToolbox.h index 540c917f3b268bdba33c886418ae324ee6df42b9..1b0080de5f95052fc00a91fc421517bddc5a2ff5 100644 --- a/src/QGCToolbox.h +++ b/src/QGCToolbox.h @@ -32,6 +32,7 @@ class QGCImageProvider; class UASMessageHandler; class QGCPositionManager; class VideoManager; +class MavlinkLogManager; /// This is used to manage all of our top level services/tools class QGCToolbox { @@ -56,6 +57,8 @@ public: FollowMe* followMe(void) { return _followMe; } QGCPositionManager* qgcPositionManager(void) { return _qgcPositionManager; } VideoManager* videoManager(void) { return _videoManager; } + MavlinkLogManager* mavlinkLogManager(void) { return _mavlinkLogManager; } + #ifndef __mobile__ GPSManager* gpsManager(void) { return _gpsManager; } #endif @@ -83,6 +86,7 @@ private: FollowMe* _followMe; QGCPositionManager* _qgcPositionManager; VideoManager* _videoManager; + MavlinkLogManager* _mavlinkLogManager; friend class QGCApplication; }; diff --git a/src/QmlControls/QGroundControlQmlGlobal.cc b/src/QmlControls/QGroundControlQmlGlobal.cc index abd618bc3897b52f0bf93d8c893255da351a0ea2..c249191bdba2e3cbd4f844c03bf979b098c21964 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.cc +++ b/src/QmlControls/QGroundControlQmlGlobal.cc @@ -44,6 +44,7 @@ QGroundControlQmlGlobal::QGroundControlQmlGlobal(QGCApplication* app) , _qgcPositionManager(NULL) , _missionCommandTree(NULL) , _videoManager(NULL) + , _mavlinkLogManager(NULL) , _virtualTabletJoystick(false) , _baseFontPointSize(0.0) { @@ -60,7 +61,6 @@ QGroundControlQmlGlobal::~QGroundControlQmlGlobal() } - void QGroundControlQmlGlobal::setToolbox(QGCToolbox* toolbox) { QGCTool::setToolbox(toolbox); @@ -72,9 +72,9 @@ void QGroundControlQmlGlobal::setToolbox(QGCToolbox* toolbox) _qgcPositionManager = toolbox->qgcPositionManager(); _missionCommandTree = toolbox->missionCommandTree(); _videoManager = toolbox->videoManager(); + _mavlinkLogManager = toolbox->mavlinkLogManager(); } - void QGroundControlQmlGlobal::saveGlobalSetting (const QString& key, const QString& value) { QSettings settings; diff --git a/src/QmlControls/QGroundControlQmlGlobal.h b/src/QmlControls/QGroundControlQmlGlobal.h index 69180218490dda7bbdcb5a6de89eadafc3d14f48..b23823f44852da08b340dfa3edd61746c055b337 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.h +++ b/src/QmlControls/QGroundControlQmlGlobal.h @@ -72,6 +72,7 @@ public: Q_PROPERTY(QGCPositionManager* qgcPositionManger READ qgcPositionManger CONSTANT) Q_PROPERTY(MissionCommandTree* missionCommandTree READ missionCommandTree CONSTANT) Q_PROPERTY(VideoManager* videoManager READ videoManager CONSTANT) + Q_PROPERTY(MavlinkLogManager* mavlinkLogManager READ mavlinkLogManager CONSTANT) Q_PROPERTY(qreal zOrderTopMost READ zOrderTopMost CONSTANT) ///< z order for top most items, toolbar, main window sub view Q_PROPERTY(qreal zOrderWidgets READ zOrderWidgets CONSTANT) ///< z order value to widgets, for example: zoom controls, hud widgetss @@ -166,6 +167,7 @@ public: QGCPositionManager* qgcPositionManger () { return _qgcPositionManager; } MissionCommandTree* missionCommandTree () { return _missionCommandTree; } VideoManager* videoManager () { return _videoManager; } + MavlinkLogManager* mavlinkLogManager () { return _mavlinkLogManager; } qreal zOrderTopMost () { return 1000; } qreal zOrderWidgets () { return 100; } @@ -237,6 +239,7 @@ private: QGCPositionManager* _qgcPositionManager; MissionCommandTree* _missionCommandTree; VideoManager* _videoManager; + MavlinkLogManager* _mavlinkLogManager; bool _virtualTabletJoystick; qreal _baseFontPointSize; diff --git a/src/Vehicle/MavlinkLogManager.cc b/src/Vehicle/MavlinkLogManager.cc new file mode 100644 index 0000000000000000000000000000000000000000..ccb067f69bed08cdeef5425487595793573f6f08 --- /dev/null +++ b/src/Vehicle/MavlinkLogManager.cc @@ -0,0 +1,899 @@ +/**************************************************************************** + * + * (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. + * + ****************************************************************************/ + +#include "MavlinkLogManager.h" +#include "QGCApplication.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define kTimeOutMilliseconds 1000 + +QGC_LOGGING_CATEGORY(MavlinkLogManagerLog, "MavlinkLogManagerLog") + +static const char* kEmailAddressKey = "MavlinkLogEmail"; +static const char* kDescriptionsKey = "MavlinkLogDescription"; +static const char* kDefaultDescr = "QGroundControl Session"; +static const char* kPx4URLKey = "MavlinkLogURL"; +static const char* kDefaultPx4URL = "http://logs.px4.io/upload"; +static const char* kEnableAutoUploadKey = "EnableAutoUploadKey"; +static const char* kEnableAutoStartKey = "EnableAutoStartKey"; +static const char* kEnableDeletetKey = "EnableDeleteKey"; +static const char* kUlogExtension = ".ulg"; +static const char* kSidecarExtension = ".uploaded"; + +//----------------------------------------------------------------------------- +MavlinkLogFiles::MavlinkLogFiles(MavlinkLogManager* manager, const QString& filePath, bool newFile) + : _manager(manager) + , _size(0) + , _selected(false) + , _uploading(false) + , _progress(0) + , _writing(false) + , _uploaded(false) +{ + QFileInfo fi(filePath); + _name = fi.baseName(); + if(!newFile) { + _size = (quint32)fi.size(); + QString sideCar = filePath; + sideCar.replace(kUlogExtension, kSidecarExtension); + QFileInfo sc(sideCar); + _uploaded = sc.exists(); + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogFiles::setSize(quint32 size) +{ + _size = size; + emit sizeChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogFiles::setSelected(bool selected) +{ + _selected = selected; + emit selectedChanged(); + emit _manager->selectedCountChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogFiles::setUploading(bool uploading) +{ + _uploading = uploading; + emit uploadingChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogFiles::setProgress(qreal progress) +{ + _progress = progress; + emit progressChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogFiles::setWriting(bool writing) +{ + _writing = writing; + emit writingChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogFiles::setUploaded(bool uploaded) +{ + _uploaded = uploaded; + emit uploadedChanged(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +MavlinkLogProcessor::MavlinkLogProcessor() + : _fd(NULL) + , _written(0) + , _sequence(-1) + , _numDrops(0) + , _gotHeader(false) + , _error(false) + , _record(NULL) +{ +} + +//----------------------------------------------------------------------------- +MavlinkLogProcessor::~MavlinkLogProcessor() +{ + close(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogProcessor::close() +{ + if(_fd) { + fclose(_fd); + _fd = NULL; + } +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogProcessor::valid() +{ + return (_fd != NULL) && (_record != NULL); +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogProcessor::create(MavlinkLogManager* manager, const QString path, uint8_t id) +{ + _fileName.sprintf("%s/%03d-%s%s", + path.toLatin1().data(), + id, + QDateTime::currentDateTime().toString("yyyy-MM-dd-hh-mm-ss-zzz").toLatin1().data(), + kUlogExtension); + _fd = fopen(_fileName.toLatin1().data(), "wb"); + if(_fd) { + _record = new MavlinkLogFiles(manager, _fileName, true); + _record->setWriting(true); + _sequence = -1; + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogProcessor::_checkSequence(uint16_t seq, int& num_drops) +{ + num_drops = 0; + //-- Check if a sequence is newer than the one previously received and if + // there were dropped messages between the last one and this. + if(_sequence == -1) { + _sequence = seq; + return true; + } + if((uint16_t)_sequence == seq) { + return false; + } + if(seq > (uint16_t)_sequence) { + // Account for wrap-arounds, sequence is 2 bytes + if((seq - _sequence) > (1 << 15)) { // Assume reordered + return false; + } + num_drops = seq - _sequence - 1; + _numDrops += num_drops; + _sequence = seq; + return true; + } else { + if((_sequence - seq) > (1 << 15)) { + num_drops = (1 << 16) - _sequence - 1 + seq; + _numDrops += num_drops; + _sequence = seq; + return true; + } + return false; + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogProcessor::_writeData(void* data, int len) +{ + if(!_error) { + _error = fwrite(data, 1, len, _fd) != (size_t)len; + if(!_error) { + _written += len; + if(_record) { + _record->setSize(_written); + } + } else { + qCDebug(MavlinkLogManagerLog) << "File IO error:" << len << "bytes into" << _fileName; + } + } +} + +//----------------------------------------------------------------------------- +QByteArray +MavlinkLogProcessor::_writeUlogMessage(QByteArray& data) +{ + //-- Write ulog data w/o integrity checking, assuming data starts with a + // valid ulog message. returns the remaining data at the end. + while(data.length() > 2) { + uint8_t* ptr = (uint8_t*)data.data(); + int message_length = ptr[0] + (ptr[1] * 256) + 3; // 3 = ULog msg header + if(message_length > data.length()) + break; + _writeData(data.data(), message_length); + data.remove(0, message_length); + return data; + } + return data; +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogProcessor::processStreamData(uint16_t sequence, uint8_t first_message, QByteArray data) +{ + int num_drops = 0; + _error = false; + while(_checkSequence(sequence, num_drops)) { + //-- The first 16 bytes need special treatment (this sounds awfully brittle) + if(!_gotHeader) { + if(data.size() < 16) { + //-- Shouldn't happen but if it does, we might as well close shop. + qCCritical(MavlinkLogManagerLog) << "Corrupt log header. Canceling log download."; + return false; + } + //-- Write header + _writeData(data.data(), 16); + data.remove(0, 16); + _gotHeader = true; + // What about data start offset now that we removed 16 bytes off the start? + } + if(_gotHeader && num_drops > 0) { + if(num_drops > 25) num_drops = 25; + //-- Hocus Pocus + // Write a dropout message. We don't really know the actual duration, + // so just use the number of drops * 10 ms + uint8_t bogus[] = {2, 0, 79, 0, 0}; + bogus[3] = num_drops * 10; + _writeData(bogus, sizeof(bogus)); + } + if(num_drops > 0) { + _writeUlogMessage(_ulogMessage); + _ulogMessage.clear(); + //-- If no usefull information in this message. Drop it. + if(first_message == 255) { + break; + } + if(first_message > 0) { + data.remove(0, first_message); + first_message = 0; + } + } + if(first_message == 255 && _ulogMessage.length() > 0) { + _ulogMessage.append(data); + break; + } + if(_ulogMessage.length()) { + _writeData(_ulogMessage.data(), _ulogMessage.length()); + if(first_message) { + _writeData(data.left(first_message).data(), first_message); + } + _ulogMessage.clear(); + } + if(first_message) { + data.remove(0, first_message); + } + _ulogMessage = _writeUlogMessage(data); + break; + } + return !_error; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +MavlinkLogManager::MavlinkLogManager(QGCApplication* app) + : QGCTool(app) + , _enableAutoUpload(true) + , _enableAutoStart(true) + , _nam(NULL) + , _currentLogfile(NULL) + , _vehicle(NULL) + , _logRunning(false) + , _loggingDisabled(false) + , _logProcessor(NULL) + , _deleteAfterUpload(false) + , _loggingCmdTryCount(0) +{ + //-- Get saved settings + QSettings settings; + setEmailAddress(settings.value(kEmailAddressKey, QString()).toString()); + setDescription(settings.value(kDescriptionsKey, QString(kDefaultDescr)).toString()); + setUploadURL(settings.value(kPx4URLKey, QString(kDefaultPx4URL)).toString()); + setEnableAutoUpload(settings.value(kEnableAutoUploadKey, true).toBool()); + setEnableAutoStart(settings.value(kEnableAutoStartKey, true).toBool()); + setDeleteAfterUpload(settings.value(kEnableDeletetKey, false).toBool()); + //-- Logging location + _logPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + _logPath += "/MavlinkLogs"; + if(!QDir(_logPath).exists()) { + if(!QDir().mkpath(_logPath)) { + qCCritical(MavlinkLogManagerLog) << "Could not create Mavlink log download path:" << _logPath; + _loggingDisabled = true; + } + } + if(!_loggingDisabled) { + //-- Load current list of logs + QString filter = "*"; + filter += kUlogExtension; + QDirIterator it(_logPath, QStringList() << filter, QDir::Files); + while(it.hasNext()) { + _insertNewLog(new MavlinkLogFiles(this, it.next())); + } + qCDebug(MavlinkLogManagerLog) << "Mavlink logs directory:" << _logPath; + } +} + +//----------------------------------------------------------------------------- +MavlinkLogManager::~MavlinkLogManager() +{ + _logFiles.clear(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setToolbox(QGCToolbox* toolbox) +{ + QGCTool::setToolbox(toolbox); + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + qmlRegisterUncreatableType("QGroundControl.MavlinkLogManager", 1, 0, "MavlinkLogManager", "Reference only"); + if(!_loggingDisabled) { + connect(toolbox->multiVehicleManager(), &MultiVehicleManager::activeVehicleChanged, this, &MavlinkLogManager::_activeVehicleChanged); + connect(&_ackTimer, &QTimer::timeout, this, &MavlinkLogManager::_processCmdAck); + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setEmailAddress(QString email) +{ + _emailAddress = email; + QSettings settings; + settings.setValue(kEmailAddressKey, email); + emit emailAddressChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setDescription(QString description) +{ + _description = description; + QSettings settings; + settings.setValue(kDescriptionsKey, description); + emit descriptionChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setUploadURL(QString url) +{ + _uploadURL = url; + if(_uploadURL.isEmpty()) { + _uploadURL = kDefaultPx4URL; + } + QSettings settings; + settings.setValue(kPx4URLKey, _uploadURL); + emit uploadURLChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setEnableAutoUpload(bool enable) +{ + _enableAutoUpload = enable; + QSettings settings; + settings.setValue(kEnableAutoUploadKey, enable); + emit enableAutoUploadChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setEnableAutoStart(bool enable) +{ + _enableAutoStart = enable; + QSettings settings; + settings.setValue(kEnableAutoStartKey, enable); + emit enableAutoStartChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::setDeleteAfterUpload(bool enable) +{ + _deleteAfterUpload = enable; + QSettings settings; + settings.setValue(kEnableDeletetKey, enable); + emit deleteAfterUploadChanged(); +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogManager::uploading() +{ + return _currentLogfile != NULL; +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::uploadLog() +{ + if(_currentLogfile) { + _currentLogfile->setUploading(false); + } + for(int i = 0; i < _logFiles.count(); i++) { + _currentLogfile = qobject_cast(_logFiles.get(i)); + Q_ASSERT(_currentLogfile); + if(_currentLogfile->selected()) { + _currentLogfile->setSelected(false); + if(!_currentLogfile->uploaded() && !_emailAddress.isEmpty() && !_uploadURL.isEmpty()) { + _currentLogfile->setUploading(true); + _currentLogfile->setProgress(0.0); + QString filePath = _makeFilename(_currentLogfile->name()); + _sendLog(filePath); + emit uploadingChanged(); + return; + } + } + } + _currentLogfile = NULL; + emit uploadingChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_insertNewLog(MavlinkLogFiles* newLog) +{ + //-- Simpler than trying to sort this thing + int count = _logFiles.count(); + if(!count) { + _logFiles.append(newLog); + } else { + for(int i = 0; i < count; i++) { + MavlinkLogFiles* f = qobject_cast(_logFiles.get(i)); + if(newLog->name() < f->name()) { + _logFiles.insert(i, newLog); + return; + } + } + _logFiles.append(newLog); + } +} + +//----------------------------------------------------------------------------- +int +MavlinkLogManager::_getFirstSelected() +{ + for(int i = 0; i < _logFiles.count(); i++) { + MavlinkLogFiles* f = qobject_cast(_logFiles.get(i)); + Q_ASSERT(f); + if(f->selected()) { + return i; + } + } + return -1; +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::deleteLog() +{ + while (true) { + int idx = _getFirstSelected(); + if(idx < 0) { + break; + } + MavlinkLogFiles* log = qobject_cast(_logFiles.get(idx)); + _deleteLog(log); + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_deleteLog(MavlinkLogFiles* log) +{ + QString filePath = _makeFilename(log->name()); + QFile gone(filePath); + if(!gone.remove()) { + qCWarning(MavlinkLogManagerLog) << "Could not delete Mavlink log file:" << _logPath; + } + //-- Remove sidecar file (if any) + filePath.replace(kUlogExtension, kSidecarExtension); + QFile sgone(filePath); + if(sgone.exists()) { + sgone.remove(); + } + //-- Remove file from list and delete record + _logFiles.removeOne(log); + delete log; + emit logFilesChanged(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::cancelUpload() +{ + for(int i = 0; i < _logFiles.count(); i++) { + MavlinkLogFiles* pLogFile = qobject_cast(_logFiles.get(i)); + Q_ASSERT(pLogFile); + if(pLogFile->selected() && pLogFile != _currentLogfile) { + pLogFile->setSelected(false); + } + } + if(_currentLogfile) { + emit abortUpload(); + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::startLogging() +{ + if(_vehicle) { + if(_createNewLog()) { + _vehicle->startMavlinkLog(); + _logRunning = true; + _loggingCmdTryCount = 0; + _ackTimer.start(kTimeOutMilliseconds); + emit logRunningChanged(); + } + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::stopLogging() +{ + if(_vehicle) { + //-- Tell vehicle to stop sending logs + _vehicle->stopMavlinkLog(); + } + if(_logProcessor) { + _logProcessor->close(); + if(_logProcessor->record()) { + _logProcessor->record()->setWriting(false); + if(_enableAutoUpload) { + //-- Queue log for auto upload (set selected flag) + _logProcessor->record()->setSelected(true); + if(!uploading()) { + uploadLog(); + } + } + } + delete _logProcessor; + _logProcessor = NULL; + _logRunning = false; + if(_vehicle) { + //-- Setup a timer to make sure vehicle received the command + _loggingCmdTryCount = 0; + _ackTimer.start(kTimeOutMilliseconds); + } + emit logRunningChanged(); + } +} + +//----------------------------------------------------------------------------- +QHttpPart +create_form_part(const QString& name, const QString& value) +{ + QHttpPart formPart; + formPart.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"%1\"").arg(name)); + formPart.setBody(value.toUtf8()); + return formPart; +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogManager::_sendLog(const QString& logFile) +{ + QString defaultDescription = _description; + if(_description.isEmpty()) { + qCWarning(MavlinkLogManagerLog) << "Log description missing. Using defaults."; + defaultDescription = kDefaultDescr; + } + if(_emailAddress.isEmpty()) { + qCCritical(MavlinkLogManagerLog) << "User email missing."; + return false; + } + if(_uploadURL.isEmpty()) { + qCCritical(MavlinkLogManagerLog) << "Upload URL missing."; + return false; + } + QFileInfo fi(logFile); + if(!fi.exists()) { + qCCritical(MavlinkLogManagerLog) << "Log file missing:" << logFile; + return false; + } + QFile* file = new QFile(logFile); + if(!file || !file->open(QIODevice::ReadOnly)) { + if(file) { + delete file; + } + qCCritical(MavlinkLogManagerLog) << "Could not open log file:" << logFile; + return false; + } + if(!_nam) { + _nam = new QNetworkAccessManager(this); + } + QNetworkProxy savedProxy = _nam->proxy(); + QNetworkProxy tempProxy; + tempProxy.setType(QNetworkProxy::DefaultProxy); + _nam->setProxy(tempProxy); + //-- Build POST request + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart emailPart = create_form_part("email", _emailAddress); + QHttpPart descriptionPart = create_form_part("description", _description); + QHttpPart sourcePart = create_form_part("source", "QGroundControl"); + QHttpPart versionPart = create_form_part("version", _app->applicationVersion()); + QHttpPart logPart; + logPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); + logPart.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"filearg\"; filename=\"%1\"").arg(fi.fileName())); + logPart.setBodyDevice(file); + //-- Assemble request and POST it + multiPart->append(emailPart); + multiPart->append(descriptionPart); + multiPart->append(sourcePart); + multiPart->append(versionPart); + multiPart->append(logPart); + file->setParent(multiPart); + QNetworkRequest request(_uploadURL); +#if QT_VERSION > 0x050600 + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); +#endif + QNetworkReply* reply = _nam->post(request, multiPart); + connect(reply, &QNetworkReply::finished, this, &MavlinkLogManager::_uploadFinished); + connect(this, &MavlinkLogManager::abortUpload, reply, &QNetworkReply::abort); + //connect(reply, &QNetworkReply::readyRead, this, &MavlinkLogManager::_dataAvailable); + connect(reply, &QNetworkReply::uploadProgress, this, &MavlinkLogManager::_uploadProgress); + multiPart->setParent(reply); + qCDebug(MavlinkLogManagerLog) << "Log" << fi.baseName() << "Uploading." << fi.size() << "bytes."; + _nam->setProxy(savedProxy); + return true; +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogManager::_processUploadResponse(int http_code, QByteArray& data) +{ + qCDebug(MavlinkLogManagerLog) << "Uploaded response:" << QString::fromUtf8(data); + emit readyRead(data); + return http_code == 200; +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_dataAvailable() +{ + QNetworkReply* reply = qobject_cast(sender()); + if(!reply) { + return; + } + QByteArray data = reply->readAll(); + qCDebug(MavlinkLogManagerLog) << "Uploaded response data:" << QString::fromUtf8(data); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_uploadFinished() +{ + QNetworkReply* reply = qobject_cast(sender()); + if(!reply) { + return; + } + const int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QByteArray data = reply->readAll(); + if(_processUploadResponse(http_code, data)) { + qCDebug(MavlinkLogManagerLog) << "Log uploaded."; + emit succeed(); + if(_deleteAfterUpload) { + if(_currentLogfile) { + _deleteLog(_currentLogfile); + _currentLogfile = NULL; + } + } else { + if(_currentLogfile) { + _currentLogfile->setUploaded(true); + //-- Write side-car file to flag it as uploaded + QString sideCar = _makeFilename(_currentLogfile->name()); + sideCar.replace(kUlogExtension, kSidecarExtension); + FILE* f = fopen(sideCar.toLatin1().data(), "wb"); + if(f) { + fclose(f); + } + } + } + } else { + qCWarning(MavlinkLogManagerLog) << QString("Log Upload Error: %1 status: %2").arg(reply->errorString(), reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString()); + emit failed(); + } + reply->deleteLater(); + //-- Next (if any) + uploadLog(); +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_uploadProgress(qint64 bytesSent, qint64 bytesTotal) +{ + if(bytesTotal) { + qreal progress = (qreal)bytesSent / (qreal)bytesTotal; + if(_currentLogfile) { + _currentLogfile->setProgress(progress); + } + } + qCDebug(MavlinkLogManagerLog) << bytesSent << "of" << bytesTotal; +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_activeVehicleChanged(Vehicle* vehicle) +{ + //-- TODO: This is not quite right. This is being used to detect when a vehicle + // connects/disconnects. In reality, if QGC is connected to multiple vehicles, + // this is called each time the user switches from one vehicle to another. So + // far, I'm working on the assumption that multiple vehicles is a rare exception. + // For now, we only handle one log download at a time. + // Disconnect the previous one (if any) + if(_vehicle) { + disconnect(_vehicle, &Vehicle::armedChanged, this, &MavlinkLogManager::_armedChanged); + disconnect(_vehicle, &Vehicle::mavlinkLogData, this, &MavlinkLogManager::_mavlinkLogData); + disconnect(_vehicle, &Vehicle::commandLongAck, this, &MavlinkLogManager::_commandLongAck); + _vehicle = NULL; + //-- Stop logging (if that's the case) + stopLogging(); + emit canStartLogChanged(); + } + // Connect new system + if(vehicle) { + _vehicle = vehicle; + connect(_vehicle, &Vehicle::armedChanged, this, &MavlinkLogManager::_armedChanged); + connect(_vehicle, &Vehicle::mavlinkLogData, this, &MavlinkLogManager::_mavlinkLogData); + connect(_vehicle, &Vehicle::commandLongAck, this, &MavlinkLogManager::_commandLongAck); + emit canStartLogChanged(); + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_processCmdAck() +{ + if(_loggingCmdTryCount++ > 3) { + _ackTimer.stop(); + //-- Give up + if(_logRunning) { + qCWarning(MavlinkLogManagerLog) << "Start MAVLink log command had no response."; + _discardLog(); + } else { + qCWarning(MavlinkLogManagerLog) << "Stop MAVLink log command had no response."; + } + } else { + if(_vehicle) { + if(_logRunning) { + _vehicle->startMavlinkLog(); + qCWarning(MavlinkLogManagerLog) << "Start MAVLink log command sent again."; + } else { + _vehicle->stopMavlinkLog(); + qCWarning(MavlinkLogManagerLog) << "Stop MAVLink log command sent again."; + } + _ackTimer.start(kTimeOutMilliseconds); + } else { + //-- Vehicle went away on us + _ackTimer.stop(); + } + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_mavlinkLogData(Vehicle* /*vehicle*/, uint8_t /*target_system*/, uint8_t /*target_component*/, uint16_t sequence, uint8_t first_message, QByteArray data, bool /*acked*/) +{ + //-- Disable timer if we got a message before an ACK for the start command + if(_logRunning) { + _ackTimer.stop(); + } + if(_logProcessor && _logProcessor->valid()) { + if(!_logProcessor->processStreamData(sequence, first_message, data)) { + qCCritical(MavlinkLogManagerLog) << "Error writing Mavlink log file:" << _logProcessor->fileName(); + delete _logProcessor; + _logProcessor = NULL; + _logRunning = false; + _vehicle->stopMavlinkLog(); + emit logRunningChanged(); + } + } else { + qCWarning(MavlinkLogManagerLog) << "Mavlink log data received when not expected."; + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_commandLongAck(uint8_t /*compID*/, uint16_t command, uint8_t result) +{ + if(command == MAV_CMD_LOGGING_START || command == MAV_CMD_LOGGING_STOP) { + _ackTimer.stop(); + //-- Did it fail? + if(result) { + if(command == MAV_CMD_LOGGING_STOP) { + //-- Not that it could happen but... + qCWarning(MavlinkLogManagerLog) << "Stop MAVLink log command failed."; + } else { + //-- Could not start logging for some reason. + qCWarning(MavlinkLogManagerLog) << "Start MAVLink log command failed."; + _discardLog(); + } + } + } +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_discardLog() +{ + //-- Delete (empty) log file (and record) + if(_logProcessor) { + _logProcessor->close(); + if(_logProcessor->record()) { + _deleteLog(_logProcessor->record()); + } + delete _logProcessor; + _logProcessor = NULL; + } + _logRunning = false; + emit logRunningChanged(); +} + +//----------------------------------------------------------------------------- +bool +MavlinkLogManager::_createNewLog() +{ + if(_logProcessor) { + delete _logProcessor; + _logProcessor = NULL; + } + _logProcessor = new MavlinkLogProcessor; + if(_logProcessor->create(this, _logPath, _vehicle->id())) { + _insertNewLog(_logProcessor->record()); + emit logFilesChanged(); + } else { + qCCritical(MavlinkLogManagerLog) << "Could not create Mavlink log file:" << _logProcessor->fileName(); + delete _logProcessor; + _logProcessor = NULL; + } + return _logProcessor != NULL; +} + +//----------------------------------------------------------------------------- +void +MavlinkLogManager::_armedChanged(bool armed) +{ + if(_vehicle) { + if(armed) { + if(_enableAutoStart) { + startLogging(); + } + } else { + if(_logRunning && _enableAutoStart) { + stopLogging(); + } + } + } +} + +//----------------------------------------------------------------------------- +QString +MavlinkLogManager::_makeFilename(const QString& baseName) +{ + QString filePath = _logPath; + filePath += "/"; + filePath += baseName; + filePath += kUlogExtension; + return filePath; +} diff --git a/src/Vehicle/MavlinkLogManager.h b/src/Vehicle/MavlinkLogManager.h new file mode 100644 index 0000000000000000000000000000000000000000..b39d2528c89245715c7f141393d27552ea70425a --- /dev/null +++ b/src/Vehicle/MavlinkLogManager.h @@ -0,0 +1,207 @@ +/**************************************************************************** + * + * (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. + * + ****************************************************************************/ + + +#ifndef MavlinkLogManager_H +#define MavlinkLogManager_H + +#include + +#include "QmlObjectListModel.h" +#include "QGCLoggingCategory.h" +#include "QGCToolbox.h" +#include "Vehicle.h" + +Q_DECLARE_LOGGING_CATEGORY(MavlinkLogManagerLog) + +class QNetworkAccessManager; +class MavlinkLogManager; + +//----------------------------------------------------------------------------- +class MavlinkLogFiles : public QObject +{ + Q_OBJECT +public: + MavlinkLogFiles (MavlinkLogManager* manager, const QString& filePath, bool newFile = false); + + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(quint32 size READ size NOTIFY sizeChanged) + Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) + Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) + Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged) + Q_PROPERTY(bool writing READ writing NOTIFY writingChanged) + Q_PROPERTY(bool uploaded READ uploaded NOTIFY uploadedChanged) + + QString name () { return _name; } + quint32 size () { return _size; } + bool selected () { return _selected; } + bool uploading () { return _uploading; } + qreal progress () { return _progress; } + bool writing () { return _writing; } + bool uploaded () { return _uploaded; } + + void setSelected (bool selected); + void setUploading (bool uploading); + void setProgress (qreal progress); + void setWriting (bool writing); + void setSize (quint32 size); + void setUploaded (bool uploaded); + +signals: + void sizeChanged (); + void selectedChanged (); + void uploadingChanged (); + void progressChanged (); + void writingChanged (); + void uploadedChanged (); + +private: + MavlinkLogManager* _manager; + QString _name; + quint32 _size; + bool _selected; + bool _uploading; + qreal _progress; + bool _writing; + bool _uploaded; +}; + +//----------------------------------------------------------------------------- +class MavlinkLogProcessor +{ +public: + MavlinkLogProcessor(); + ~MavlinkLogProcessor(); + void close (); + bool valid (); + bool create (MavlinkLogManager *manager, const QString path, uint8_t id); + MavlinkLogFiles* record () { return _record; } + QString fileName () { return _fileName; } + bool processStreamData(uint16_t _sequence, uint8_t first_message, QByteArray data); +private: + bool _checkSequence(uint16_t seq, int &num_drops); + QByteArray _writeUlogMessage(QByteArray &data); + void _writeData(void* data, int len); +private: + FILE* _fd; + quint32 _written; + int _sequence; + int _numDrops; + bool _gotHeader; + bool _error; + QByteArray _ulogMessage; + QString _fileName; + MavlinkLogFiles* _record; +}; + +//----------------------------------------------------------------------------- +class MavlinkLogManager : public QGCTool +{ + Q_OBJECT + +public: + MavlinkLogManager (QGCApplication* app); + ~MavlinkLogManager (); + + Q_PROPERTY(QString emailAddress READ emailAddress WRITE setEmailAddress NOTIFY emailAddressChanged) + Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) + Q_PROPERTY(QString uploadURL READ uploadURL WRITE setUploadURL NOTIFY uploadURLChanged) + Q_PROPERTY(bool enableAutoUpload READ enableAutoUpload WRITE setEnableAutoUpload NOTIFY enableAutoUploadChanged) + Q_PROPERTY(bool enableAutoStart READ enableAutoStart WRITE setEnableAutoStart NOTIFY enableAutoStartChanged) + Q_PROPERTY(bool deleteAfterUpload READ deleteAfterUpload WRITE setDeleteAfterUpload NOTIFY deleteAfterUploadChanged) + Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) + Q_PROPERTY(bool logRunning READ logRunning NOTIFY logRunningChanged) + Q_PROPERTY(bool canStartLog READ canStartLog NOTIFY canStartLogChanged) + Q_PROPERTY(QmlObjectListModel* logFiles READ logFiles NOTIFY logFilesChanged) + + Q_INVOKABLE void uploadLog (); + Q_INVOKABLE void deleteLog (); + Q_INVOKABLE void cancelUpload (); + Q_INVOKABLE void startLogging (); + Q_INVOKABLE void stopLogging (); + + QString emailAddress () { return _emailAddress; } + QString description () { return _description; } + QString uploadURL () { return _uploadURL; } + bool enableAutoUpload () { return _enableAutoUpload; } + bool enableAutoStart () { return _enableAutoStart; } + bool uploading (); + bool logRunning () { return _logRunning; } + bool canStartLog () { return _vehicle != NULL; } + bool deleteAfterUpload () { return _deleteAfterUpload; } + + QmlObjectListModel* logFiles () { return &_logFiles; } + + void setEmailAddress (QString email); + void setDescription (QString description); + void setUploadURL (QString url); + void setEnableAutoUpload (bool enable); + void setEnableAutoStart (bool enable); + void setDeleteAfterUpload(bool enable); + + // Override from QGCTool + void setToolbox (QGCToolbox *toolbox); + +signals: + void emailAddressChanged (); + void descriptionChanged (); + void uploadURLChanged (); + void enableAutoUploadChanged (); + void enableAutoStartChanged (); + void logFilesChanged (); + void selectedCountChanged (); + void uploadingChanged (); + void readyRead (QByteArray data); + void failed (); + void succeed (); + void abortUpload (); + void logRunningChanged (); + void canStartLogChanged (); + void deleteAfterUploadChanged (); + +private slots: + void _uploadFinished (); + void _dataAvailable (); + void _uploadProgress (qint64 bytesSent, qint64 bytesTotal); + void _activeVehicleChanged (Vehicle* vehicle); + void _mavlinkLogData (Vehicle* vehicle, uint8_t target_system, uint8_t target_component, uint16_t sequence, uint8_t first_message, QByteArray data, bool acked); + void _armedChanged (bool armed); + void _commandLongAck (uint8_t compID, uint16_t command, uint8_t result); + void _processCmdAck (); + +private: + bool _sendLog (const QString& logFile); + bool _processUploadResponse (int http_code, QByteArray &data); + bool _createNewLog (); + int _getFirstSelected (); + void _insertNewLog (MavlinkLogFiles* newLog); + void _deleteLog (MavlinkLogFiles* log); + void _discardLog (); + QString _makeFilename (const QString& baseName); + +private: + QString _description; + QString _emailAddress; + QString _uploadURL; + QString _logPath; + bool _enableAutoUpload; + bool _enableAutoStart; + QNetworkAccessManager* _nam; + QmlObjectListModel _logFiles; + MavlinkLogFiles* _currentLogfile; + Vehicle* _vehicle; + bool _logRunning; + bool _loggingDisabled; + MavlinkLogProcessor* _logProcessor; + bool _deleteAfterUpload; + int _loggingCmdTryCount; + QTimer _ackTimer; +}; + +#endif diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 09480d7bdbe047dc33ef6df9e96f45923e4e4f14..c9ed3a839b49e0379792d9b3f3328b8f4d3e8ec1 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -400,6 +400,7 @@ Vehicle::resetCounters() void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t message) { + if (message.sysid != _id && message.sysid != 0) { return; } @@ -480,7 +481,7 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes _handleCommandAck(message); break; case MAVLINK_MSG_ID_AUTOPILOT_VERSION: - _handleAutopilotVersion(message); + _handleAutopilotVersion(link, message); break; case MAVLINK_MSG_ID_WIND_COV: _handleWindCov(message); @@ -488,6 +489,12 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes case MAVLINK_MSG_ID_HIL_ACTUATOR_CONTROLS: _handleHilActuatorControls(message); break; + case MAVLINK_MSG_ID_LOGGING_DATA: + _handleMavlinkLoggingData(message); + break; + case MAVLINK_MSG_ID_LOGGING_DATA_ACKED: + _handleMavlinkLoggingDataAcked(message); + break; // Following are ArduPilot dialect messages @@ -501,11 +508,17 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes _uas->receiveMessage(message); } -void Vehicle::_handleAutopilotVersion(mavlink_message_t& message) +void Vehicle::_handleAutopilotVersion(LinkInterface *link, mavlink_message_t& message) { mavlink_autopilot_version_t autopilotVersion; mavlink_msg_autopilot_version_decode(&message, &autopilotVersion); + bool isMavlink2 = (autopilotVersion.capabilities & MAV_PROTOCOL_CAPABILITY_MAVLINK2) != 0; + if(isMavlink2) { + mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(link->mavlinkChannel()); + mavlinkStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1; + } + if (autopilotVersion.flight_sw_version != 0) { int majorVersion, minorVersion, patchVersion; FIRMWARE_VERSION_TYPE versionType; @@ -549,9 +562,14 @@ void Vehicle::_handleCommandAck(mavlink_message_t& message) emit commandLongAck(message.compid, ack.command, ack.result); - if (ack.command == MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES) { - // Disregard failures - return; + // Disregard failures for these (handled above) + switch (ack.command) { + case MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES: + case MAV_CMD_LOGGING_START: + case MAV_CMD_LOGGING_STOP: + return; + default: + break; } QString commandName = qgcApp()->toolbox()->missionCommandTree()->friendlyName((MAV_CMD)ack.command); @@ -1959,6 +1977,62 @@ VehicleGPSFactGroup::VehicleGPSFactGroup(QObject* parent) _courseOverGroundFact.setRawValue(std::numeric_limits::quiet_NaN()); } +//----------------------------------------------------------------------------- +void +Vehicle::startMavlinkLog() +{ + doCommandLong(defaultComponentId(), MAV_CMD_LOGGING_START); +} + +//----------------------------------------------------------------------------- +void +Vehicle::stopMavlinkLog() +{ + doCommandLong(defaultComponentId(), MAV_CMD_LOGGING_STOP); +} + +//----------------------------------------------------------------------------- +void +Vehicle::_ackMavlinkLogData(uint16_t sequence) +{ + mavlink_message_t msg; + mavlink_logging_ack_t ack; + ack.sequence = sequence; + ack.target_component = defaultComponentId(); + ack.target_system = id(); + mavlink_msg_logging_ack_encode_chan( + _mavlink->getSystemId(), + _mavlink->getComponentId(), + priorityLink()->mavlinkChannel(), + &msg, + &ack); + sendMessageOnLink(priorityLink(), msg); +} + +//----------------------------------------------------------------------------- +void +Vehicle::_handleMavlinkLoggingData(mavlink_message_t& message) +{ + mavlink_logging_data_t log; + mavlink_msg_logging_data_decode(&message, &log); + emit mavlinkLogData(this, log.target_system, log.target_component, log.sequence, + log.first_message_offset, QByteArray((const char*)log.data, log.length), false); +} + +//----------------------------------------------------------------------------- +void +Vehicle::_handleMavlinkLoggingDataAcked(mavlink_message_t& message) +{ + mavlink_logging_data_t log; + mavlink_msg_logging_data_decode(&message, &log); + _ackMavlinkLogData(log.sequence); + emit mavlinkLogData(this, log.target_system, log.target_component, log.sequence, + log.first_message_offset, QByteArray((const char*)log.data, log.length), true); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + void VehicleGPSFactGroup::setVehicle(Vehicle* vehicle) { _vehicle = vehicle; diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index c2b3bfbed2d9ea30c2a06d5cd4bd19b38202f6c7..0f6243519922c1ddfb48bea6c2eae67d8a04e99c 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -485,6 +485,10 @@ public: int flowImageIndex() { return _flowImageIndex; } + //-- Mavlink Logging + void startMavlinkLog(); + void stopMavlinkLog(); + /// Requests the specified data stream from the vehicle /// @param stream Stream which is being requested /// @param rate Rate at which to send stream in Hz @@ -638,6 +642,9 @@ signals: void mavlinkScaledImu2(mavlink_message_t message); void mavlinkScaledImu3(mavlink_message_t message); + // Mavlink Log Download + void mavlinkLogData (Vehicle* vehicle, uint8_t target_system, uint8_t target_component, uint16_t sequence, uint8_t first_message, QByteArray data, bool acked); + private slots: void _mavlinkMessageReceived(LinkInterface* link, mavlink_message_t message); void _linkInactiveOrDeleted(LinkInterface* link); @@ -685,7 +692,7 @@ private: void _handleVibration(mavlink_message_t& message); void _handleExtendedSysState(mavlink_message_t& message); void _handleCommandAck(mavlink_message_t& message); - void _handleAutopilotVersion(mavlink_message_t& message); + void _handleAutopilotVersion(LinkInterface* link, mavlink_message_t& message); void _handleHilActuatorControls(mavlink_message_t& message); void _missionManagerError(int errorCode, const QString& errorMsg); void _geoFenceManagerError(int errorCode, const QString& errorMsg); @@ -695,6 +702,9 @@ private: void _connectionActive(void); void _say(const QString& text); QString _vehicleIdSpeech(void); + void _handleMavlinkLoggingData(mavlink_message_t& message); + void _handleMavlinkLoggingDataAcked(mavlink_message_t& message); + void _ackMavlinkLogData(uint16_t sequence); private: int _id; ///< Mavlink system id diff --git a/src/comm/MAVLinkProtocol.cc b/src/comm/MAVLinkProtocol.cc index 3e238138972e14698cb0270f8cd61c89c2fb8db3..bf1ce9852adf17a4a4075272d46b4fd0ac9dfdd7 100644 --- a/src/comm/MAVLinkProtocol.cc +++ b/src/comm/MAVLinkProtocol.cc @@ -73,7 +73,7 @@ MAVLinkProtocol::MAVLinkProtocol(QGCApplication* app) MAVLinkProtocol::~MAVLinkProtocol() { storeSettings(); - + #ifndef __mobile__ _closeLogFile(); #endif @@ -162,7 +162,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) if (!_linkMgr->links()->contains(link)) { return; } - + // receiveMutex.lock(); mavlink_message_t message; mavlink_status_t status; @@ -214,6 +214,8 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) { decodedFirstPacket = true; + /* + * Handled in Vehicle.cc now. 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() << "switch to mavlink 2.0" << mavlinkStatus << mavlinkChannel << mavlinkStatus->flags; @@ -222,6 +224,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) qDebug() << "switch to mavlink 1.0" << mavlinkStatus << mavlinkChannel << mavlinkStatus->flags; mavlinkStatus->flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1; } + */ if(message.msgid == MAVLINK_MSG_ID_RADIO_STATUS) { @@ -255,7 +258,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) #ifndef __mobile__ // Log data - + if (!_logSuspendError && !_logSuspendReplay && _tempLogFile.isOpen()) { uint8_t buf[MAVLINK_MAX_PACKET_LEN+sizeof(quint64)]; @@ -280,7 +283,7 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) _stopLogging(); _logSuspendError = true; } - + // Check for the vehicle arming going by. This is used to trigger log save. if (!_logPromptForSave && message.msgid == MAVLINK_MSG_ID_HEARTBEAT) { mavlink_heartbeat_t state; @@ -412,7 +415,7 @@ bool MAVLinkProtocol::_closeLogFile(void) return true; } } - + return false; } @@ -458,11 +461,11 @@ void MAVLinkProtocol::_stopLogging(void) 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) { @@ -488,10 +491,10 @@ void MAVLinkProtocol::suspendLogForReplay(bool 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()); } diff --git a/src/uas/UASInterface.h b/src/uas/UASInterface.h index cf022f5932c4c6955baf1b47d30bf84130678a2b..10ad42fe840d4c3e28a27d8b9f1e17e0d1e23f09 100644 --- a/src/uas/UASInterface.h +++ b/src/uas/UASInterface.h @@ -307,6 +307,7 @@ signals: // Log Download Signals void logEntry (UASInterface* uas, uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num); void logData (UASInterface* uas, uint32_t ofs, uint16_t id, uint8_t count, const uint8_t* data); + }; Q_DECLARE_INTERFACE(UASInterface, "org.qgroundcontrol/1.0") diff --git a/src/ui/preferences/MavlinkSettings.qml b/src/ui/preferences/MavlinkSettings.qml index 37ac33284738907b82949d711ebc5f5686f32259..b98e2b5c3956df7fc9f15ef176d550c10c3f0a5f 100644 --- a/src/ui/preferences/MavlinkSettings.qml +++ b/src/ui/preferences/MavlinkSettings.qml @@ -25,56 +25,461 @@ Rectangle { color: qgcPal.window anchors.fill: parent + property real _labelWidth: ScreenTools.defaultFontPixelWidth * 28 + property real _valueWidth: ScreenTools.defaultFontPixelWidth * 24 + property int _selectedCount: 0 + property real _columnSpacing: ScreenTools.defaultFontPixelHeight * 0.25 + property bool _uploadedSelected: false + QGCPalette { id: qgcPal } + Connections { + target: QGroundControl.mavlinkLogManager + onSelectedCountChanged: { + _uploadedSelected = false + var selected = 0 + for(var i = 0; i < QGroundControl.mavlinkLogManager.logFiles.count; i++) { + var logFile = QGroundControl.mavlinkLogManager.logFiles.get(i) + if(logFile.selected) { + selected++ + //-- If an uploaded file is selected, disable "Upload" button + if(logFile.uploaded) { + _uploadedSelected = true + } + } + } + _selectedCount = selected + } + } + + MessageDialog { + id: emptyEmailDialog + visible: false + icon: StandardIcon.Warning + standardButtons: StandardButton.Close + title: qsTr("MAVLink Logging") + text: qsTr("Please enter an email address before uploading MAVLink log files.") + } + QGCFlickable { clip: true anchors.fill: parent anchors.margins: ScreenTools.defaultFontPixelWidth contentHeight: settingsColumn.height contentWidth: settingsColumn.width + flickableDirection: Flickable.VerticalFlick Column { id: settingsColumn - spacing: ScreenTools.defaultFontPixelHeight + width: __mavlinkRoot.width + spacing: ScreenTools.defaultFontPixelHeight * 0.5 anchors.margins: ScreenTools.defaultFontPixelWidth - anchors.left: parent.left - anchors.top: parent.top //----------------------------------------------------------------- - //-- System ID - Row { - spacing: ScreenTools.defaultFontPixelWidth + //-- Ground Station + Item { + width: __mavlinkRoot.width * 0.8 + height: gcsLabel.height + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter QGCLabel { - text: qsTr("Ground Station MavLink System ID:") - anchors.verticalCenter: parent.verticalCenter + id: gcsLabel + text: qsTr("Ground Station") + font.family: ScreenTools.demiboldFontFamily } - QGCTextField { - id: sysidField - text: QGroundControl.mavlinkSystemID.toString() - width: ScreenTools.defaultFontPixelWidth * 6 - inputMethodHints: Qt.ImhFormattedNumbersOnly - anchors.verticalCenter: parent.verticalCenter - onEditingFinished: { - QGroundControl.mavlinkSystemID = parseInt(sysidField.text) + } + Rectangle { + height: gcsColumn.height + (ScreenTools.defaultFontPixelHeight * 2) + width: __mavlinkRoot.width * 0.8 + color: qgcPal.windowShade + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + Column { + id: gcsColumn + spacing: _columnSpacing + anchors.centerIn: parent + Row { + spacing: ScreenTools.defaultFontPixelWidth + QGCLabel { + width: _labelWidth + anchors.baseline: sysidField.baseline + text: qsTr("MavLink System ID:") + } + QGCTextField { + id: sysidField + text: QGroundControl.mavlinkSystemID.toString() + width: _valueWidth + inputMethodHints: Qt.ImhFormattedNumbersOnly + anchors.verticalCenter: parent.verticalCenter + onEditingFinished: { + QGroundControl.mavlinkSystemID = parseInt(sysidField.text) + } + } + } + //----------------------------------------------------------------- + //-- Mavlink Heartbeats + QGCCheckBox { + text: qsTr("Emit heartbeat") + checked: QGroundControl.multiVehicleManager.gcsHeartBeatEnabled + onClicked: { + QGroundControl.multiVehicleManager.gcsHeartBeatEnabled = checked + } + } + //----------------------------------------------------------------- + //-- Mavlink Version Check + QGCCheckBox { + text: qsTr("Only accept MAVs with same protocol version") + checked: QGroundControl.isVersionCheckEnabled + onClicked: { + QGroundControl.isVersionCheckEnabled = checked + } } } } //----------------------------------------------------------------- - //-- Mavlink Heartbeats - QGCCheckBox { - text: qsTr("Emit heartbeat") - checked: QGroundControl.multiVehicleManager.gcsHeartBeatEnabled - onClicked: { - QGroundControl.multiVehicleManager.gcsHeartBeatEnabled = checked + //-- Mavlink Logging + Item { + width: __mavlinkRoot.width * 0.8 + height: mavlogLabel.height + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + QGCLabel { + id: mavlogLabel + text: qsTr("Vehicle Mavlink Logging") + font.family: ScreenTools.demiboldFontFamily + } + } + Rectangle { + height: mavlogColumn.height + (ScreenTools.defaultFontPixelHeight * 2) + width: __mavlinkRoot.width * 0.8 + color: qgcPal.windowShade + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + Column { + id: mavlogColumn + width: gcsColumn.width + spacing: _columnSpacing + anchors.centerIn: parent + //----------------------------------------------------------------- + //-- Manual Start/Stop + Row { + spacing: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + QGCLabel { + width: _labelWidth + text: qsTr("Manual Start/Stop:") + anchors.verticalCenter: parent.verticalCenter + } + QGCButton { + text: qsTr("Start Logging") + width: (_valueWidth * 0.5) - (ScreenTools.defaultFontPixelWidth * 0.5) + enabled: !QGroundControl.mavlinkLogManager.logRunning && QGroundControl.mavlinkLogManager.canStartLog + onClicked: QGroundControl.mavlinkLogManager.startLogging() + anchors.verticalCenter: parent.verticalCenter + } + QGCButton { + text: qsTr("Stop Logging") + width: (_valueWidth * 0.5) - (ScreenTools.defaultFontPixelWidth * 0.5) + enabled: QGroundControl.mavlinkLogManager.logRunning + onClicked: QGroundControl.mavlinkLogManager.stopLogging() + anchors.verticalCenter: parent.verticalCenter + } + } + //----------------------------------------------------------------- + //-- Enable auto log on arming + QGCCheckBox { + text: qsTr("Enable automatic logging") + checked: QGroundControl.mavlinkLogManager.enableAutoStart + onClicked: { + QGroundControl.mavlinkLogManager.enableAutoStart = checked + } + } } } //----------------------------------------------------------------- - //-- Mavlink Version Check - QGCCheckBox { - text: qsTr("Only accept MAVs with same protocol version") - checked: QGroundControl.isVersionCheckEnabled - onClicked: { - QGroundControl.isVersionCheckEnabled = checked + //-- Mavlink Logging + Item { + width: __mavlinkRoot.width * 0.8 + height: logLabel.height + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + QGCLabel { + id: logLabel + text: qsTr("Mavlink Log Uploads") + font.family: ScreenTools.demiboldFontFamily + } + } + Rectangle { + height: logColumn.height + (ScreenTools.defaultFontPixelHeight * 2) + width: __mavlinkRoot.width * 0.8 + color: qgcPal.windowShade + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + Column { + id: logColumn + spacing: _columnSpacing + anchors.centerIn: parent + //----------------------------------------------------------------- + //-- Email address Field + Row { + spacing: ScreenTools.defaultFontPixelWidth + QGCLabel { + width: _labelWidth + anchors.baseline: emailField.baseline + text: qsTr("Email address for Log Upload:") + } + QGCTextField { + id: emailField + text: QGroundControl.mavlinkLogManager.emailAddress + width: _valueWidth + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhEmailCharactersOnly + anchors.verticalCenter: parent.verticalCenter + onEditingFinished: { + QGroundControl.mavlinkLogManager.emailAddress = emailField.text + if(emailField.text === "") { + autoUploadCheck.checked = false + QGroundControl.mavlinkLogManager.enableAutoUpload = false + console.log("forcing enableAutoUpload to false") + } + } + } + } + //----------------------------------------------------------------- + //-- Description Field + Row { + spacing: ScreenTools.defaultFontPixelWidth + QGCLabel { + width: _labelWidth + anchors.baseline: descField.baseline + text: qsTr("Default Description:") + } + QGCTextField { + id: descField + text: QGroundControl.mavlinkLogManager.description + width: _valueWidth + anchors.verticalCenter: parent.verticalCenter + onEditingFinished: { + QGroundControl.mavlinkLogManager.description = descField.text + } + } + } + //----------------------------------------------------------------- + //-- Upload URL + Row { + spacing: ScreenTools.defaultFontPixelWidth + QGCLabel { + width: _labelWidth + anchors.baseline: urlField.baseline + text: qsTr("Default Upload URL") + } + QGCTextField { + id: urlField + text: QGroundControl.mavlinkLogManager.uploadURL + width: _valueWidth + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhUrlCharactersOnly + anchors.verticalCenter: parent.verticalCenter + onEditingFinished: { + QGroundControl.mavlinkLogManager.uploadURL = urlField.text + } + } + } + //----------------------------------------------------------------- + //-- Automatic Upload + QGCCheckBox { + id: autoUploadCheck + text: qsTr("Enable automatic log uploads") + checked: QGroundControl.mavlinkLogManager.enableAutoUpload + onClicked: { + QGroundControl.mavlinkLogManager.emailAddress = emailField.text + if(checked && QGroundControl.mavlinkLogManager.emailAddress === "") { + checked = false + emptyEmailDialog.open() + } else { + QGroundControl.mavlinkLogManager.enableAutoUpload = checked + } + } + } + //----------------------------------------------------------------- + //-- Delete log after upload + QGCCheckBox { + text: qsTr("Delete log file after uploading") + checked: QGroundControl.mavlinkLogManager.deleteAfterUpload + enabled: autoUploadCheck.checked + onClicked: { + QGroundControl.mavlinkLogManager.deleteAfterUpload = checked + } + } + } + } + //----------------------------------------------------------------- + //-- Log Files + Item { + width: __mavlinkRoot.width * 0.8 + height: logFilesLabel.height + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + QGCLabel { + id: logFilesLabel + text: qsTr("Saved Log Files") + font.family: ScreenTools.demiboldFontFamily + } + } + Rectangle { + height: logFilesColumn.height + (ScreenTools.defaultFontPixelHeight * 2) + width: __mavlinkRoot.width * 0.8 + color: qgcPal.windowShade + anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + Column { + id: logFilesColumn + spacing: _columnSpacing * 4 + anchors.centerIn: parent + width: ScreenTools.defaultFontPixelWidth * 68 + Rectangle { + width: ScreenTools.defaultFontPixelWidth * 64 + height: ScreenTools.defaultFontPixelHeight * 14 + anchors.horizontalCenter: parent.horizontalCenter + color: qgcPal.window + border.color: qgcPal.text + border.width: 0.5 + ListView { + width: ScreenTools.defaultFontPixelWidth * 56 + height: ScreenTools.defaultFontPixelHeight * 12 + anchors.centerIn: parent + orientation: ListView.Vertical + model: QGroundControl.mavlinkLogManager.logFiles + clip: true + delegate: Rectangle { + width: ScreenTools.defaultFontPixelWidth * 52 + height: selectCheck.height + color: qgcPal.window + Row { + width: ScreenTools.defaultFontPixelWidth * 50 + anchors.centerIn: parent + spacing: ScreenTools.defaultFontPixelWidth + QGCCheckBox { + id: selectCheck + width: ScreenTools.defaultFontPixelWidth * 4 + checked: object.selected + enabled: !object.writing && !object.uploading + anchors.verticalCenter: parent.verticalCenter + onClicked: { + object.selected = checked + } + } + QGCLabel { + text: object.name + width: ScreenTools.defaultFontPixelWidth * 28 + color: object.writing ? qgcPal.warningText : qgcPal.text + anchors.verticalCenter: parent.verticalCenter + } + QGCLabel { + text: Number(object.size).toLocaleString(Qt.locale(), 'f', 0) + visible: !object.uploading && !object.uploaded + width: ScreenTools.defaultFontPixelWidth * 20; + color: object.writing ? qgcPal.warningText : qgcPal.text + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + QGCLabel { + text: "Uploaded" + visible: object.uploaded + width: ScreenTools.defaultFontPixelWidth * 20; + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + ProgressBar { + visible: object.uploading && !object.uploaded + width: ScreenTools.defaultFontPixelWidth * 20; + height: ScreenTools.defaultFontPixelHeight + anchors.verticalCenter: parent.verticalCenter + minimumValue: 0 + maximumValue: 100 + value: object.progress * 100.0 + } + } + } + } + } + Row { + spacing: ScreenTools.defaultFontPixelWidth + anchors.horizontalCenter: parent.horizontalCenter + QGCButton { + text: "Check All" + enabled: !QGroundControl.mavlinkLogManager.uploading && !QGroundControl.mavlinkLogManager.logRunning + onClicked: { + for(var i = 0; i < QGroundControl.mavlinkLogManager.logFiles.count; i++) { + var logFile = QGroundControl.mavlinkLogManager.logFiles.get(i) + logFile.selected = true + } + } + } + QGCButton { + text: "Check None" + enabled: !QGroundControl.mavlinkLogManager.uploading && !QGroundControl.mavlinkLogManager.logRunning + onClicked: { + for(var i = 0; i < QGroundControl.mavlinkLogManager.logFiles.count; i++) { + var logFile = QGroundControl.mavlinkLogManager.logFiles.get(i) + logFile.selected = false + } + } + } + QGCButton { + text: "Delete Selected" + enabled: _selectedCount > 0 && !QGroundControl.mavlinkLogManager.uploading && !QGroundControl.mavlinkLogManager.logRunning + onClicked: deleteDialog.open() + MessageDialog { + id: deleteDialog + visible: false + icon: StandardIcon.Warning + standardButtons: StandardButton.Yes | StandardButton.No + title: qsTr("Delete Selected Log Files") + text: qsTr("Confirm deleting selected log files?") + onYes: { + QGroundControl.mavlinkLogManager.deleteLog() + } + } + } + QGCButton { + text: "Upload Selected" + enabled: _selectedCount > 0 && !QGroundControl.mavlinkLogManager.uploading && !QGroundControl.mavlinkLogManager.logRunning && !_uploadedSelected + visible: !QGroundControl.mavlinkLogManager.uploading + onClicked: { + QGroundControl.mavlinkLogManager.emailAddress = emailField.text + if(QGroundControl.mavlinkLogManager.emailAddress === "") + emptyEmailDialog.open() + else + uploadDialog.open() + } + MessageDialog { + id: uploadDialog + visible: false + icon: StandardIcon.Question + standardButtons: StandardButton.Yes | StandardButton.No + title: qsTr("Upload Selected Log Files") + text: qsTr("Confirm uploading selected log files?") + onYes: { + QGroundControl.mavlinkLogManager.uploadLog() + } + } + } + QGCButton { + text: "Cancel" + enabled: QGroundControl.mavlinkLogManager.uploading && !QGroundControl.mavlinkLogManager.logRunning + visible: QGroundControl.mavlinkLogManager.uploading + onClicked: cancelDialog.open() + MessageDialog { + id: cancelDialog + visible: false + icon: StandardIcon.Warning + standardButtons: StandardButton.Yes | StandardButton.No + title: qsTr("Cancel Upload") + text: qsTr("Confirm canceling the upload process?") + onYes: { + QGroundControl.mavlinkLogManager.cancelUpload() + } + } + } + } } } }