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 // Entry format is "F<name>\t<size>" and, when the server supports kCmdListDirectoryWithTime,
182 // "F<name>\t<size>\t<modification time in seconds since UNIX epoch UTC>".
183 const QString fileInfo = entry.mid(1);
184 const int tabIdx = fileInfo.indexOf(QLatin1Char('\t'));
185 if (tabIdx < 0) {
186 continue;
187 }
188
189 const QString fileName = fileInfo.left(tabIdx);
190 const QString sizeStr = fileInfo.section(QLatin1Char('\t'), 1, 1);
191 const QString mtimeStr = fileInfo.section(QLatin1Char('\t'), 2, 2);
192
193 if (!fileName.endsWith(QStringLiteral(".ulg"), Qt::CaseInsensitive) &&
194 !fileName.endsWith(QStringLiteral(".bin"), Qt::CaseInsensitive)) {
195 continue;
196 }
197
198 bool sizeOk = false;
199 const uint fileSize = sizeStr.toUInt(&sizeOk);
200 if (!sizeOk) {
201 continue;
202 }
203
204 QDateTime dateTime;
205
206 // Prefer the modification time reported by the vehicle when available (0 means unknown).
207 bool mtimeOk = false;
208 const qint64 mtimeSecs = mtimeStr.toLongLong(&mtimeOk);
209 if (mtimeOk && mtimeSecs > 0) {
210 dateTime = QDateTime::fromSecsSinceEpoch(mtimeSecs, QTimeZone::UTC);
211 }
212
213 // Otherwise reconstruct the date from the date sub-directory name and the filename time.
214 if (!dateTime.isValid() && dirDate.isValid()) {
215 const QString baseName = fileName.left(fileName.lastIndexOf(QLatin1Char('.')));
216 const QTime fileTime = QTime::fromString(baseName, QStringLiteral("HH_mm_ss"));
217 if (fileTime.isValid()) {
218 dateTime = QDateTime(dirDate, fileTime, QTimeZone::UTC);
219 } else {
220 dateTime = QDateTime(dirDate, QTime(), QTimeZone::UTC);
221 }
222 }
223
224 const QString ftpPath = subdir.isEmpty()
225 ? (_logRoot + QStringLiteral("/") + fileName)
226 : (_logRoot + QStringLiteral("/") + subdir + QStringLiteral("/") + fileName);
227
228 QGCOnboardLogFtpEntry *const logEntry = new QGCOnboardLogFtpEntry(_logIdCounter++, dateTime, fileSize, true, this);
229 logEntry->setFtpPath(ftpPath);
230 logEntry->setStatus(tr("Available"));
231 _logEntriesModel->append(logEntry);
232 logsFound++;
233 }
234
235 return logsFound;
236}
237
238void OnboardLogFtpController::_listNextSubdir()
239{
240 if (_dirsToList.isEmpty()) {
241 qCDebug(OnboardLogFtpControllerLog) << "listing complete, found" << _logEntriesModel->count() << "logs";
242 _finishListing();
243 return;
244 }
245
246 const QString subdir = _dirsToList.first();
247 const QString path = _logRoot + QStringLiteral("/") + subdir;
248
249 qCDebug(OnboardLogFtpControllerLog) << "listing subdir" << path;
250
251 if (!_vehicle->ftpManager()->listDirectory(MAV_COMP_ID_AUTOPILOT1, path)) {
252 qCWarning(OnboardLogFtpControllerLog) << "failed to list subdir" << path;
253 _dirsToList.removeFirst();
254 _listNextSubdir();
255 }
256}
257
258void OnboardLogFtpController::_finishListing()
259{
260 _listState = Idle;
261 _setListing(false);
262}
263
264void OnboardLogFtpController::download(const QString &path)
265{
266 const QString dir = path.isEmpty() ? SettingsManager::instance()->appSettings()->logSavePath() : path;
267 _downloadToDirectory(dir);
268}
269
270void OnboardLogFtpController::_downloadToDirectory(const QString &dir)
271{
272 _downloadPath = dir;
273 if (_downloadPath.isEmpty()) {
274 return;
275 }
276
277 if (!_downloadPath.endsWith(QDir::separator())) {
278 _downloadPath += QDir::separator();
279 }
280
281 _downloadQueue.clear();
282 const int numLogs = _logEntriesModel->count();
283 for (int i = 0; i < numLogs; i++) {
284 QGCOnboardLogFtpEntry *const entry = _logEntriesModel->value<QGCOnboardLogFtpEntry*>(i);
285 if (entry && entry->selected() && !entry->ftpPath().isEmpty()) {
286 entry->setStatus(tr("Waiting"));
287 _downloadQueue.enqueue(entry);
288 }
289 }
290
291 if (_downloadQueue.isEmpty()) {
292 qCWarning(OnboardLogFtpControllerLog) << "no selected logs have FTP paths for download";
293 return;
294 }
295
296 qCDebug(OnboardLogFtpControllerLog) << "queued" << _downloadQueue.size() << "logs for download to" << _downloadPath;
297 _setDownloading(true);
298
299 _downloadEntry(_downloadQueue.dequeue());
300}
301
302void OnboardLogFtpController::_downloadEntry(QGCOnboardLogFtpEntry *entry)
303{
304 if (!entry || !_vehicle) {
305 return;
306 }
307
308 entry->setSelected(false);
309 emit selectionChanged();
310
311 _currentDownloadEntry = entry;
312 _downloadBytesAtLastUpdate = 0;
313 _downloadRateAvg = 0.;
314 _downloadElapsed.start();
315
316 entry->setStatus(tr("Downloading"));
317
318 QString localFilename;
319 if (entry->time().isValid() && entry->time().date().year() >= 2010) {
320 localFilename = entry->time().toString(QStringLiteral("yyyy-M-d-hh-mm-ss")) + QStringLiteral(".ulg");
321 } else {
322 localFilename = QStringLiteral("log_") + QString::number(entry->id()) + QStringLiteral(".ulg");
323 }
324
325 if (QFile::exists(_downloadPath + localFilename)) {
326 const QStringList parts = localFilename.split(QLatin1Char('.'));
327 uint numDups = 0;
328 do {
329 numDups++;
330 localFilename = parts[0] + QStringLiteral("_") + QString::number(numDups) + QStringLiteral(".") + parts[1];
331 } while (QFile::exists(_downloadPath + localFilename));
332 }
333
334 FTPManager *const ftp = _vehicle->ftpManager();
335 (void) disconnect(ftp, &FTPManager::downloadComplete, this, &OnboardLogFtpController::_downloadComplete);
336 (void) disconnect(ftp, &FTPManager::commandProgress, this, &OnboardLogFtpController::_downloadProgress);
337 (void) connect(ftp, &FTPManager::downloadComplete, this, &OnboardLogFtpController::_downloadComplete);
338 (void) connect(ftp, &FTPManager::commandProgress, this, &OnboardLogFtpController::_downloadProgress);
339
340 qCDebug(OnboardLogFtpControllerLog) << "downloading" << entry->ftpPath() << "to" << _downloadPath + localFilename;
341
342 if (!ftp->download(MAV_COMP_ID_AUTOPILOT1, entry->ftpPath(), _downloadPath, localFilename, true)) {
343 qCWarning(OnboardLogFtpControllerLog) << "failed to start download for" << entry->ftpPath();
344 entry->setStatus(tr("Error"));
345 _currentDownloadEntry = nullptr;
346
347 if (!_downloadQueue.isEmpty()) {
348 _downloadEntry(_downloadQueue.dequeue());
349 } else {
350 _setDownloading(false);
351 }
352 }
353}
354
355void OnboardLogFtpController::_downloadComplete(const QString &file, const QString &errorMsg)
356{
357 if (!_currentDownloadEntry) {
358 return;
359 }
360
361 if (errorMsg.isEmpty()) {
362 _currentDownloadEntry->setStatus(tr("Downloaded"));
363 qCDebug(OnboardLogFtpControllerLog) << "download complete" << file;
364 } else {
365 _currentDownloadEntry->setStatus(tr("Error"));
366 qCWarning(OnboardLogFtpControllerLog) << "download error:" << errorMsg;
367 }
368
369 _currentDownloadEntry = nullptr;
370
371 if (!_downloadQueue.isEmpty()) {
372 _downloadEntry(_downloadQueue.dequeue());
373 } else {
374 _setDownloading(false);
375 }
376}
377
378void OnboardLogFtpController::_downloadProgress(float value)
379{
380 if (!_currentDownloadEntry) {
381 return;
382 }
383
384 if (_downloadElapsed.elapsed() < kGUIRateMs) {
385 return;
386 }
387
388 const size_t totalBytes = static_cast<size_t>(static_cast<qreal>(_currentDownloadEntry->size()) * static_cast<qreal>(value));
389 const size_t bytesSinceLastUpdate = totalBytes - _downloadBytesAtLastUpdate;
390 const qreal elapsedSec = _downloadElapsed.elapsed() / 1000.0;
391 const qreal rate = (elapsedSec > 0) ? (bytesSinceLastUpdate / elapsedSec) : 0;
392 _downloadRateAvg = (_downloadRateAvg * 0.95) + (rate * 0.05);
393 _downloadBytesAtLastUpdate = totalBytes;
394 _downloadElapsed.start();
395
396 const QString status = QStringLiteral("%1 (%2/s)").arg(
397 QGC::bigSizeToString(totalBytes),
398 QGC::bigSizeToString(_downloadRateAvg));
399
400 _currentDownloadEntry->setStatus(status);
401}
402
404{
405 if (!_vehicle) {
406 return;
407 }
408
409 if (_requestingLogEntries) {
410 _vehicle->ftpManager()->cancelListDirectory();
411 _dirsToList.clear();
412 _finishListing();
413 }
414
415 if (_downloadingLogs) {
416 _vehicle->ftpManager()->cancelDownload();
417 if (_currentDownloadEntry) {
418 _currentDownloadEntry->setStatus(tr("Canceled"));
419 _currentDownloadEntry = nullptr;
420 }
421 _downloadQueue.clear();
422 }
423
424 _resetSelection(true);
425 _setDownloading(false);
426}
427
428void OnboardLogFtpController::_resetSelection(bool canceled)
429{
430 const int numLogs = _logEntriesModel->count();
431 for (int i = 0; i < numLogs; i++) {
432 QGCOnboardLogFtpEntry *const entry = _logEntriesModel->value<QGCOnboardLogFtpEntry*>(i);
433 if (!entry) {
434 continue;
435 }
436
437 if (entry->selected()) {
438 if (canceled) {
439 entry->setStatus(tr("Canceled"));
440 }
441 entry->setSelected(false);
442 }
443 }
444
445 emit selectionChanged();
446}
447
448void OnboardLogFtpController::_setDownloading(bool active)
449{
450 if (_downloadingLogs != active) {
451 _downloadingLogs = active;
452 if (_vehicle) {
454 }
456 }
457}
458
459void OnboardLogFtpController::_setListing(bool active)
460{
461 if (_requestingLogEntries != active) {
462 _requestingLogEntries = active;
463 if (_vehicle) {
465 }
467 }
468}
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:498
uint64_t capabilityBits() const
Definition Vehicle.h:709
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:579
bool apmFirmware() const
Definition Vehicle.h:499
FTPManager * ftpManager()
Definition Vehicle.h:580
bool capabilitiesKnown() const
Definition Vehicle.h:708
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