QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
OnboardLogFtpController.cc
Go to the documentation of this file.
2#include "AppSettings.h"
3#include "FTPManager.h"
6#include "QGCFormat.h"
9#include "SettingsManager.h"
10#include "Vehicle.h"
11#include "VehicleLinkManager.h"
12
13#include <QtCore/QDateTime>
14#include <QtCore/QDir>
15#include <QtCore/QFile>
16#include <QtCore/QTimeZone>
17
18QGC_LOGGING_CATEGORY(OnboardLogFtpControllerLog, "AnalyzeView.OnboardLogFtpController")
19
20// MAVLink FTP defines "@MAV_LOG" as the virtual log directory.
21// Older firmware that doesn't implement the alias requires the physical path
22// instead — which is firmware-specific.
23static constexpr const char *kMavlinkLogRoot = "@MAV_LOG";
24static constexpr const char *kPx4LogRootFallback = "/fs/microsd/log";
25static constexpr const char *kApmLogRootFallback = "/APM/LOGS";
26
28 : QObject(parent)
29 , _logEntriesModel(new QmlObjectListModel(this))
30{
31 qCDebug(OnboardLogFtpControllerLog) << this;
32
33 (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &OnboardLogFtpController::_setActiveVehicle);
34 _setActiveVehicle(MultiVehicleManager::instance()->activeVehicle());
35}
36
38{
39 qCDebug(OnboardLogFtpControllerLog) << this;
40}
41
42void OnboardLogFtpController::_setActiveVehicle(Vehicle *vehicle)
43{
44 if (vehicle == _vehicle) {
45 return;
46 }
47
48 if (_vehicle) {
49 _logEntriesModel->clearAndDeleteContents();
50 FTPManager *const ftp = _vehicle->ftpManager();
51 (void) disconnect(ftp, &FTPManager::listDirectoryComplete, this, &OnboardLogFtpController::_listDirComplete);
52 (void) disconnect(ftp, &FTPManager::downloadComplete, this, &OnboardLogFtpController::_downloadComplete);
53 (void) disconnect(ftp, &FTPManager::commandProgress, this, &OnboardLogFtpController::_downloadProgress);
54
55 _listState = Idle;
56 _dirsToList.clear();
57 _logIdCounter = 0;
58 _downloadQueue.clear();
59 _currentDownloadEntry = nullptr;
60 }
61
62 _vehicle = vehicle;
63}
64
66{
67 _logEntriesModel->clearAndDeleteContents();
68
69 if (!_vehicle) {
70 qCWarning(OnboardLogFtpControllerLog) << "refresh: no active vehicle";
71 return;
72 }
73
74 if (!_vehicle->capabilitiesKnown() || !(_vehicle->capabilityBits() & MAV_PROTOCOL_CAPABILITY_FTP)) {
75 qCWarning(OnboardLogFtpControllerLog) << "refresh: vehicle does not advertise MAV_PROTOCOL_CAPABILITY_FTP"
76 << "(capsKnown:" << _vehicle->capabilitiesKnown() << ")";
77 return;
78 }
79
80 _startListing();
81}
82
83void OnboardLogFtpController::_startListing()
84{
85 _dirsToList.clear();
86 _logIdCounter = 0;
87 _logRoot = QString::fromLatin1(kMavlinkLogRoot);
88 _triedFallbackRoot = false;
89
90 FTPManager *const ftp = _vehicle->ftpManager();
91 (void) disconnect(ftp, &FTPManager::listDirectoryComplete, this, &OnboardLogFtpController::_listDirComplete);
92 (void) connect(ftp, &FTPManager::listDirectoryComplete, this, &OnboardLogFtpController::_listDirComplete);
93
94 _setListing(true);
95 _listRoot();
96}
97
98void OnboardLogFtpController::_listRoot()
99{
100 _listState = ListingRoot;
101
102 qCDebug(OnboardLogFtpControllerLog) << "listing root" << _logRoot;
103
104 if (!_vehicle->ftpManager()->listDirectory(MAV_COMP_ID_AUTOPILOT1, _logRoot)) {
105 qCWarning(OnboardLogFtpControllerLog) << "failed to start root listing for" << _logRoot;
106 _finishListing();
107 }
108}
109
110void OnboardLogFtpController::_listDirComplete(const QStringList &dirList, const QString &errorMsg)
111{
112 if (!errorMsg.isEmpty()) {
113 if (_listState == ListingRoot && !_triedFallbackRoot && _vehicle) {
114 const char *fallback = nullptr;
115 if (_vehicle->px4Firmware()) {
116 fallback = kPx4LogRootFallback;
117 } else if (_vehicle->apmFirmware()) {
118 fallback = kApmLogRootFallback;
119 }
120
121 if (fallback) {
122 qCDebug(OnboardLogFtpControllerLog) << "root listing of" << _logRoot << "failed (" << errorMsg
123 << "), falling back to" << fallback;
124 _triedFallbackRoot = true;
125 _logRoot = QString::fromLatin1(fallback);
126 _listRoot();
127 return;
128 }
129 }
130
131 qCWarning(OnboardLogFtpControllerLog) << "listing error:" << errorMsg;
132 _finishListing();
133 return;
134 }
135
136 if (_listState == ListingRoot) {
137 // The root listing may contain log files directly (flat layout, e.g. @MAV_LOG)
138 // and/or date subdirectories to descend into (PX4 fallback /fs/microsd/log).
139 const uint flatLogs = _processFileEntries(dirList, QString());
140
141 for (const QString &entry : dirList) {
142 if (entry.startsWith(QLatin1Char('D'))) {
143 const QString dirName = entry.mid(1);
144 if (!dirName.isEmpty()) {
145 _dirsToList.append(dirName);
146 }
147 }
148 }
149
150 _dirsToList.sort();
151 qCDebug(OnboardLogFtpControllerLog) << "root listing of" << _logRoot
152 << "found" << flatLogs << "flat logs and" << _dirsToList.size() << "subdirectories";
153
154 _listState = ListingSubdir;
155 _listNextSubdir();
156 return;
157 }
158
159 const QString currentDir = _dirsToList.isEmpty() ? QString() : _dirsToList.first();
160 const uint logsFoundInDir = _processFileEntries(dirList, currentDir);
161
162 qCDebug(OnboardLogFtpControllerLog) << currentDir << "->" << logsFoundInDir << "logs";
163
164 if (!_dirsToList.isEmpty()) {
165 _dirsToList.removeFirst();
166 }
167
168 _listNextSubdir();
169}
170
171uint OnboardLogFtpController::_processFileEntries(const QStringList &dirList, const QString &subdir)
172{
173 const QDate dirDate = subdir.isEmpty() ? QDate() : QDate::fromString(subdir, QStringLiteral("yyyy-MM-dd"));
174 uint logsFound = 0;
175
176 for (const QString &entry : dirList) {
177 if (!entry.startsWith(QLatin1Char('F'))) {
178 continue;
179 }
180
181 const QString fileInfo = entry.mid(1);
182 const int tabIdx = fileInfo.indexOf(QLatin1Char('\t'));
183 if (tabIdx < 0) {
184 continue;
185 }
186
187 const QString fileName = fileInfo.left(tabIdx);
188 const QString sizeStr = fileInfo.mid(tabIdx + 1);
189
190 if (!fileName.endsWith(QStringLiteral(".ulg"), Qt::CaseInsensitive) &&
191 !fileName.endsWith(QStringLiteral(".bin"), Qt::CaseInsensitive)) {
192 continue;
193 }
194
195 bool sizeOk = false;
196 const uint fileSize = sizeStr.toUInt(&sizeOk);
197 if (!sizeOk) {
198 continue;
199 }
200
201 QDateTime dateTime;
202 if (dirDate.isValid()) {
203 const QString baseName = fileName.left(fileName.lastIndexOf(QLatin1Char('.')));
204 const QTime fileTime = QTime::fromString(baseName, QStringLiteral("HH_mm_ss"));
205 if (fileTime.isValid()) {
206 dateTime = QDateTime(dirDate, fileTime, QTimeZone::UTC);
207 } else {
208 dateTime = QDateTime(dirDate, QTime(), QTimeZone::UTC);
209 }
210 }
211
212 const QString ftpPath = subdir.isEmpty()
213 ? (_logRoot + QStringLiteral("/") + fileName)
214 : (_logRoot + QStringLiteral("/") + subdir + QStringLiteral("/") + fileName);
215
216 QGCOnboardLogFtpEntry *const logEntry = new QGCOnboardLogFtpEntry(_logIdCounter++, dateTime, fileSize, true, this);
217 logEntry->setFtpPath(ftpPath);
218 logEntry->setStatus(tr("Available"));
219 _logEntriesModel->append(logEntry);
220 logsFound++;
221 }
222
223 return logsFound;
224}
225
226void OnboardLogFtpController::_listNextSubdir()
227{
228 if (_dirsToList.isEmpty()) {
229 qCDebug(OnboardLogFtpControllerLog) << "listing complete, found" << _logEntriesModel->count() << "logs";
230 _finishListing();
231 return;
232 }
233
234 const QString subdir = _dirsToList.first();
235 const QString path = _logRoot + QStringLiteral("/") + subdir;
236
237 qCDebug(OnboardLogFtpControllerLog) << "listing subdir" << path;
238
239 if (!_vehicle->ftpManager()->listDirectory(MAV_COMP_ID_AUTOPILOT1, path)) {
240 qCWarning(OnboardLogFtpControllerLog) << "failed to list subdir" << path;
241 _dirsToList.removeFirst();
242 _listNextSubdir();
243 }
244}
245
246void OnboardLogFtpController::_finishListing()
247{
248 _listState = Idle;
249 _setListing(false);
250}
251
252void OnboardLogFtpController::download(const QString &path)
253{
254 const QString dir = path.isEmpty() ? SettingsManager::instance()->appSettings()->logSavePath() : path;
255 _downloadToDirectory(dir);
256}
257
258void OnboardLogFtpController::_downloadToDirectory(const QString &dir)
259{
260 _downloadPath = dir;
261 if (_downloadPath.isEmpty()) {
262 return;
263 }
264
265 if (!_downloadPath.endsWith(QDir::separator())) {
266 _downloadPath += QDir::separator();
267 }
268
269 _downloadQueue.clear();
270 const int numLogs = _logEntriesModel->count();
271 for (int i = 0; i < numLogs; i++) {
272 QGCOnboardLogFtpEntry *const entry = _logEntriesModel->value<QGCOnboardLogFtpEntry*>(i);
273 if (entry && entry->selected() && !entry->ftpPath().isEmpty()) {
274 entry->setStatus(tr("Waiting"));
275 _downloadQueue.enqueue(entry);
276 }
277 }
278
279 if (_downloadQueue.isEmpty()) {
280 qCWarning(OnboardLogFtpControllerLog) << "no selected logs have FTP paths for download";
281 return;
282 }
283
284 qCDebug(OnboardLogFtpControllerLog) << "queued" << _downloadQueue.size() << "logs for download to" << _downloadPath;
285 _setDownloading(true);
286
287 _downloadEntry(_downloadQueue.dequeue());
288}
289
290void OnboardLogFtpController::_downloadEntry(QGCOnboardLogFtpEntry *entry)
291{
292 if (!entry || !_vehicle) {
293 return;
294 }
295
296 entry->setSelected(false);
297 emit selectionChanged();
298
299 _currentDownloadEntry = entry;
300 _downloadBytesAtLastUpdate = 0;
301 _downloadRateAvg = 0.;
302 _downloadElapsed.start();
303
304 entry->setStatus(tr("Downloading"));
305
306 QString localFilename;
307 if (entry->time().isValid() && entry->time().date().year() >= 2010) {
308 localFilename = entry->time().toString(QStringLiteral("yyyy-M-d-hh-mm-ss")) + QStringLiteral(".ulg");
309 } else {
310 localFilename = QStringLiteral("log_") + QString::number(entry->id()) + QStringLiteral(".ulg");
311 }
312
313 if (QFile::exists(_downloadPath + localFilename)) {
314 const QStringList parts = localFilename.split(QLatin1Char('.'));
315 uint numDups = 0;
316 do {
317 numDups++;
318 localFilename = parts[0] + QStringLiteral("_") + QString::number(numDups) + QStringLiteral(".") + parts[1];
319 } while (QFile::exists(_downloadPath + localFilename));
320 }
321
322 FTPManager *const ftp = _vehicle->ftpManager();
323 (void) disconnect(ftp, &FTPManager::downloadComplete, this, &OnboardLogFtpController::_downloadComplete);
324 (void) disconnect(ftp, &FTPManager::commandProgress, this, &OnboardLogFtpController::_downloadProgress);
325 (void) connect(ftp, &FTPManager::downloadComplete, this, &OnboardLogFtpController::_downloadComplete);
326 (void) connect(ftp, &FTPManager::commandProgress, this, &OnboardLogFtpController::_downloadProgress);
327
328 qCDebug(OnboardLogFtpControllerLog) << "downloading" << entry->ftpPath() << "to" << _downloadPath + localFilename;
329
330 if (!ftp->download(MAV_COMP_ID_AUTOPILOT1, entry->ftpPath(), _downloadPath, localFilename, true)) {
331 qCWarning(OnboardLogFtpControllerLog) << "failed to start download for" << entry->ftpPath();
332 entry->setStatus(tr("Error"));
333 _currentDownloadEntry = nullptr;
334
335 if (!_downloadQueue.isEmpty()) {
336 _downloadEntry(_downloadQueue.dequeue());
337 } else {
338 _setDownloading(false);
339 }
340 }
341}
342
343void OnboardLogFtpController::_downloadComplete(const QString &file, const QString &errorMsg)
344{
345 if (!_currentDownloadEntry) {
346 return;
347 }
348
349 if (errorMsg.isEmpty()) {
350 _currentDownloadEntry->setStatus(tr("Downloaded"));
351 qCDebug(OnboardLogFtpControllerLog) << "download complete" << file;
352 } else {
353 _currentDownloadEntry->setStatus(tr("Error"));
354 qCWarning(OnboardLogFtpControllerLog) << "download error:" << errorMsg;
355 }
356
357 _currentDownloadEntry = nullptr;
358
359 if (!_downloadQueue.isEmpty()) {
360 _downloadEntry(_downloadQueue.dequeue());
361 } else {
362 _setDownloading(false);
363 }
364}
365
366void OnboardLogFtpController::_downloadProgress(float value)
367{
368 if (!_currentDownloadEntry) {
369 return;
370 }
371
372 if (_downloadElapsed.elapsed() < kGUIRateMs) {
373 return;
374 }
375
376 const size_t totalBytes = static_cast<size_t>(static_cast<qreal>(_currentDownloadEntry->size()) * static_cast<qreal>(value));
377 const size_t bytesSinceLastUpdate = totalBytes - _downloadBytesAtLastUpdate;
378 const qreal elapsedSec = _downloadElapsed.elapsed() / 1000.0;
379 const qreal rate = (elapsedSec > 0) ? (bytesSinceLastUpdate / elapsedSec) : 0;
380 _downloadRateAvg = (_downloadRateAvg * 0.95) + (rate * 0.05);
381 _downloadBytesAtLastUpdate = totalBytes;
382 _downloadElapsed.start();
383
384 const QString status = QStringLiteral("%1 (%2/s)").arg(
385 QGC::bigSizeToString(totalBytes),
386 QGC::bigSizeToString(_downloadRateAvg));
387
388 _currentDownloadEntry->setStatus(status);
389}
390
392{
393 if (!_vehicle) {
394 return;
395 }
396
397 if (_requestingLogEntries) {
398 _vehicle->ftpManager()->cancelListDirectory();
399 _dirsToList.clear();
400 _finishListing();
401 }
402
403 if (_downloadingLogs) {
404 _vehicle->ftpManager()->cancelDownload();
405 if (_currentDownloadEntry) {
406 _currentDownloadEntry->setStatus(tr("Canceled"));
407 _currentDownloadEntry = nullptr;
408 }
409 _downloadQueue.clear();
410 }
411
412 _resetSelection(true);
413 _setDownloading(false);
414}
415
416void OnboardLogFtpController::_resetSelection(bool canceled)
417{
418 const int numLogs = _logEntriesModel->count();
419 for (int i = 0; i < numLogs; i++) {
420 QGCOnboardLogFtpEntry *const entry = _logEntriesModel->value<QGCOnboardLogFtpEntry*>(i);
421 if (!entry) {
422 continue;
423 }
424
425 if (entry->selected()) {
426 if (canceled) {
427 entry->setStatus(tr("Canceled"));
428 }
429 entry->setSelected(false);
430 }
431 }
432
433 emit selectionChanged();
434}
435
436void OnboardLogFtpController::_setDownloading(bool active)
437{
438 if (_downloadingLogs != active) {
439 _downloadingLogs = active;
440 if (_vehicle) {
442 }
444 }
445}
446
447void OnboardLogFtpController::_setListing(bool active)
448{
449 if (_requestingLogEntries != active) {
450 _requestingLogEntries = active;
451 if (_vehicle) {
453 }
455 }
456}
static constexpr const char * kApmLogRootFallback
static constexpr const char * kPx4LogRootFallback
static constexpr const char * kMavlinkLogRoot
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QString logSavePath()
void cancelDownload()
bool listDirectory(uint8_t fromCompId, const QString &fromURI)
void commandProgress(float value)
void downloadComplete(const QString &file, const QString &errorMsg)
void cancelListDirectory()
bool download(uint8_t fromCompId, const QString &fromURI, const QString &toDir, const QString &fileName="", bool checksize=true)
Definition FTPManager.cc:30
void listDirectoryComplete(const QStringList &dirList, const QString &errorMsg)
static MultiVehicleManager * instance()
void activeVehicleChanged(Vehicle *activeVehicle)
Q_INVOKABLE void download(const QString &path=QString())
void setFtpPath(const QString &path)
void setStatus(const QString &stat)
void append(QObject *object)
Caller maintains responsibility for object ownership and deletion.
T value(int index) const
int count() const override final
void clearAndDeleteContents() override final
Clears the list and calls deleteLater on each entry.
static SettingsManager * instance()
AppSettings * appSettings() const
void setCommunicationLostEnabled(bool communicationLostEnabled)
bool px4Firmware() const
Definition Vehicle.h:494
uint64_t capabilityBits() const
Definition Vehicle.h:697
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:575
bool apmFirmware() const
Definition Vehicle.h:495
FTPManager * ftpManager()
Definition Vehicle.h:576
bool capabilitiesKnown() const
Definition Vehicle.h:696
Definition APM.h:4
QString bigSizeToString(quint64 size)
Byte size with unit: B, KB, MB, GB, TB. 1 fractional digit above 1 KB.
Definition QGCFormat.cc:20