diff --git a/qgroundcontrol.pro b/qgroundcontrol.pro index 2174a90cbca104795d9d5ead7645e20e90612192..f226a0593b820473686968c2babfcbf23871cbcc 100644 --- a/qgroundcontrol.pro +++ b/qgroundcontrol.pro @@ -356,6 +356,8 @@ HEADERS += \ src/VehicleSetup/JoystickConfigController.h \ src/ViewWidgets/CustomCommandWidget.h \ src/ViewWidgets/CustomCommandWidgetController.h \ + src/ViewWidgets/LogDownload.h \ + src/ViewWidgets/LogDownloadController.h \ src/ViewWidgets/ViewWidgetController.h \ } @@ -468,6 +470,8 @@ SOURCES += \ src/VehicleSetup/JoystickConfigController.cc \ src/ViewWidgets/CustomCommandWidget.cc \ src/ViewWidgets/CustomCommandWidgetController.cc \ + src/ViewWidgets/LogDownload.cc \ + src/ViewWidgets/LogDownloadController.cc \ src/ViewWidgets/ViewWidgetController.cc \ } diff --git a/qgroundcontrol.qrc b/qgroundcontrol.qrc index 8d3a851918ef2d62c91d5884dab6571e0257b06d..37046ce9762a649de9d10aaf7142df41f572618a 100644 --- a/qgroundcontrol.qrc +++ b/qgroundcontrol.qrc @@ -11,6 +11,7 @@ src/AutoPilotPlugins/APM/APMAirframeComponent.qml src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml src/ViewWidgets/CustomCommandWidget.qml + src/ViewWidgets/LogDownload.qml src/VehicleSetup/FirmwareUpgrade.qml src/FlightDisplay/FlightDisplayView.qml src/AutoPilotPlugins/PX4/FlightModesComponent.qml diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index f7922cbd6845b7b370ce1933cb683da24af8fa21..8a348817a808ebb1a9510e9f6b2d4a991ff5175c 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -95,6 +95,7 @@ #include "FlightDisplayViewController.h" #include "VideoSurface.h" #include "VideoReceiver.h" +#include "LogDownloadController.h" #ifndef __ios__ #include "SerialLink.h" @@ -377,7 +378,7 @@ void QGCApplication::_initCommon(void) qmlRegisterUncreatableType ("QGroundControl.JoystickManager", 1, 0, "JoystickManager", "Reference only"); qmlRegisterUncreatableType ("QGroundControl.JoystickManager", 1, 0, "Joystick", "Reference only"); - qmlRegisterType ("QGroundControl.Controllers", 1, 0, "ParameterEditorController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "ParameterEditorController"); qmlRegisterType ("QGroundControl.Controllers", 1, 0, "APMFlightModesComponentController"); qmlRegisterType ("QGroundControl.Controllers", 1, 0, "FlightModesComponentController"); qmlRegisterType ("QGroundControl.Controllers", 1, 0, "APMAirframeComponentController"); @@ -396,6 +397,7 @@ void QGCApplication::_initCommon(void) qmlRegisterType ("QGroundControl.Controllers", 1, 0, "CustomCommandWidgetController"); qmlRegisterType ("QGroundControl.Controllers", 1, 0, "FirmwareUpgradeController"); qmlRegisterType ("QGroundControl.Controllers", 1, 0, "JoystickConfigController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "LogDownloadController"); #endif // Register Qml Singletons diff --git a/src/QGCDockWidget.cc b/src/QGCDockWidget.cc index b17cbc943399f010de9d401cf5ce8dea7d5468df..0adb405e62e11b847ea01efbf6946fce143e7191 100644 --- a/src/QGCDockWidget.cc +++ b/src/QGCDockWidget.cc @@ -36,7 +36,6 @@ QGCDockWidget::QGCDockWidget(const QString& title, QAction* action, QWidget* par if (action) { setWindowTitle(title); setWindowFlags(Qt::Tool); - loadSettings(); } } @@ -55,11 +54,11 @@ void QGCDockWidget::loadSettings(void) { if (_action) { QSettings settings; - settings.beginGroup(_settingsGroup); if (settings.contains(_title)) { restoreGeometry(settings.value(_title).toByteArray()); } + settings.endGroup(); } } @@ -67,8 +66,8 @@ void QGCDockWidget::saveSettings(void) { if (_action) { QSettings settings; - settings.beginGroup(_settingsGroup); settings.setValue(_title, saveGeometry()); + settings.endGroup(); } } diff --git a/src/ViewWidgets/LogDownload.cc b/src/ViewWidgets/LogDownload.cc new file mode 100644 index 0000000000000000000000000000000000000000..f30eee3dca92a6966dfbe918e40da60384e2dcb3 --- /dev/null +++ b/src/ViewWidgets/LogDownload.cc @@ -0,0 +1,33 @@ +/*===================================================================== + +QGroundControl Open Source Ground Control Station + +(c) 2009, 2010 QGROUNDCONTROL PROJECT + +This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + +======================================================================*/ + +#include "LogDownload.h" + +LogDownload::LogDownload(const QString& title, QAction* action, QWidget *parent) : + QGCQmlWidgetHolder(title, action, parent) +{ + Q_UNUSED(title); + Q_UNUSED(action); + setSource(QUrl::fromUserInput("qrc:/qml/LogDownload.qml")); + loadSettings(); +} diff --git a/src/ViewWidgets/LogDownload.h b/src/ViewWidgets/LogDownload.h new file mode 100644 index 0000000000000000000000000000000000000000..d22a3f231fc411b290d08ce766802cae6ec8822a --- /dev/null +++ b/src/ViewWidgets/LogDownload.h @@ -0,0 +1,37 @@ +/*===================================================================== + +QGroundControl Open Source Ground Control Station + +(c) 2009, 2015 QGROUNDCONTROL PROJECT + +This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + +======================================================================*/ + +#ifndef LogDownload_H +#define LogDownload_H + +#include "QGCQmlWidgetHolder.h" + +class LogDownload : public QGCQmlWidgetHolder +{ + Q_OBJECT + +public: + LogDownload(const QString& title, QAction* action, QWidget *parent = 0); +}; + +#endif diff --git a/src/ViewWidgets/LogDownload.qml b/src/ViewWidgets/LogDownload.qml new file mode 100644 index 0000000000000000000000000000000000000000..e6092351eeeda32a31f074b1abf96bec91f06771 --- /dev/null +++ b/src/ViewWidgets/LogDownload.qml @@ -0,0 +1,210 @@ +/*===================================================================== + +QGroundControl Open Source Ground Control Station + +(c) 2009, 2015 QGROUNDCONTROL PROJECT + +This file is part of the QGROUNDCONTROL project + +QGROUNDCONTROL is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +QGROUNDCONTROL is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with QGROUNDCONTROL. If not, see . + +======================================================================*/ + +import QtQuick 2.5 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick.Dialogs 1.2 + +import QGroundControl.Palette 1.0 +import QGroundControl.Controls 1.0 +import QGroundControl.Controllers 1.0 +import QGroundControl.ScreenTools 1.0 + +QGCView { + viewPanel: panel + + property real _margins: ScreenTools.defaultFontPixelHeight + + function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",").replace(/,,/g, ","); + } + + LogDownloadController { + id: controller + factPanel: panel + onSelectionChanged: { + tableView.selection.clear() + for(var i = 0; i < controller.model.count; i++) { + var o = controller.model.get(i) + if (o && o.selected) { + tableView.selection.select(i, i) + } + } + } + } + + QGCPalette { id: palette; colorGroupEnabled: enabled } + + QGCViewPanel { + id: panel + anchors.fill: parent + + TableView { + id: tableView + anchors.margins: _margins + anchors.left: parent.left + anchors.right: refreshButton.left + anchors.top: parent.top + anchors.bottom: parent.bottom + model: controller.model + selectionMode: SelectionMode.MultiSelection + + TableViewColumn { + title: "Id" + width: ScreenTools.defaultFontPixelWidth * 4 + horizontalAlignment: Text.AlignHCenter + delegate : Text { + horizontalAlignment: Text.AlignHCenter + text: { + var o = controller.model.get(styleData.row) + return o ? o.id : "" + } + } + } + + TableViewColumn { + title: "Date" + width: ScreenTools.defaultFontPixelWidth * 30 + horizontalAlignment: Text.AlignHCenter + delegate : Text { + text: { + var o = controller.model.get(styleData.row) + if (o) { + //-- Have we received this entry already? + if(controller.model.get(styleData.row).received) { + var d = controller.model.get(styleData.row).time + if(d.getUTCFullYear() < 1980) + return "Date Unknown" + else + return d.toLocaleString() + } + } + return "" + } + } + } + + TableViewColumn { + title: "Size" + width: ScreenTools.defaultFontPixelWidth * 12 + horizontalAlignment: Text.AlignHCenter + delegate : Text { + horizontalAlignment: Text.AlignRight + text: { + var o = controller.model.get(styleData.row) + return o ? numberWithCommas(o.size) : "" + } + } + } + + TableViewColumn { + title: "Status" + width: ScreenTools.defaultFontPixelWidth * 18 + horizontalAlignment: Text.AlignHCenter + delegate : Text { + horizontalAlignment: Text.AlignHCenter + text: { + var o = controller.model.get(styleData.row) + return o ? o.status : "" + } + } + } + + } + + QGCButton { + id: refreshButton + anchors.margins: _margins + anchors.top: parent.top + anchors.right: parent.right + enabled: !controller.requestingList && !controller.downloadingLogs + text: "Refresh" + onClicked: { + controller.refresh() + } + } + + QGCButton { + id: downloadButton + anchors.margins: _margins + anchors.top: refreshButton.bottom + anchors.right: parent.right + enabled: !controller.requestingList && !controller.downloadingLogs && tableView.selection.count > 0 + text: "Download" + onClicked: { + //-- Clear selection + for(var i = 0; i < controller.model.count; i++) { + var o = controller.model.get(i) + if (o) o.selected = false + } + //-- Flag selected log files + tableView.selection.forEach(function(rowIndex){ + var o = controller.model.get(rowIndex) + if (o) o.selected = true + }) + //-- Download them + controller.download() + } + } + + QGCButton { + id: eraseAllButton + anchors.margins: _margins + anchors.top: downloadButton.bottom + anchors.right: parent.right + enabled: !controller.requestingList && !controller.downloadingLogs && controller.model.count > 0 + text: "Erase All" + onClicked: { + eraseAllDialog.visible = true + } + MessageDialog { + id: eraseAllDialog + visible: false + icon: StandardIcon.Warning + standardButtons: StandardButton.Yes | StandardButton.No + title: "Delete All Log Files" + text: "All log files will be erased permanently. Is this really what you want?" + onYes: { + controller.eraseAll() + eraseAllDialog.visible = false + } + onNo: { + eraseAllDialog.visible = false + } + } + } + + QGCButton { + id: cancelButton + anchors.margins: _margins + anchors.top: eraseAllButton.bottom + anchors.right: parent.right + text: "Cancel" + enabled: controller.requestingList || controller.downloadingLogs + onClicked: { + controller.cancel() + } + } + } +} diff --git a/src/ViewWidgets/LogDownloadController.cc b/src/ViewWidgets/LogDownloadController.cc new file mode 100644 index 0000000000000000000000000000000000000000..445d946eedf1293a87c0d4057c512b3569cd59eb --- /dev/null +++ b/src/ViewWidgets/LogDownloadController.cc @@ -0,0 +1,627 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "LogDownloadController.h" +#include "MultiVehicleManager.h" +#include "QGCMAVLink.h" +#include "QGCFileDialog.h" +#include "UAS.h" +#include "QGCApplication.h" +#include "QGCToolbox.h" +#include "Vehicle.h" +#include "MainWindow.h" + +#include +#include +#include + +#define kTimeOutMilliseconds 500 + +QGC_LOGGING_CATEGORY(LogDownloadLog, "LogDownloadLog") + +//---------------------------------------------------------------------------------------- +LogDownloadData::LogDownloadData(QGCLogEntry* entry_) + : ID(entry_->id()) + , entry(entry_) + , written(0) +{ + +} + +//---------------------------------------------------------------------------------------- +QGCLogEntry:: QGCLogEntry(uint logId, const QDateTime& dateTime, uint logSize, bool received) + : _logID(logId) + , _logSize(logSize) + , _logTimeUTC(dateTime) + , _received(received) + , _selected(false) +{ + _status = "Pending"; +} + +//---------------------------------------------------------------------------------------- +LogDownloadController::LogDownloadController(void) + : _uas(NULL) + , _downloadData(NULL) + , _vehicle(NULL) + , _requestingLogEntries(false) + , _downloadingLogs(false) + , _retries(0) +{ + MultiVehicleManager *manager = qgcApp()->toolbox()->multiVehicleManager(); + connect(manager, &MultiVehicleManager::activeVehicleChanged, this, &LogDownloadController::_setActiveVehicle); + connect(&_timer, &QTimer::timeout, this, &LogDownloadController::_processDownload); + _setActiveVehicle(manager->activeVehicle()); +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_processDownload() +{ + if(_requestingLogEntries) { + _findMissingEntries(); + } else if(_downloadingLogs) { + _findMissingData(); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_setActiveVehicle(Vehicle* vehicle) +{ + if((_uas && vehicle && _uas == vehicle->uas()) || !vehicle ) { + return; + } + _vehicle = vehicle; + if (_uas) { + _logEntriesModel.clear(); + disconnect(_uas, &UASInterface::logEntry, this, &LogDownloadController::_logEntry); + disconnect(_uas, &UASInterface::logData, this, &LogDownloadController::_logData); + _uas = NULL; + } + _uas = vehicle->uas(); + connect(_uas, &UASInterface::logEntry, this, &LogDownloadController::_logEntry); + connect(_uas, &UASInterface::logData, this, &LogDownloadController::_logData); +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_logEntry(UASInterface* uas, uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t /*last_log_num*/) +{ + //-- Do we care? + if(!_uas || uas != _uas || !_requestingLogEntries) { + return; + } + //-- If this is the first, pre-fill it + if(!_logEntriesModel.count() && num_logs > 0) { + for(int i = 0; i < num_logs; i++) { + QGCLogEntry *entry = new QGCLogEntry(i); + _logEntriesModel.append(entry); + } + } + //-- Update this log record + if(num_logs > 0) { + if(id < _logEntriesModel.count()) { + QGCLogEntry* entry = _logEntriesModel[id]; + entry->setSize(size); + entry->setTime(QDateTime::fromTime_t(time_utc)); + entry->setReceived(true); + entry->setStatus(QString("Available")); + } else { + qWarning() << "Received log entry for out-of-bound index:" << id; + } + } else { + //-- No logs to list + _receivedAllEntries(); + } + //-- Reset retry count + _retries = 0; + //-- Do we have it all? + if(_entriesComplete()) { + _receivedAllEntries(); + } else { + //-- Reset timer + _timer.start(kTimeOutMilliseconds); + } +} + +//---------------------------------------------------------------------------------------- +bool +LogDownloadController::_entriesComplete() +{ + //-- Iterate entries and look for a gap + int num_logs = _logEntriesModel.count(); + for(int i = 0; i < num_logs; i++) { + QGCLogEntry* entry = _logEntriesModel[i]; + if(entry) { + if(!entry->received()) { + return false; + } + } + } + return true; +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_resetSelection() +{ + int num_logs = _logEntriesModel.count(); + for(int i = 0; i < num_logs; i++) { + QGCLogEntry* entry = _logEntriesModel[i]; + if(entry) { + entry->setSelected(false); + } + } + emit selectionChanged(); +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_receivedAllEntries() +{ + _timer.stop(); + _requestingLogEntries = false; + emit requestingListChanged(); +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_findMissingEntries() +{ + int start = -1; + int end = -1; + int num_logs = _logEntriesModel.count(); + //-- Iterate entries and look for a gap + for(int i = 0; i < num_logs; i++) { + QGCLogEntry* entry = _logEntriesModel[i]; + if(entry) { + if(!entry->received()) { + if(start < 0) + start = i; + else + end = i; + } else { + if(start >= 0) { + break; + } + } + } + } + //-- Is there something missing? + if(start >= 0) { + //-- Have we tried too many times? + if(_retries++ > 2) { + for(int i = 0; i < num_logs; i++) { + QGCLogEntry* entry = _logEntriesModel[i]; + if(entry && !entry->received()) { + entry->setStatus(QString("Error")); + } + } + //-- Give up + _receivedAllEntries(); + qWarning() << "Too many errors retreiving log list. Giving up."; + return; + } + //-- Is it a sequence or just one entry? + if(end < 0) { + end = start; + } + //-- Request these entries again + _requestLogList((uint32_t)start, (uint32_t) end); + } else { + _receivedAllEntries(); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_logData(UASInterface* uas, uint32_t ofs, uint16_t id, uint8_t count, const uint8_t* data) +{ + if(!_uas || uas != _uas || !_downloadData) { + return; + } + if(_downloadData->ID != id) { + qWarning() << "Received log data for wrong log"; + return; + } + bool result = false; + //-- Find offset table entry + uint o_index = ofs / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; + if(o_index <= (uint)_downloadData->offsets.count()) { + _downloadData->offsets[o_index] = count; + //-- Write chunk to file + if(_downloadData->file.seek(ofs)) { + if(_downloadData->file.write((const char*)data, count)) { + _downloadData->written += count; + //-- Update status + _downloadData->entry->setStatus(QString::number(_downloadData->written)); + result = true; + //-- reset retries + _retries = 0; + //-- Reset timer + _timer.start(kTimeOutMilliseconds); + //-- Do we have it all? + if(_logComplete()) { + _downloadData->entry->setStatus(QString("Downloaded")); + //-- Check for more + _receivedAllData(); + } + } else { + qWarning() << "Error while writing log file chunk"; + } + } else { + qWarning() << "Error while seeking log file offset"; + } + } else { + qWarning() << "Received log offset greater than expected"; + } + if(!result) { + _downloadData->entry->setStatus(QString("Error")); + } +} + +//---------------------------------------------------------------------------------------- +bool +LogDownloadController::_logComplete() +{ + //-- Iterate entries and look for a gap + int num_ofs = _downloadData->offsets.count(); + for(int i = 0; i < num_ofs; i++) { + if(_downloadData->offsets[i] == 0) { + return false; + } + } + return true; +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_receivedAllData() +{ + _timer.stop(); + //-- Anything queued up for download? + if(_prepareLogDownload()) { + //-- Request Log + _requestLogData(_downloadData->ID, 0, _downloadData->entry->size()); + } else { + _resetSelection(); + _downloadingLogs = false; + emit downloadingLogsChanged(); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_findMissingData() +{ + int start = -1; + int end = -1; + int num_ofs = _downloadData->offsets.count(); + //-- Iterate offsets and look for a gap + for(int i = 0; i < num_ofs; i++) { + if(_downloadData->offsets[i] == 0) { + if(start < 0) + start = i; + else + end = i; + } else { + if(start >= 0) { + break; + } + } + } + //-- Is there something missing? + if(start >= 0) { + //-- Have we tried too many times? + if(_retries++ > 2) { + _downloadData->entry->setStatus(QString("Timed Out")); + //-- Give up + qWarning() << "Too many errors retreiving log data. Giving up."; + _receivedAllData(); + return; + } + //-- Is it a sequence or just one entry? + if(end < 0) { + end = start; + } + //-- Request these log chunks again + _requestLogData( + _downloadData->ID, + (uint32_t)(start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN), + (uint32_t)((end - start + 1) * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN)); + } else { + _receivedAllData(); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_requestLogData(uint8_t id, uint32_t offset, uint32_t count) +{ + if(_vehicle) { + qCDebug(LogDownloadLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << ")"; + mavlink_message_t msg; + mavlink_msg_log_request_data_pack( + qgcApp()->toolbox()->mavlinkProtocol()->getSystemId(), + qgcApp()->toolbox()->mavlinkProtocol()->getComponentId(), + &msg, + qgcApp()->toolbox()->multiVehicleManager()->activeVehicle()->id(), MAV_COMP_ID_ALL, + id, offset, count); + _vehicle->sendMessage(msg); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::refresh(void) +{ + _logEntriesModel.clear(); + _requestLogList(); +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::_requestLogList(uint32_t start, uint32_t end) +{ + if(_vehicle && _uas) { + qCDebug(LogDownloadLog) << "Request log entry list (" << start << "through" << end << ")"; + _requestingLogEntries = true; + emit requestingListChanged(); + mavlink_message_t msg; + mavlink_msg_log_request_list_pack( + qgcApp()->toolbox()->mavlinkProtocol()->getSystemId(), + qgcApp()->toolbox()->mavlinkProtocol()->getComponentId(), + &msg, + _vehicle->id(), + MAV_COMP_ID_ALL, + start, + end); + _vehicle->sendMessage(msg); + //-- Wait 2 seconds before bitching about not getting anything + _timer.start(2000); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::download(void) +{ + //-- Stop listing just in case + _receivedAllEntries(); + //-- Reset downloads, again just in case + if(_downloadData) { + delete _downloadData; + _downloadData = 0; + } + _downloadPath.clear(); + _downloadPath = QGCFileDialog::getExistingDirectory( + MainWindow::instance(), + "Log Download Directory", + QDir::homePath(), + QGCFileDialog::ShowDirsOnly | QGCFileDialog::DontResolveSymlinks); + if(!_downloadPath.isEmpty()) { + if(!_downloadPath.endsWith(QDir::separator())) + _downloadPath += QDir::separator(); + //-- Start download process + _downloadingLogs = true; + emit downloadingLogsChanged(); + _receivedAllData(); + } +} + +//---------------------------------------------------------------------------------------- +QGCLogEntry* +LogDownloadController::_getNextSelected() +{ + //-- Iterate entries and look for a selected file + int num_logs = _logEntriesModel.count(); + for(int i = 0; i < num_logs; i++) { + QGCLogEntry* entry = _logEntriesModel[i]; + if(entry) { + if(entry->selected()) { + return entry; + } + } + } + return NULL; +} + +//---------------------------------------------------------------------------------------- +bool +LogDownloadController::_prepareLogDownload() +{ + if(_downloadData) { + delete _downloadData; + _downloadData = NULL; + } + QGCLogEntry* entry = _getNextSelected(); + if(!entry) { + return false; + } + //-- Deselect file + entry->setSelected(false); + emit selectionChanged(); + bool result = false; + QString ftime; + if(entry->time().date().year() < 1980) { + ftime = "UnknownDate"; + } else { + ftime = entry->time().toString("yyyy-M-d-hh-mm-ss"); + } + _downloadData = new LogDownloadData(entry); + _downloadData->filename = QString("log_") + QString::number(entry->id()) + "_" + ftime + ".txt"; + _downloadData->file.setFileName(_downloadPath + _downloadData->filename); + //-- Append a number to the end if the filename already exists + if (_downloadData->file.exists()){ + uint num_dups = 0; + QStringList filename_spl = _downloadData->filename.split('.'); + do { + num_dups +=1; + _downloadData->file.setFileName(filename_spl[0] + '_' + QString::number(num_dups) + '.' + filename_spl[1]); + } while( _downloadData->file.exists()); + } + //-- Create file + if (!_downloadData->file.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to create log file:" << _downloadData->filename; + } else { + //-- Preallocate file + if(!_downloadData->file.resize(entry->size())) { + qWarning() << "Failed to allocate space for log file:" << _downloadData->filename; + } else { + //-- Prepare Offset Table + uint o_count = (uint)ceil(entry->size() / (double)MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); + for(uint i = 0; i < o_count; i++) { + _downloadData->offsets.append(0); + } + result = true; + } + } + if(!result) { + if (_downloadData->file.exists()) { + _downloadData->file.remove(); + } + _downloadData->entry->setStatus(QString("Error")); + delete _downloadData; + _downloadData = NULL; + } + return result; +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::eraseAll(void) +{ + if(_vehicle && _uas) { + mavlink_message_t msg; + mavlink_msg_log_erase_pack( + qgcApp()->toolbox()->mavlinkProtocol()->getSystemId(), + qgcApp()->toolbox()->mavlinkProtocol()->getComponentId(), + &msg, + qgcApp()->toolbox()->multiVehicleManager()->activeVehicle()->id(), MAV_COMP_ID_ALL); + _vehicle->sendMessage(msg); + refresh(); + } +} + +//---------------------------------------------------------------------------------------- +void +LogDownloadController::cancel(void) +{ + if(_uas){ + _receivedAllEntries(); + } + if(_downloadData) { + _downloadData->entry->setStatus(QString("Canceled")); + if (_downloadData->file.exists()) { + _downloadData->file.remove(); + } + delete _downloadData; + _downloadData = 0; + } + _resetSelection(); + _downloadingLogs = false; + emit downloadingLogsChanged(); +} + +//----------------------------------------------------------------------------- +QGCLogModel::QGCLogModel(QObject* parent) + : QAbstractListModel(parent) +{ + +} + +//----------------------------------------------------------------------------- +QGCLogEntry* +QGCLogModel::get(int index) +{ + if (index < 0 || index >= _logEntries.count()) { + return NULL; + } + return _logEntries[index]; +} + +//----------------------------------------------------------------------------- +int +QGCLogModel::count() const +{ + return _logEntries.count(); +} + +//----------------------------------------------------------------------------- +void +QGCLogModel::append(QGCLogEntry* object) +{ + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + _logEntries.append(object); + endInsertRows(); + emit countChanged(); +} + +//----------------------------------------------------------------------------- +void +QGCLogModel::clear(void) +{ + if(!_logEntries.isEmpty()) { + beginRemoveRows(QModelIndex(), 0, _logEntries.count()); + while (_logEntries.count()) { + QGCLogEntry* entry = _logEntries.last(); + if(entry) delete entry; + _logEntries.removeLast(); + } + endRemoveRows(); + emit countChanged(); + } +} + +//----------------------------------------------------------------------------- +QGCLogEntry* +QGCLogModel::operator[](int index) +{ + return get(index); +} + +//----------------------------------------------------------------------------- +int +QGCLogModel::rowCount(const QModelIndex& /*parent*/) const +{ + return _logEntries.count(); +} + +//----------------------------------------------------------------------------- +QVariant +QGCLogModel::data(const QModelIndex & index, int role) const { + if (index.row() < 0 || index.row() >= _logEntries.count()) + return QVariant(); + if (role == ObjectRole) + return QVariant::fromValue(_logEntries[index.row()]); + return QVariant(); +} + +//----------------------------------------------------------------------------- +QHash +QGCLogModel::roleNames() const { + QHash roles; + roles[ObjectRole] = "logEntry"; + return roles; +} diff --git a/src/ViewWidgets/LogDownloadController.h b/src/ViewWidgets/LogDownloadController.h new file mode 100644 index 0000000000000000000000000000000000000000..3c29836f6ac311c0458f42ddb510db14ee917f9a --- /dev/null +++ b/src/ViewWidgets/LogDownloadController.h @@ -0,0 +1,193 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef LogDownloadController_H +#define LogDownloadController_H + +#include +#include +#include + +#include + +#include "UASInterface.h" +#include "AutoPilotPlugin.h" +#include "FactPanelController.h" + +class MultiVehicleManager; +class UASInterface; +class Vehicle; +class QGCLogEntry; +class LogDownloadData; + +Q_DECLARE_LOGGING_CATEGORY(LogDownloadLog) + +//----------------------------------------------------------------------------- +class QGCLogModel : public QAbstractListModel +{ + Q_OBJECT +public: + + enum QGCLogModelRoles { + ObjectRole = Qt::UserRole + 1 + }; + + QGCLogModel(QObject *parent = 0); + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_INVOKABLE QGCLogEntry* get(int index); + + int count (void) const; + void append (QGCLogEntry* entry); + void clear (void); + QGCLogEntry*operator[] (int i); + + int rowCount (const QModelIndex & parent = QModelIndex()) const; + QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; + +signals: + void countChanged (); + +protected: + QHash roleNames() const; +private: + QList _logEntries; +}; + +//----------------------------------------------------------------------------- +class QGCLogEntry : public QObject { + Q_OBJECT + Q_PROPERTY(uint id READ id CONSTANT) + Q_PROPERTY(QDateTime time READ time NOTIFY timeChanged) + Q_PROPERTY(uint size READ size NOTIFY sizeChanged) + Q_PROPERTY(bool received READ received NOTIFY receivedChanged) + Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) + Q_PROPERTY(QString status READ status NOTIFY statusChanged) + +public: + QGCLogEntry(uint logId, const QDateTime& dateTime = QDateTime(), uint logSize = 0, bool received = false); + + uint id () const { return _logID; } + uint size () const { return _logSize; } + QDateTime time () const { return _logTimeUTC; } + bool received () const { return _received; } + bool selected () const { return _selected; } + QString status () const { return _status; } + + void setId (uint id_) { _logID = id_; } + void setSize (uint size_) { _logSize = size_; emit sizeChanged(); } + void setTime (QDateTime date_) { _logTimeUTC = date_; emit timeChanged(); } + void setReceived (bool rec_) { _received = rec_; emit receivedChanged(); } + void setSelected (bool sel_) { _selected = sel_; emit selectedChanged(); } + void setStatus (QString stat_) { _status = stat_; emit statusChanged(); } + +signals: + void idChanged (); + void timeChanged (); + void sizeChanged (); + void receivedChanged (); + void selectedChanged (); + void statusChanged (); + +private: + uint _logID; + uint _logSize; + QDateTime _logTimeUTC; + bool _received; + bool _selected; + QString _status; +}; + +//----------------------------------------------------------------------------- +class LogDownloadData { +public: + LogDownloadData(QGCLogEntry* entry); + QList offsets; + QFile file; + QString filename; + uint ID; + QTimer processDataTimer; + QGCLogEntry* entry; + uint written; +}; + +//----------------------------------------------------------------------------- +class LogDownloadController : public FactPanelController +{ + Q_OBJECT +public: + + LogDownloadController(); + + Q_PROPERTY(QGCLogModel* model READ model NOTIFY modelChanged) + Q_PROPERTY(bool requestingList READ requestingList NOTIFY requestingListChanged) + Q_PROPERTY(bool downloadingLogs READ downloadingLogs NOTIFY downloadingLogsChanged) + + QGCLogModel* model () { return &_logEntriesModel; } + bool requestingList () { return _requestingLogEntries; } + bool downloadingLogs () { return _downloadingLogs; } + + Q_INVOKABLE void refresh (); + Q_INVOKABLE void download (); + Q_INVOKABLE void eraseAll (); + Q_INVOKABLE void cancel (); + +signals: + void requestingListChanged (); + void downloadingLogsChanged (); + void modelChanged (); + void selectionChanged (); + +private slots: + void _setActiveVehicle (Vehicle* vehicle); + 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); + void _processDownload (); + +private: + + bool _entriesComplete (); + bool _logComplete (); + void _findMissingEntries(); + void _receivedAllEntries(); + void _receivedAllData (); + void _resetSelection (); + void _findMissingData (); + void _requestLogList (uint32_t start = 0, uint32_t end = 0xFFFF); + void _requestLogData (uint8_t id, uint32_t offset = 0, uint32_t count = 0xFFFFFFFF); + bool _prepareLogDownload(); + + QGCLogEntry* _getNextSelected(); + + UASInterface* _uas; + LogDownloadData* _downloadData; + QTimer _timer; + QGCLogModel _logEntriesModel; + Vehicle* _vehicle; + bool _requestingLogEntries; + bool _downloadingLogs; + int _retries; + QString _downloadPath; +}; + +#endif diff --git a/src/uas/UAS.cc b/src/uas/UAS.cc index 7e8be3234bc41609c10ac02826f00c5366c51e66..0dbfbb1469c86073f05bf324e75a815d0ce428b8 100644 --- a/src/uas/UAS.cc +++ b/src/uas/UAS.cc @@ -961,6 +961,23 @@ void UAS::receiveMessage(mavlink_message_t message) emit NavigationControllerDataChanged(this, p.nav_roll, p.nav_pitch, p.nav_bearing, p.target_bearing, p.wp_dist); } break; + + case MAVLINK_MSG_ID_LOG_ENTRY: + { + mavlink_log_entry_t log; + mavlink_msg_log_entry_decode(&message, &log); + emit logEntry(this, log.time_utc, log.size, log.id, log.num_logs, log.last_log_num); + } + break; + + case MAVLINK_MSG_ID_LOG_DATA: + { + mavlink_log_data_t log; + mavlink_msg_log_data_decode(&message, &log); + emit logData(this, log.ofs, log.id, log.count, log.data); + } + break; + default: break; } diff --git a/src/uas/UASInterface.h b/src/uas/UASInterface.h index 31cb0957a3ab6a21116b2e639e28c2425772441c..d351fd72f79959896a4b0b867ed080d45be5c190 100644 --- a/src/uas/UASInterface.h +++ b/src/uas/UASInterface.h @@ -88,25 +88,25 @@ public: static QColor getNextColor() { /* Create color map */ static QList colors = QList() - << QColor(231,72,28) - << QColor(104,64,240) - << QColor(203,254,121) - << QColor(161,252,116) - << QColor(232,33,47) - << QColor(116,251,110) - << QColor(234,38,107) - << QColor(104,250,138) + << QColor(231,72,28) + << QColor(104,64,240) + << QColor(203,254,121) + << QColor(161,252,116) + << QColor(232,33,47) + << QColor(116,251,110) + << QColor(234,38,107) + << QColor(104,250,138) << QColor(235,43,165) - << QColor(98,248,176) - << QColor(236,48,221) - << QColor(92,247,217) + << QColor(98,248,176) + << QColor(236,48,221) + << QColor(92,247,217) << QColor(200,54,238) - << QColor(87,231,246) - << QColor(151,59,239) - << QColor(81,183,244) + << QColor(87,231,246) + << QColor(151,59,239) + << QColor(81,183,244) << QColor(75,133,243) - << QColor(242,255,128) - << QColor(230,126,23); + << QColor(242,255,128) + << QColor(230,126,23); static int nextColor = -1; if(nextColor == 18){//if at the end of the list @@ -139,10 +139,10 @@ public: StartBusConfigActuators, EndBusConfigActuators, }; - + /// Starts the specified calibration virtual void startCalibration(StartCalibrationType calType) = 0; - + /// Ends any current calibration virtual void stopCalibration(void) = 0; @@ -232,11 +232,11 @@ signals: * * Typically this is used to send lowlevel information like the battery voltage to the plotting facilities of * the groundstation. The data here should be converted to human-readable values before being passed, so ideally - * SI units. + * SI units. * * @param uasId ID of this system * @param name name of the value, e.g. "battery voltage" - * @param unit The units this variable is in as an abbreviation. For system-dependent (such as raw ADC values) use "raw", for bitfields use "bits", for true/false or on/off use "bool", for unitless values use "-". + * @param unit The units this variable is in as an abbreviation. For system-dependent (such as raw ADC values) use "raw", for bitfields use "bits", for true/false or on/off use "bool", for unitless values use "-". * @param value the value that changed * @param msec the timestamp of the message, in milliseconds */ @@ -323,6 +323,10 @@ signals: // HOME POSITION / ORIGIN CHANGES void homePositionChanged(int uas, double lat, double lon, double alt); + // 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); + protected: // TIMEOUT CONSTANTS diff --git a/src/ui/MainWindow.cc b/src/ui/MainWindow.cc index 961775833d2f67634a22de040ff81d963df48ac7..df8b7bc2729f34ef859a5d2d31b134b720cf9711 100644 --- a/src/ui/MainWindow.cc +++ b/src/ui/MainWindow.cc @@ -65,6 +65,7 @@ This file is part of the QGROUNDCONTROL project #include "QGCDockWidget.h" #include "UASInfoWidget.h" #include "HILDockWidget.h" +#include "LogDownload.h" #endif #ifndef __ios__ @@ -86,7 +87,8 @@ enum DockWidgetTypes { STATUS_DETAILS, INFO_VIEW, HIL_CONFIG, - ANALYZE + ANALYZE, + LOG_DOWNLOAD }; static const char *rgDockWidgetNames[] = { @@ -96,7 +98,8 @@ static const char *rgDockWidgetNames[] = { "Status Details", "Info View", "HIL Config", - "Analyze" + "Analyze", + "Log Download" }; #define ARRAY_SIZE(ARRAY) (sizeof(ARRAY) / sizeof(ARRAY[0])) @@ -358,6 +361,9 @@ bool MainWindow::_createInnerDockWidget(const QString& widgetName) case ONBOARD_FILES: widget = new QGCUASFileViewMulti(widgetName, action, this); break; + case LOG_DOWNLOAD: + widget = new LogDownload(widgetName, action, this); + break; case STATUS_DETAILS: widget = new UASInfoWidget(widgetName, action, this); break;