QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
FTPManager.cc
Go to the documentation of this file.
1#include "FTPManager.h"
2#include "MAVLinkProtocol.h"
3#include "AppMessages.h"
5#include "Vehicle.h"
7
8#include <QtCore/QFile>
9#include <QtCore/QFileInfo>
10#include <QtCore/QDir>
11#include <cerrno>
12#include <iterator>
13#include <limits>
14
15QGC_LOGGING_CATEGORY(FTPManagerLog, "Vehicle.FTPManager")
16
18 : QObject (vehicle)
19 , _vehicle (vehicle)
20{
21 _ackOrNakTimeoutTimer.setSingleShot(true);
22 // Mock link responds immediately if at all, speed up unit tests with faster timeout
23 _ackOrNakTimeoutTimer.setInterval(QGC::runningUnitTests() ? kTestAckTimeoutMs : _ackOrNakTimeoutMsecs);
24 connect(&_ackOrNakTimeoutTimer, &QTimer::timeout, this, &FTPManager::_ackOrNakTimeout);
25
26 // Make sure we don't have bad structure packing
27 static_assert(sizeof(MavlinkFTP::RequestHeader) == 12, "MavlinkFTP::RequestHeader must be 12 bytes");
28
29 _uploadState.reset();
30}
31
32bool FTPManager::download(uint8_t fromCompId, const QString& fromURI, const QString& toDir, const QString& fileName, bool checksize)
33{
34 qCDebug(FTPManagerLog) << "Download fromCompId:" << fromCompId
35 << "fromURI:" << fromURI
36 << "to:" << toDir
37 << "fileName:" << fileName;
38
39 if (!_rgStateMachine.isEmpty()) {
40 qCDebug(FTPManagerLog) << "Cannot download. Already in another operation";
41 return false;
42 }
43
44 _downloadState.reset();
45 _downloadState.toDir.setPath(toDir);
46 _downloadState.checksize = checksize;
47
48 if (!_parseURI(fromCompId, fromURI, _downloadState.fullPathOnVehicle, _ftpCompId)) {
49 qCWarning(FTPManagerLog) << "_parseURI failed";
50 return false;
51 }
52
53 // We need to strip off the file name from the fully qualified path. We can't use the usual QDir
54 // routines because this path does not exist locally.
55 int lastDirSlashIndex;
56 for (lastDirSlashIndex=_downloadState.fullPathOnVehicle.size()-1; lastDirSlashIndex>=0; lastDirSlashIndex--) {
57 if (_downloadState.fullPathOnVehicle[lastDirSlashIndex] == '/') {
58 break;
59 }
60 }
61 lastDirSlashIndex++; // move past slash
62
63 if (fileName.isEmpty()) {
64 _downloadState.fileName = _downloadState.fullPathOnVehicle.right(_downloadState.fullPathOnVehicle.size() - lastDirSlashIndex);
65 } else {
66 // fileName can originate from the vehicle: a MAVLink-FTP ListDirectory entry is
67 // surfaced to QML and handed straight back here as the local name. Reduce it the
68 // same way the derived branch above already does, so it cannot act as a path.
69 _downloadState.fileName = QFileInfo(fileName).fileName();
70 }
71
72 qCDebug(FTPManagerLog) << "_downloadState.fullPathOnVehicle:_downloadState.fileName" << _downloadState.fullPathOnVehicle << _downloadState.fileName;
73
74 static const StateFunctions_t rgDownloadStateMachine[] = {
75 { &FTPManager::_openFileROBegin, &FTPManager::_openFileROAckOrNak, &FTPManager::_openFileROTimeout },
76 { &FTPManager::_burstReadFileBegin, &FTPManager::_burstReadFileAckOrNak, &FTPManager::_burstReadFileTimeout },
77 { &FTPManager::_fillMissingBlocksBegin, &FTPManager::_fillMissingBlocksAckOrNak, &FTPManager::_fillMissingBlocksTimeout },
78 { &FTPManager::_resetSessionsBegin, &FTPManager::_resetSessionsAckOrNak, &FTPManager::_resetSessionsTimeout },
79 { &FTPManager::_downloadCompleteNoError, nullptr, nullptr },
80 };
81 _setStateMachine(rgDownloadStateMachine, std::size(rgDownloadStateMachine));
82
83 _downloadState.active = true;
84 _startStateMachine();
85
86 return true;
87}
88
89bool FTPManager::upload(uint8_t toCompId, const QString& toURI, const QString& fromFile)
90{
91 qCDebug(FTPManagerLog) << "upload fromFile:" << fromFile << "toURI:" << toURI << "toCompId:" << toCompId;
92
93 if (!_rgStateMachine.isEmpty()) {
94 qCDebug(FTPManagerLog) << "Cannot upload. Already in another operation";
95 return false;
96 }
97
98 QFileInfo sourceInfo(fromFile);
99 if (!sourceInfo.exists() || !sourceInfo.isFile()) {
100 qCWarning(FTPManagerLog) << "Cannot upload. Source file missing" << fromFile;
101 return false;
102 }
103
104 if (sourceInfo.size() > std::numeric_limits<uint32_t>::max()) {
105 qCWarning(FTPManagerLog) << "Cannot upload. File too large" << fromFile << sourceInfo.size();
106 return false;
107 }
108
109 _uploadState.reset();
110 _uploadState.localFilePath = fromFile;
111 _uploadState.file.setFileName(fromFile);
112 if (!_uploadState.file.open(QFile::ReadOnly)) {
113 qCWarning(FTPManagerLog) << "Cannot upload. Failed to open file" << fromFile << _uploadState.file.errorString();
114 _uploadState.reset();
115 return false;
116 }
117
118 _uploadState.fileSize = static_cast<uint32_t>(sourceInfo.size());
119 _uploadState.totalBytesSent = 0;
120 _uploadState.lastChunkSize = 0;
121 _uploadState.retryCount = 0;
122 _uploadState.sessionId = 0;
123 _uploadState.cancelled = false;
124
125 if (!_parseURI(toCompId, toURI, _uploadState.fullPathOnVehicle, _ftpCompId)) {
126 qCWarning(FTPManagerLog) << "_parseURI failed";
127 _uploadState.reset();
128 return false;
129 }
130
131 static const StateFunctions_t rgUploadStateMachine[] = {
132 { &FTPManager::_createFileBegin, &FTPManager::_createFileAckOrNak, &FTPManager::_createFileTimeout },
133 { &FTPManager::_writeFileBegin, &FTPManager::_writeFileAckOrNak, &FTPManager::_writeFileTimeout },
134 { &FTPManager::_resetSessionsBegin, &FTPManager::_resetSessionsAckOrNak, &FTPManager::_resetSessionsTimeout },
135 { &FTPManager::_uploadFinalize, nullptr, nullptr },
136 };
137 _setStateMachine(rgUploadStateMachine, std::size(rgUploadStateMachine));
138
139 _startStateMachine();
140
141 return true;
142}
143
144bool FTPManager::listDirectory(uint8_t fromCompId, const QString& fromURI)
145{
146 qCDebug(FTPManagerLog) << "list directory fromURI:" << fromURI << "fromCompId:" << fromCompId;
147
148 if (!_rgStateMachine.isEmpty()) {
149 qCDebug(FTPManagerLog) << "Cannot list directory. Already in another operation";
150 return false;
151 }
152
153 _listDirectoryState.reset();
154
155 // Prefer the timestamped listing unless we already learned this vehicle doesn't support it.
156 _listDirectoryState.opCode = (_listDirWithTimeSupport == WithTimeSupport_t::Unsupported)
159
160 // Re-report the cached capability on every listing so any log capture window shows it
161 const char* supportStr =
162 (_listDirWithTimeSupport == WithTimeSupport_t::Supported) ? "vehicle supports it" :
163 (_listDirWithTimeSupport == WithTimeSupport_t::Unsupported) ? "vehicle Nak'ed it earlier this connection" :
164 "support unknown, probing";
165 qCDebug(FTPManagerLog) << "listDirectory using" << MavlinkFTP::opCodeToString(_listDirectoryState.opCode)
166 << "-" << supportStr;
167
168 if (!_parseURI(fromCompId, fromURI, _listDirectoryState.fullPathOnVehicle, _ftpCompId)) {
169 qCWarning(FTPManagerLog) << "_parseURI failed";
170 return false;
171 }
172
173 qCDebug(FTPManagerLog) << "_listDirectoryState.fullPathOnVehicle" << _listDirectoryState.fullPathOnVehicle;
174
175 static const StateFunctions_t rgStateMachine[] = {
176 { &FTPManager::_listDirectoryBegin, &FTPManager::_listDirectoryAckOrNak, &FTPManager::_listDirectoryTimeout },
177 { &FTPManager::_listDirectoryCompleteNoError, nullptr, nullptr },
178 };
179 _setStateMachine(rgStateMachine, std::size(rgStateMachine));
180
181 _startStateMachine();
182
183 return true;
184}
185
186bool FTPManager::deleteFile(uint8_t fromCompId, const QString& fromURI)
187{
188 qCDebug(FTPManagerLog) << "delete file fromURI:" << fromURI << "fromCompId:" << fromCompId;
189
190 if (!_rgStateMachine.isEmpty()) {
191 qCDebug(FTPManagerLog) << "Cannot delete file. Already in another operation";
192 return false;
193 }
194
195 _deleteState.reset();
196
197 if (!_parseURI(fromCompId, fromURI, _deleteState.fullPathOnVehicle, _ftpCompId)) {
198 qCWarning(FTPManagerLog) << "_parseURI failed";
199 return false;
200 }
201
202 qCDebug(FTPManagerLog) << "_deleteState.fullPathOnVehicle" << _deleteState.fullPathOnVehicle;
203
204 static const StateFunctions_t rgStateMachine[] = {
205 { &FTPManager::_deleteFileBegin, &FTPManager::_deleteFileAckOrNak, &FTPManager::_deleteFileTimeout },
206 { &FTPManager::_deleteCompleteNoError, nullptr, nullptr },
207 };
208 _setStateMachine(rgStateMachine, std::size(rgStateMachine));
209
210 _startStateMachine();
211
212 return true;
213}
214
216{
217 if (!_downloadState.inProgress()) {
218 return;
219 }
220
221 // No point waiting for a Terminate ack on a download we are abandoning; _downloadComplete releases the
222 // vehicle session with a fire-and-forget reset instead.
223 _downloadComplete(tr("Aborted"));
224}
225
227{
228 if (!_listDirectoryState.inProgress()) {
229 return;
230 }
231
232 if (_rgStateMachine.isEmpty()) {
233 return;
234 }
235
236 _listDirectoryComplete(tr("Aborted"));
237}
238
240{
241 if (!_uploadState.inProgress()) {
242 return;
243 }
244
245 _uploadState.cancelled = true;
246 _ackOrNakTimeoutTimer.stop();
247 _rgStateMachine.clear();
248
249 if (_uploadState.sessionId != 0) {
250 static const StateFunctions_t rgTerminateStateMachine[] = {
251 { &FTPManager::_terminateUploadSessionBegin, &FTPManager::_terminateUploadSessionAckOrNak, &FTPManager::_terminateUploadSessionTimeout },
252 { &FTPManager::_uploadFinalize, nullptr, nullptr },
253 };
254 for (size_t i=0; i<sizeof(rgTerminateStateMachine)/sizeof(rgTerminateStateMachine[0]); i++) {
255 _rgStateMachine.append(rgTerminateStateMachine[i]);
256 }
257 _uploadState.retryCount = 0;
258 _startStateMachine();
259 } else {
260 _uploadComplete(tr("Aborted"));
261 }
262}
263
265{
266 if (!_deleteState.inProgress()) {
267 return;
268 }
269
270 _deleteComplete(tr("Aborted"));
271}
272
275void FTPManager::_downloadComplete(const QString& errorMsg)
276{
277 qCDebug(FTPManagerLog) << QString("_downloadComplete: errorMsg(%1)").arg(errorMsg);
278
279 QString downloadFilePath = _downloadState.toDir.absoluteFilePath(_downloadState.fileName);
280 QString error = errorMsg;
281
282 _ackOrNakTimeoutTimer.stop();
283 _rgStateMachine.clear();
284 _currentStateMachineIndex = -1;
285 if (_downloadState.file.isOpen()) {
286 _downloadState.file.close();
287 if (!errorMsg.isEmpty()) {
288 _downloadState.file.remove();
289 }
290 }
291
292 // Abandoning a session leaves single-session servers (PX4) unable to open anything else. Sent even if the
293 // open never acked: the server may have opened it after we gave up.
294 if (!errorMsg.isEmpty()) {
295 _sendResetSessionsNoAck();
296 }
297 _downloadState.active = false;
298
299 emit downloadComplete(downloadFilePath, errorMsg);
300}
301
304void FTPManager::_listDirectoryComplete(const QString& errorMsg)
305{
306 qCDebug(FTPManagerLog) << QString("_listDirectoryComplete: errorMsg(%1)").arg(errorMsg);
307
308 _ackOrNakTimeoutTimer.stop();
309 _rgStateMachine.clear();
310 _currentStateMachineIndex = -1;
311
312 QStringList rgDirectoryList = _listDirectoryState.rgDirectoryList;
313 if (!errorMsg.isEmpty()) {
314 rgDirectoryList.clear();
315 }
316
317 _listDirectoryState.reset();
318
319 emit listDirectoryComplete(errorMsg.isEmpty() ? rgDirectoryList : QStringList(), errorMsg);
320}
321
322void FTPManager::_deleteFileBegin(void)
323{
324 qCDebug(FTPManagerLog) << "file" << _deleteState.fullPathOnVehicle;
325
326 MavlinkFTP::Request request{};
327 request.hdr.session = 0;
328 request.hdr.opcode = MavlinkFTP::kCmdRemoveFile;
329 request.hdr.offset = 0;
330 request.hdr.size = 0;
331 _fillRequestDataWithString(&request, _deleteState.fullPathOnVehicle);
332 _sendRequestExpectAck(&request);
333}
334
335void FTPManager::_deleteFileAckOrNak(const MavlinkFTP::Request* ackOrNak)
336{
337 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
338 if (requestOpCode != MavlinkFTP::kCmdRemoveFile) {
339 qCDebug(FTPManagerLog) << "_deleteFileAckOrNak: Ack disregarding ack for incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
340 return;
341 }
342 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
343 qCDebug(FTPManagerLog) << "_deleteFileAckOrNak: Ack disregarding ack for incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
344 return;
345 }
346
347 _ackOrNakTimeoutTimer.stop();
348
349 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
350 _advanceStateMachine();
351 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
352 qCDebug(FTPManagerLog) << "_deleteFileAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
353 _deleteComplete(tr("Delete failed") + ": " + _errorMsgFromNak(ackOrNak));
354 }
355}
356
357void FTPManager::_deleteFileTimeout(void)
358{
359 if (++_deleteState.retryCount > _maxRetry) {
360 qCDebug(FTPManagerLog) << QString("_deleteFileTimeout retries exceeded");
361 _deleteComplete(tr("Delete failed"));
362 } else {
363 qCDebug(FTPManagerLog) << QString("_deleteFileTimeout: retrying - retryCount(%1)").arg(_deleteState.retryCount);
364 _deleteFileBegin();
365 }
366}
367
368void FTPManager::_deleteComplete(const QString& errorMsg)
369{
370 qCDebug(FTPManagerLog) << QString("_deleteComplete: errorMsg(%1)").arg(errorMsg);
371
372 const QString deletedPath = _deleteState.fullPathOnVehicle;
373
374 _ackOrNakTimeoutTimer.stop();
375 _rgStateMachine.clear();
376 _currentStateMachineIndex = -1;
377
378 _deleteState.reset();
379
380 emit deleteComplete(deletedPath, errorMsg);
381}
382
383void FTPManager::_createFileBegin(void)
384{
385 qCDebug(FTPManagerLog) << "file" << _uploadState.fullPathOnVehicle;
386
387 MavlinkFTP::Request request{};
388 request.hdr.session = 0;
389 request.hdr.opcode = MavlinkFTP::kCmdCreateFile;
390 request.hdr.offset = 0;
391 request.hdr.size = 0;
392 _fillRequestDataWithString(&request, _uploadState.fullPathOnVehicle);
393 _sendRequestExpectAck(&request);
394}
395
396void FTPManager::_createFileAckOrNak(const MavlinkFTP::Request* ackOrNak)
397{
398 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
399 if (requestOpCode != MavlinkFTP::kCmdCreateFile) {
400 qCDebug(FTPManagerLog) << "_createFileAckOrNak: Ack disregarding ack for incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
401 return;
402 }
403 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
404 qCDebug(FTPManagerLog) << "_createFileAckOrNak: Ack disregarding ack for incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
405 return;
406 }
407
408 _ackOrNakTimeoutTimer.stop();
409
410 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
411 qCDebug(FTPManagerLog) << "_createFileAckOrNak: Ack - sessionId" << ackOrNak->hdr.session;
412
413 _uploadState.sessionId = ackOrNak->hdr.session;
414 _advanceStateMachine();
415 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
416 qCDebug(FTPManagerLog) << "_createFileAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
417 _uploadComplete(tr("Upload failed for: %1 - error: %2").arg(_uploadState.fullPathOnVehicle).arg(_errorMsgFromNak(ackOrNak)));
418 }
419}
420
421void FTPManager::_createFileTimeout(void)
422{
423 qCDebug(FTPManagerLog) << "_createFileTimeout";
424 _uploadComplete(tr("Upload failed for: %1 - no response from vehicle").arg(_uploadState.fullPathOnVehicle));
425}
426
427void FTPManager::_writeFileBegin(void)
428{
429 _writeFileWorker(true /* firstRequest */);
430}
431
432void FTPManager::_writeFileWorker(bool firstRequest)
433{
434 if (!_uploadState.file.isOpen()) {
435 _uploadComplete(tr("Upload failed for: %1 - file not open").arg(_uploadState.fullPathOnVehicle));
436 return;
437 }
438
439 if (_uploadState.totalBytesSent >= _uploadState.fileSize) {
440 _advanceStateMachine();
441 return;
442 }
443
444 qCDebug(FTPManagerLog) << "_writeFileWorker: offset:firstRequest:retryCount" << _uploadState.totalBytesSent << firstRequest << _uploadState.retryCount;
445
446 MavlinkFTP::Request request{};
447 request.hdr.session = _uploadState.sessionId;
448 request.hdr.opcode = MavlinkFTP::kCmdWriteFile;
449 request.hdr.offset = _uploadState.totalBytesSent;
450
451 if (firstRequest) {
452 _uploadState.retryCount = 0;
453 } else {
454 _expectedIncomingSeqNumber -= 2;
455 }
456
457 qint64 bytesRemaining = static_cast<qint64>(_uploadState.fileSize) - static_cast<qint64>(_uploadState.totalBytesSent);
458 qint64 bytesToSend = bytesRemaining;
459 if (bytesToSend > static_cast<qint64>(sizeof(request.data))) {
460 bytesToSend = sizeof(request.data);
461 }
462
463 if (!_uploadState.file.seek(_uploadState.totalBytesSent)) {
464 qCDebug(FTPManagerLog) << "_writeFileWorker: seek failed" << _uploadState.file.errorString();
465 _uploadComplete(tr("Upload failed for: %1 - error reading file").arg(_uploadState.fullPathOnVehicle));
466 return;
467 }
468
469 qint64 bytesRead = _uploadState.file.read(reinterpret_cast<char*>(request.data), bytesToSend);
470 if (bytesRead != bytesToSend) {
471 qCDebug(FTPManagerLog) << "_writeFileWorker: read failed" << _uploadState.file.errorString();
472 _uploadComplete(tr("Upload failed for: %1 - error reading file").arg(_uploadState.fullPathOnVehicle));
473 return;
474 }
475
476 request.hdr.size = static_cast<uint8_t>(bytesRead);
477 _uploadState.lastChunkSize = static_cast<uint32_t>(bytesRead);
478
479 _sendRequestExpectAck(&request);
480}
481
482void FTPManager::_writeFileAckOrNak(const MavlinkFTP::Request* ackOrNak)
483{
484 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
485
486 if (requestOpCode != MavlinkFTP::kCmdWriteFile) {
487 qCDebug(FTPManagerLog) << "_writeFileAckOrNak: Disregarding due to incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
488 return;
489 }
490 if (ackOrNak->hdr.session != _uploadState.sessionId) {
491 qCDebug(FTPManagerLog) << "_writeFileAckOrNak: Disregarding due to incorrect session id actual:expected" << ackOrNak->hdr.session << _uploadState.sessionId;
492 return;
493 }
494
495 _ackOrNakTimeoutTimer.stop();
496
497 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
498 if (ackOrNak->hdr.seqNumber < _expectedIncomingSeqNumber) {
499 qCDebug(FTPManagerLog) << "_writeFileAckOrNak: Disregarding Ack due to incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
500 return;
501 }
502
503 if (ackOrNak->hdr.size != 0) {
504 qCDebug(FTPManagerLog) << "_writeFileAckOrNak: unexpected ack size expected:actual 0" << ackOrNak->hdr.size;
505 }
506
507 _uploadState.totalBytesSent += _uploadState.lastChunkSize;
508 _uploadState.lastChunkSize = 0;
509 _expectedIncomingSeqNumber = ackOrNak->hdr.seqNumber;
510
511 if (_uploadState.fileSize != 0) {
512 emit commandProgress(static_cast<float>(_uploadState.totalBytesSent) / static_cast<float>(_uploadState.fileSize));
513 }
514
515 if (_uploadState.totalBytesSent >= _uploadState.fileSize) {
516 _advanceStateMachine();
517 } else {
518 _writeFileWorker(true /* firstRequest */);
519 }
520 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
521 qCDebug(FTPManagerLog) << "_writeFileAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
522 _uploadComplete(tr("Upload failed for: %1 - error: %2").arg(_uploadState.fullPathOnVehicle).arg(_errorMsgFromNak(ackOrNak)));
523 }
524}
525
526void FTPManager::_writeFileTimeout(void)
527{
528 if (++_uploadState.retryCount > _maxRetry) {
529 qCDebug(FTPManagerLog) << QString("_writeFileTimeout retries exceeded");
530 _uploadComplete(tr("Upload failed for: %1 - no response from vehicle").arg(_uploadState.fullPathOnVehicle));
531 } else {
532 qCDebug(FTPManagerLog) << QString("_writeFileTimeout: retrying - retryCount(%1) offset(%2)").arg(_uploadState.retryCount).arg(_uploadState.totalBytesSent);
533 _writeFileWorker(false /* firstRequest */);
534 }
535}
536
537void FTPManager::_terminateUploadSessionBegin(void)
538{
539 if (_uploadState.sessionId == 0) {
540 qCWarning(FTPManagerLog) << "_terminateUploadSessionBegin: No session to terminate";
541 _advanceStateMachine();
542 return;
543 }
544
545 MavlinkFTP::Request request{};
546 request.hdr.session = _uploadState.sessionId;
547 request.hdr.opcode = MavlinkFTP::kCmdTerminateSession;
548 _sendRequestExpectAck(&request);
549}
550
551void FTPManager::_terminateUploadSessionAckOrNak(const MavlinkFTP::Request* ackOrNak)
552{
553 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
554 if (requestOpCode != MavlinkFTP::kCmdTerminateSession) {
555 qCDebug(FTPManagerLog) << "_terminateUploadSessionAckOrNak: Ack disregarding ack for incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
556 return;
557 }
558 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
559 qCDebug(FTPManagerLog) << "_terminateUploadSessionAckOrNak: Ack disregarding ack for incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
560 return;
561 }
562
563 _ackOrNakTimeoutTimer.stop();
564
565 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
566 qCDebug(FTPManagerLog) << "_terminateUploadSessionAckOrNak: Ack";
567 _advanceStateMachine();
568 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
569 qCDebug(FTPManagerLog) << "_terminateUploadSessionAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
570 _uploadComplete(tr("Upload failed for: %1 - error: %2").arg(_uploadState.fullPathOnVehicle).arg(_errorMsgFromNak(ackOrNak)));
571 }
572}
573
574void FTPManager::_terminateUploadSessionTimeout(void)
575{
576 if (++_uploadState.retryCount > _maxRetry) {
577 qCDebug(FTPManagerLog) << QString("_terminateUploadSessionTimeout retries exceeded");
578 _uploadComplete(tr("Upload failed for: %1 - no response from vehicle").arg(_uploadState.fullPathOnVehicle));
579 } else {
580 qCDebug(FTPManagerLog) << QString("_terminateUploadSessionTimeout: retrying - retryCount(%1)").arg(_uploadState.retryCount);
581 _terminateUploadSessionBegin();
582 }
583}
584
585void FTPManager::_uploadFinalize(void)
586{
587 QString error = _uploadState.cancelled ? tr("Aborted for: %1").arg(_uploadState.fullPathOnVehicle) : QString();
588 _uploadComplete(error);
589}
590
591void FTPManager::_uploadComplete(const QString& errorMsg)
592{
593 qCDebug(FTPManagerLog) << QString("_uploadComplete: errorMsg(%1)").arg(errorMsg)
594 << "local" << _uploadState.localFilePath
595 << "remote" << _uploadState.fullPathOnVehicle;
596
597 QString remotePath = _uploadState.fullPathOnVehicle;
598
599 _ackOrNakTimeoutTimer.stop();
600 _rgStateMachine.clear();
601 _currentStateMachineIndex = -1;
602
603 if (_uploadState.file.isOpen()) {
604 _uploadState.file.close();
605 }
606
607 _uploadState.reset();
608
609 emit uploadComplete(remotePath, errorMsg);
610}
611
612void FTPManager::_mavlinkMessageReceived(const mavlink_message_t& message)
613{
614 if (message.msgid != MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL ||
615 message.sysid != _vehicle->id() || message.compid != _ftpCompId) {
616 return;
617 }
618
619 if (_currentStateMachineIndex == -1) {
620 return;
621 }
622
623 mavlink_file_transfer_protocol_t data;
624 mavlink_msg_file_transfer_protocol_decode(&message, &data);
625
626 // Make sure we are the target system
627 int qgcId = MAVLinkProtocol::instance()->getSystemId();
628 if (data.target_system != qgcId) {
629 return;
630 }
631
632 MavlinkFTP::Request* request = (MavlinkFTP::Request*)&data.payload[0];
633
634 // Reject messages where hdr.size exceeds data array bounds to prevent over-reads
635 if (request->hdr.size > sizeof(request->data)) {
636 qCWarning(FTPManagerLog) << "_mavlinkMessageReceived: hdr.size exceeds data array, discarding." << request->hdr.size;
637 return;
638 }
639
640 // Ignore old/reordered packets (handle wrap-around properly). Burst data is the exception: its offset says
641 // where it goes, and a late packet from an earlier burst is still good data.
642 uint16_t actualIncomingSeqNumber = request->hdr.seqNumber;
643 const bool isBurstData = (request->hdr.opcode == MavlinkFTP::kRspAck)
644 && (request->hdr.req_opcode == MavlinkFTP::kCmdBurstReadFile)
645 && (_rgStateMachine[_currentStateMachineIndex].ackNakFn == &FTPManager::_burstReadFileAckOrNak);
646 if (!isBurstData && (uint16_t)((_expectedIncomingSeqNumber - 1) - actualIncomingSeqNumber) < (std::numeric_limits<uint16_t>::max()/2)) {
647 qCDebug(FTPManagerLog) << "_mavlinkMessageReceived: Received old packet seqNum expected:actual" << _expectedIncomingSeqNumber << actualIncomingSeqNumber
648 << "hdr.opcode:hdr.req_opcode" << MavlinkFTP::opCodeToString(static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode)) << MavlinkFTP::opCodeToString(static_cast<MavlinkFTP::OpCode_t>(request->hdr.req_opcode));
649
650 return;
651 }
652
653 qCDebug(FTPManagerLog) << "_mavlinkMessageReceived: hdr.opcode:hdr.req_opcode:seqNumber"
654 << MavlinkFTP::opCodeToString(static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode)) << MavlinkFTP::opCodeToString(static_cast<MavlinkFTP::OpCode_t>(request->hdr.req_opcode))
655 << request->hdr.seqNumber;
656
657 (this->*_rgStateMachine[_currentStateMachineIndex].ackNakFn)(request);
658}
659
660void FTPManager::_startStateMachine(void)
661{
662 _currentStateMachineIndex = -1;
663 _advanceStateMachine();
664}
665
666void FTPManager::_advanceStateMachine(void)
667{
668 _currentStateMachineIndex++;
669 (this->*_rgStateMachine[_currentStateMachineIndex].beginFn)();
670}
671
672void FTPManager::_ackOrNakTimeout(void)
673{
674 (this->*_rgStateMachine[_currentStateMachineIndex].timeoutFn)();
675}
676
677void FTPManager::_fillRequestDataWithString(MavlinkFTP::Request* request, const QString& str)
678{
679 strncpy((char *)&request->data[0], str.toStdString().c_str(), sizeof(request->data));
680 request->hdr.size = static_cast<uint8_t>(strnlen((const char *)&request->data[0], sizeof(request->data)));
681}
682
683QString FTPManager::_errorMsgFromNak(const MavlinkFTP::Request* nak)
684{
685 QString errorMsg;
686 MavlinkFTP::ErrorCode_t errorCode = static_cast<MavlinkFTP::ErrorCode_t>(nak->data[0]);
687
688 // Nak's normally have 1 byte of data for error code, except for MavlinkFTP::kErrFailErrno which has additional byte for errno
689 if ((errorCode == MavlinkFTP::kErrFailErrno && nak->hdr.size != 2) || ((errorCode != MavlinkFTP::kErrFailErrno) && nak->hdr.size != 1)) {
690 errorMsg = tr("Invalid Nak format");
691 } else if (errorCode == MavlinkFTP::kErrFailErrno) {
692 // PX4 reports a missing file as errno rather than kErrFailFileNotFound. The value is the
693 // vehicle's, but ENOENT is 2 on every platform PX4 runs on.
694 errorMsg = (nak->data[1] == ENOENT) ? MavlinkFTP::errorCodeToString(MavlinkFTP::kErrFailFileNotFound)
695 : tr("errno %1").arg(nak->data[1]);
696 } else {
697 errorMsg = MavlinkFTP::errorCodeToString(errorCode);
698 }
699
700 return errorMsg;
701}
702
703void FTPManager::_openFileROBegin(void)
704{
705 MavlinkFTP::Request request{};
706 request.hdr.session = 0;
707 request.hdr.opcode = MavlinkFTP::kCmdOpenFileRO;
708 request.hdr.offset = 0;
709 request.hdr.size = 0;
710 _fillRequestDataWithString(&request, _downloadState.fullPathOnVehicle);
711 _sendRequestExpectAck(&request);
712}
713
714void FTPManager::_openFileROTimeout(void)
715{
716 qCDebug(FTPManagerLog) << "_openFileROTimeout";
717 _downloadComplete(tr("Download failed"));
718}
719
720void FTPManager::_openFileROAckOrNak(const MavlinkFTP::Request* ackOrNak)
721{
722 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
723 if (requestOpCode != MavlinkFTP::kCmdOpenFileRO) {
724 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: Ack disregarding ack for incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
725 return;
726 }
727 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
728 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: Ack disregarding ack for incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
729 return;
730 }
731
732 _ackOrNakTimeoutTimer.stop();
733
734 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
735 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: Ack - sessionId:openFileLength" << ackOrNak->hdr.session << ackOrNak->openFileLength;
736
737 if (ackOrNak->hdr.size != sizeof(uint32_t)) {
738 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: Ack ack->hdr.size != sizeof(uint32_t)" << ackOrNak->hdr.size << sizeof(uint32_t);
739 _downloadComplete(tr("Download failed"));
740 return;
741 }
742
743 if (_downloadState.resumeStateIndex >= 0) {
744 if (_downloadState.checksize && (ackOrNak->openFileLength != _downloadState.fileSize)) {
745 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: file size changed across re-open" << ackOrNak->openFileLength << _downloadState.fileSize;
746 _downloadComplete(tr("Download failed"));
747 return;
748 }
749 const int resumeStateIndex = _downloadState.resumeStateIndex;
750 _downloadState.resumeStateIndex = -1;
751 _downloadState.sessionId = ackOrNak->hdr.session;
752 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: session re-opened, resuming at offset" << _downloadState.expectedOffset;
753 _currentStateMachineIndex = resumeStateIndex;
754 (this->*_rgStateMachine[resumeStateIndex].beginFn)();
755 return;
756 }
757
758 _downloadState.sessionId = ackOrNak->hdr.session;
759 _downloadState.fileSize = ackOrNak->openFileLength;
760 _downloadState.expectedOffset = 0;
761
762 _downloadState.file.setFileName(_downloadState.toDir.filePath(_downloadState.fileName));
763 if (_downloadState.file.open(QFile::WriteOnly | QFile::Truncate)) {
764 _advanceStateMachine();
765 } else {
766 qCDebug(FTPManagerLog) << "_openFileROAckOrNak: Ack _downloadState.file open failed" << _downloadState.file.errorString();
767 _downloadComplete(tr("Download failed"));
768 }
769 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
770 qCDebug(FTPManagerLog) << "_handlOpenFileROAck: Nak -" << _errorMsgFromNak(ackOrNak);
771 _downloadComplete(tr("Download failed") + ": " + _errorMsgFromNak(ackOrNak));
772 }
773}
774
775uint8_t FTPManager::_readChunkSize(void) const
776{
777 const SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock();
778 return (sharedLink && sharedLink->isRadioLink()) ? kRadioReadChunkSize : kFullReadChunkSize;
779}
780
781void FTPManager::_burstReadFileWorker(bool firstRequest)
782{
783 const uint8_t chunkSize = _readChunkSize();
784 qCDebug(FTPManagerLog) << "_burstReadFileWorker: starting burst at offset:firstRequest:retryCount:chunkSize" << _downloadState.expectedOffset << firstRequest << _downloadState.retryCount << chunkSize;
785
786 MavlinkFTP::Request request{};
787 request.hdr.session = _downloadState.sessionId;
788 request.hdr.opcode = MavlinkFTP::kCmdBurstReadFile;
789 request.hdr.offset = _downloadState.expectedOffset;
790 request.hdr.size = chunkSize;
791
792 if (firstRequest) {
793 _downloadState.retryCount = 0;
794 } else {
795 // Must used same sequence number as previous request
796 _expectedIncomingSeqNumber -= 2;
797 }
798
799 _sendRequestExpectAck(&request);
800}
801
802void FTPManager::_burstReadFileBegin(void)
803{
804 _burstReadFileWorker(true /* firstRequestr */);
805}
806
807void FTPManager::_burstReadFileAckOrNak(const MavlinkFTP::Request* ackOrNak)
808{
809 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
810
811 if (requestOpCode != MavlinkFTP::kCmdBurstReadFile) {
812 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak: Disregarding due to incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
813 return;
814 }
815 if (ackOrNak->hdr.session != _downloadState.sessionId) {
816 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak: Disregarding due to incorrect session id actual:expected" << ackOrNak->hdr.session << _downloadState.sessionId;
817 return;
818 }
819
820 _ackOrNakTimeoutTimer.stop();
821
822 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
823 qCDebug(FTPManagerLog) << QString("_burstReadFileAckOrNak: Ack offset(%1) size(%2) burstComplete(%3)").arg(ackOrNak->hdr.offset).arg(ackOrNak->hdr.size).arg(ackOrNak->hdr.burstComplete);
824
825 if (ackOrNak->hdr.offset < _downloadState.expectedOffset) {
826 // Behind the frontier: a late packet from an earlier burst or a duplicate. Keep it if it plugs a hole,
827 // and leave sequence tracking alone since it belongs to a stream we have moved past.
828 if (_claimMissingRange(ackOrNak->hdr.offset, ackOrNak->hdr.size)) {
829 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak: late packet fills hole at offset" << ackOrNak->hdr.offset;
830 _downloadState.file.seek(ackOrNak->hdr.offset);
831 if (_downloadState.file.write((const char*)ackOrNak->data, ackOrNak->hdr.size) != ackOrNak->hdr.size) {
832 _downloadComplete(tr("Download failed: Error saving file"));
833 return;
834 }
835 _downloadState.bytesWritten += ackOrNak->hdr.size;
836 } else {
837 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak: duplicate packet at offset" << ackOrNak->hdr.offset;
838 }
839 _ackOrNakTimeoutTimer.start();
840 if (_downloadState.fileSize != 0) {
841 emit commandProgress((float)(_downloadState.bytesWritten) / (float)_downloadState.fileSize);
842 }
843 return;
844 }
845
846 if (ackOrNak->hdr.offset > _downloadState.expectedOffset) {
847 // There is a hole in our data, record it as missing and continue on
848 MissingData_t missingData;
849 missingData.offset = _downloadState.expectedOffset;
850 missingData.cBytesMissing = ackOrNak->hdr.offset - _downloadState.expectedOffset;
851 _downloadState.rgMissingData.append(missingData);
852 qCDebug(FTPManagerLog) << "_handleBurstReadFileAck: adding missing data offset:cBytesMissing" << missingData.offset << missingData.cBytesMissing;
853 }
854
855 _downloadState.file.seek(ackOrNak->hdr.offset);
856 int bytesWritten = _downloadState.file.write((const char*)ackOrNak->data, ackOrNak->hdr.size);
857 if (bytesWritten != ackOrNak->hdr.size) {
858 _downloadComplete(tr("Download failed: Error saving file"));
859 return;
860 }
861 _downloadState.bytesWritten += ackOrNak->hdr.size;
862 _downloadState.expectedOffset = ackOrNak->hdr.offset + ackOrNak->hdr.size;
863
864 if (ackOrNak->hdr.burstComplete) {
865 // The current burst is done, request next one in offset sequence
866 _expectedIncomingSeqNumber = ackOrNak->hdr.seqNumber;
867 _burstReadFileWorker(true /* firstRequest */);
868 } else {
869 // Still within a burst, next ack should come automatically
870 _expectedIncomingSeqNumber = ackOrNak->hdr.seqNumber + 1;
871 _ackOrNakTimeoutTimer.start();
872 }
873
874 // Emit progress last, as cancel could be called in there
875 if (_downloadState.fileSize != 0) {
876 emit commandProgress((float)(_downloadState.bytesWritten) / (float)_downloadState.fileSize);
877 }
878 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
879 MavlinkFTP::ErrorCode_t errorCode = static_cast<MavlinkFTP::ErrorCode_t>(ackOrNak->data[0]);
880
881 if (errorCode == MavlinkFTP::kErrEOF) {
882 // Burst sequence has gone through the whole file
883 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
884 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak: EOF Nak"
885 "with incorrect sequence nr actual:expected"
886 << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
887 /* We have received the EOF Nak but out of sequence, i.e. data is missing */
888 _expectedIncomingSeqNumber = ackOrNak->hdr.seqNumber;
889 _burstReadFileWorker(true); /* Retry from last expected offset */
890 } else {
891 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak EOF";
892 _advanceStateMachine();
893 }
894 } else if (errorCode == MavlinkFTP::kErrInvalidSession) {
895 _downloadSessionLost();
896 } else { /* Don't care is this is out of sequence */
897 qCDebug(FTPManagerLog) << "_burstReadFileAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
898 _downloadComplete(tr("Download failed"));
899 }
900 }
901}
902
903bool FTPManager::_claimMissingRange(uint32_t offset, uint32_t cBytes)
904{
905 if (cBytes == 0 || offset > std::numeric_limits<uint32_t>::max() - cBytes) {
906 return false;
907 }
908
909 for (int i = 0; i < _downloadState.rgMissingData.count(); i++) {
910 MissingData_t& hole = _downloadState.rgMissingData[i];
911 const uint32_t holeEnd = hole.offset + hole.cBytesMissing;
912 if ((offset < hole.offset) || (offset + cBytes > holeEnd)) {
913 continue;
914 }
915
916 const MissingData_t after{ offset + cBytes, holeEnd - (offset + cBytes) };
917 if (offset == hole.offset) {
918 if (after.cBytesMissing == 0) {
919 _downloadState.rgMissingData.removeAt(i);
920 } else {
921 hole = after;
922 }
923 } else {
924 hole.cBytesMissing = offset - hole.offset;
925 if (after.cBytesMissing != 0) {
926 _downloadState.rgMissingData.insert(i + 1, after);
927 }
928 }
929 return true;
930 }
931 return false;
932}
933
934void FTPManager::_downloadSessionLost(void)
935{
936 if (++_downloadState.sessionReopenCount > kMaxDownloadSessionReopens) {
937 qCDebug(FTPManagerLog) << "_downloadSessionLost: re-open limit reached";
938 _downloadComplete(tr("Download failed"));
939 return;
940 }
941
942 qCDebug(FTPManagerLog) << "_downloadSessionLost: re-opening to resume at offset" << _downloadState.expectedOffset << "reopenCount" << _downloadState.sessionReopenCount;
943 _downloadState.resumeStateIndex = _currentStateMachineIndex;
944 // Back to the front of the machine: ResetSessions then OpenFileRO, which jumps back to resumeStateIndex
945 _currentStateMachineIndex = -1;
946 _advanceStateMachine();
947}
948
949void FTPManager::_burstReadFileTimeout(void)
950{
951 if (++_downloadState.retryCount > _maxRetry) {
952 qCDebug(FTPManagerLog) << QString("_burstReadFileTimeout retries exceeded");
953 _downloadComplete(tr("Download failed"));
954 } else {
955 // Try again
956 qCDebug(FTPManagerLog) << QString("_burstReadFileTimeout: retrying - retryCount(%1) offset(%2)").arg(_downloadState.retryCount).arg(_downloadState.expectedOffset);
957 _burstReadFileWorker(false /* firstReqeust */);
958 }
959}
960
961void FTPManager::_listDirectoryWorker(bool firstRequest)
962{
963 qCDebug(FTPManagerLog) << "_listDirectoryWorker: offset:firstRequest:retryCount" << _listDirectoryState.expectedOffset << firstRequest << _listDirectoryState.retryCount;
964
965 MavlinkFTP::Request request{};
966 request.hdr.session = _downloadState.sessionId;
967 request.hdr.opcode = _listDirectoryState.opCode;
968 request.hdr.offset = _listDirectoryState.expectedOffset;
969 request.hdr.size = sizeof(request.data);
970 _fillRequestDataWithString(&request, _listDirectoryState.fullPathOnVehicle);
971
972 if (firstRequest) {
973 _listDirectoryState.retryCount = 0;
974 } else {
975 // Must used same sequence number as previous request
976 _expectedIncomingSeqNumber -= 2;
977 }
978
979 _sendRequestExpectAck(&request);
980}
981
982void FTPManager::_listDirectoryBegin(void)
983{
984 _listDirectoryWorker(true /* firstRequest */);
985}
986
987void FTPManager::_listDirectoryAckOrNak(const MavlinkFTP::Request* ackOrNak)
988{
989 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
990
991 if (requestOpCode != _listDirectoryState.opCode) {
992 qCDebug(FTPManagerLog) << "_listDirectoryAckOrNak: Disregarding due to incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
993 return;
994 }
995
996 _ackOrNakTimeoutTimer.stop();
997
998 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
999 if (ackOrNak->hdr.seqNumber < _expectedIncomingSeqNumber) {
1000 qCDebug(FTPManagerLog) << "_listDirectoryAckOrNak: Disregarding Ack due to incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
1001 return;
1002 }
1003
1004 if (_listDirectoryState.opCode == MavlinkFTP::kCmdListDirectoryWithTime) {
1005 _listDirWithTimeSupport = WithTimeSupport_t::Supported;
1006 }
1007
1008 qCDebug(FTPManagerLog) << QString("_listDirectoryAckOrNak: Ack size(%1)").arg(ackOrNak->hdr.size);
1009
1010 // Parse entries in ackOrNak->data into _listDirectoryState.rgDirectoryList
1011 const char* curDataPtr = (const char*)ackOrNak->data;
1012 while (curDataPtr < (const char*)ackOrNak->data + ackOrNak->hdr.size) {
1013 QString dirEntry = curDataPtr;
1014 curDataPtr += dirEntry.size() + 1;
1015 _listDirectoryState.rgDirectoryList.append(dirEntry);
1016 _listDirectoryState.expectedOffset++;
1017 }
1018
1019 // Request next set of directory entries
1020 _expectedIncomingSeqNumber = ackOrNak->hdr.seqNumber;
1021 _listDirectoryWorker(true /* firstRequest */);
1022 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
1023 MavlinkFTP::ErrorCode_t errorCode = static_cast<MavlinkFTP::ErrorCode_t>(ackOrNak->data[0]);
1024
1025 if (errorCode == MavlinkFTP::kErrEOF) {
1026 // All entries returned
1027 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
1028 qCDebug(FTPManagerLog) << "_listDirectoryAckOrNak: Disregarding Nak due to incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
1029 _ackOrNakTimeoutTimer.start();
1030 return;
1031 } else {
1032 qCDebug(FTPManagerLog) << "_listDirectoryAckOrNak EOF";
1033 if (_listDirectoryState.opCode == MavlinkFTP::kCmdListDirectoryWithTime) {
1034 _listDirWithTimeSupport = WithTimeSupport_t::Supported;
1035 }
1036 _advanceStateMachine();
1037 }
1038 } else if (_listDirectoryState.opCode == MavlinkFTP::kCmdListDirectoryWithTime &&
1039 (errorCode == MavlinkFTP::kErrUnknownCommand ||
1040 (errorCode == MavlinkFTP::kErrFail && _listDirWithTimeSupport == WithTimeSupport_t::Unknown))) {
1041 // Server doesn't implement kCmdListDirectoryWithTime. Remember that, fall back to the
1042 // plain listing and restart from the beginning. ArduPilot Naks unknown opcodes with a
1043 // generic kErrFail rather than kErrUnknownCommand, so kErrFail is also treated as
1044 // unsupported while probing. We act on it without a strict sequence check; the restart
1045 // is idempotent (offset and accumulated entries are reset).
1046 qCDebug(FTPManagerLog) << "_listDirectoryAckOrNak: kCmdListDirectoryWithTime unsupported, falling back to kCmdListDirectory";
1047 _listDirWithTimeSupport = WithTimeSupport_t::Unsupported;
1048 _listDirectoryState.opCode = MavlinkFTP::kCmdListDirectory;
1049 _listDirectoryState.expectedOffset = 0;
1050 _listDirectoryState.rgDirectoryList.clear();
1051 _expectedIncomingSeqNumber = ackOrNak->hdr.seqNumber;
1052 _listDirectoryWorker(true /* firstRequest */);
1053 } else { /* Don't care is this is out of sequence */
1054 qCDebug(FTPManagerLog) << "_listDirectoryAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
1055 _listDirectoryComplete(tr("List directory failed"));
1056 }
1057 }
1058}
1059
1060void FTPManager::_listDirectoryTimeout(void)
1061{
1062 if (++_listDirectoryState.retryCount > _maxRetry) {
1063 qCDebug(FTPManagerLog) << QString("_listDirectoryTimeout retries exceeded");
1064 _listDirectoryComplete(tr("List directory failed"));
1065 } else {
1066 // Try again
1067 qCDebug(FTPManagerLog) << QString("_listDirectoryTimeout: retrying - retryCount(%1) offset(%2)").arg(_listDirectoryState.retryCount).arg(_listDirectoryState.expectedOffset);
1068 _listDirectoryWorker(false /* firstReqeust */);
1069 }
1070}
1071
1072void FTPManager::_fillMissingBlocksWorker(bool firstRequest)
1073{
1074 if (_downloadState.rgMissingData.count()) {
1075 MavlinkFTP::Request request{};
1076 MissingData_t& missingData = _downloadState.rgMissingData.first();
1077
1078 uint32_t cBytesToRead = qMin(static_cast<uint32_t>(_readChunkSize()), missingData.cBytesMissing);
1079
1080 qCDebug(FTPManagerLog) << "_fillMissingBlocksBegin: offset:cBytesToRead" << missingData.offset << cBytesToRead;
1081
1082 request.hdr.session = _downloadState.sessionId;
1083 request.hdr.opcode = MavlinkFTP::kCmdReadFile;
1084 request.hdr.offset = missingData.offset;
1085 request.hdr.size = cBytesToRead;
1086
1087 if (firstRequest) {
1088 _downloadState.retryCount = 0;
1089 } else {
1090 // Must used same sequence number as previous request
1091 _expectedIncomingSeqNumber -= 2;
1092 }
1093 _downloadState.expectedOffset = request.hdr.offset;
1094
1095 _sendRequestExpectAck(&request);
1096 } else {
1097 // We should have the full file now
1098 if (_downloadState.checksize == false || _downloadState.bytesWritten == _downloadState.fileSize) {
1099 _advanceStateMachine();
1100 } else {
1101 qCDebug(FTPManagerLog) << "_fillMissingBlocksWorker: no missing blocks but file still incomplete - bytesWritten:fileSize" << _downloadState.bytesWritten << _downloadState.fileSize;
1102 _downloadComplete(tr("Download failed"));
1103 }
1104 }
1105}
1106
1107void FTPManager::_fillMissingBlocksBegin(void)
1108{
1109 _fillMissingBlocksWorker(true /* firstRequest */);
1110}
1111
1112void FTPManager::_fillMissingBlocksAckOrNak(const MavlinkFTP::Request* ackOrNak)
1113{
1114 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
1115
1116 if (requestOpCode != MavlinkFTP::kCmdReadFile) {
1117 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak: Disregarding due to incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
1118 return;
1119 }
1120 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
1121 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak: Disregarding due to incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
1122 return;
1123 }
1124 if (ackOrNak->hdr.session != _downloadState.sessionId) {
1125 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak: Disregarding due to incorrect session id actual:expected" << ackOrNak->hdr.session << _downloadState.sessionId;
1126 return;
1127 }
1128
1129 _ackOrNakTimeoutTimer.stop();
1130
1131 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
1132 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak: Ack offset:size" << ackOrNak->hdr.offset << ackOrNak->hdr.size;
1133
1134 if (ackOrNak->hdr.offset != _downloadState.expectedOffset) {
1135 if (++_downloadState.retryCount > _maxRetry) {
1136 qCDebug(FTPManagerLog) << QString("_fillMissingBlocksAckOrNak: offset mismatch, retries exceeded");
1137 _downloadComplete(tr("Download failed"));
1138 return;
1139 }
1140
1141 // Ask for current offset again
1142 qCDebug(FTPManagerLog) << QString("_fillMissingBlocksAckOrNak: Ack offset mismatch retry, retryCount(%1) offset(%2)").arg(_downloadState.retryCount).arg(_downloadState.expectedOffset);
1143 _fillMissingBlocksWorker(false /* firstReqeust */);
1144 return;
1145 }
1146
1147 _downloadState.file.seek(ackOrNak->hdr.offset);
1148 int bytesWritten = _downloadState.file.write((const char*)ackOrNak->data, ackOrNak->hdr.size);
1149 if (bytesWritten != ackOrNak->hdr.size) {
1150 _downloadComplete(tr("Download failed: Error saving file"));
1151 return;
1152 }
1153 _downloadState.bytesWritten += ackOrNak->hdr.size;
1154
1155 MissingData_t& missingData = _downloadState.rgMissingData.first();
1156 missingData.offset += ackOrNak->hdr.size;
1157 missingData.cBytesMissing -= ackOrNak->hdr.size;
1158 if (missingData.cBytesMissing == 0) {
1159 // This block is finished, remove it
1160 _downloadState.rgMissingData.takeFirst();
1161 }
1162
1163 // Move on to fill in possible next hole
1164 _fillMissingBlocksWorker(true /* firstReqeust */);
1165
1166 // Emit progress last, as cancel could be called in there
1167 if (_downloadState.fileSize != 0) {
1168 emit commandProgress((float)(_downloadState.bytesWritten) / (float)_downloadState.fileSize);
1169 }
1170 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
1171 MavlinkFTP::ErrorCode_t errorCode = static_cast<MavlinkFTP::ErrorCode_t>(ackOrNak->data[0]);
1172
1173 if (errorCode == MavlinkFTP::kErrEOF) {
1174 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak EOF";
1175 if (_downloadState.checksize == false || _downloadState.bytesWritten == _downloadState.fileSize) {
1176 // We've successfully complete filling in all missing blocks
1177 _advanceStateMachine();
1178 return;
1179 }
1180 } else if (errorCode == MavlinkFTP::kErrInvalidSession) {
1181 _downloadSessionLost();
1182 return;
1183 }
1184
1185 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
1186 _downloadComplete(tr("Download failed"));
1187 }
1188}
1189
1190void FTPManager::_fillMissingBlocksTimeout(void)
1191{
1192 if (++_downloadState.retryCount > _maxRetry) {
1193 qCDebug(FTPManagerLog) << QString("_fillMissingBlocksTimeout retries exceeded");
1194 _downloadComplete(tr("Download failed"));
1195 } else {
1196 // Ask for current offset again
1197 qCDebug(FTPManagerLog) << QString("_fillMissingBlocksTimeout: retrying - retryCount(%1) offset(%2)").arg(_downloadState.retryCount).arg(_downloadState.expectedOffset);
1198 _fillMissingBlocksWorker(false /* firstReqeust */);
1199 }
1200}
1201
1202void FTPManager::_resetSessionsBegin(void)
1203{
1204 MavlinkFTP::Request request{};
1205 request.hdr.opcode = MavlinkFTP::kCmdResetSessions;
1206 request.hdr.size = 0;
1207 _sendRequestExpectAck(&request);
1208}
1209
1210void FTPManager::_resetSessionsAckOrNak(const MavlinkFTP::Request* ackOrNak)
1211{
1212 MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
1213
1214 if (requestOpCode != MavlinkFTP::kCmdResetSessions) {
1215 qCDebug(FTPManagerLog) << "_fillMissingBlocksAckOrNak: Disregarding due to incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
1216 return;
1217 }
1218 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
1219 qCDebug(FTPManagerLog) << "_resetSessionsAckOrNak: Disregarding due to incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
1220 return;
1221 }
1222
1223 _ackOrNakTimeoutTimer.stop();
1224
1225 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspAck) {
1226 qCDebug(FTPManagerLog) << "_resetSessionsAckOrNak: Ack";
1227 _advanceStateMachine();
1228 } else if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
1229 qCDebug(FTPManagerLog) << "_resetSessionsAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak);
1230 _downloadComplete(QString());
1231 }
1232}
1233
1234void FTPManager::_resetSessionsTimeout(void)
1235{
1236 qCDebug(FTPManagerLog) << "_resetSessionsTimeout";
1237 _downloadComplete(QString());
1238}
1239
1240void FTPManager::_setStateMachine(const StateFunctions_t* rgStates, size_t cStates)
1241{
1242 _rgStateMachine.append({ &FTPManager::_clearStaleSessionsBegin, &FTPManager::_clearStaleSessionsAckOrNak, &FTPManager::_clearStaleSessionsTimeout });
1243 for (size_t i = 0; i < cStates; i++) {
1244 _rgStateMachine.append(rgStates[i]);
1245 }
1246}
1247
1248void FTPManager::_clearStaleSessionsBegin(void)
1249{
1250 _clearStaleSessionsRetryCount = 0;
1251 _resetSessionsBegin();
1252}
1253
1254void FTPManager::_clearStaleSessionsAckOrNak(const MavlinkFTP::Request* ackOrNak)
1255{
1256 const MavlinkFTP::OpCode_t requestOpCode = static_cast<MavlinkFTP::OpCode_t>(ackOrNak->hdr.req_opcode);
1257 if (requestOpCode != MavlinkFTP::kCmdResetSessions) {
1258 qCDebug(FTPManagerLog) << "_clearStaleSessionsAckOrNak: Disregarding due to incorrect requestOpCode" << MavlinkFTP::opCodeToString(requestOpCode);
1259 return;
1260 }
1261 if (ackOrNak->hdr.seqNumber != _expectedIncomingSeqNumber) {
1262 qCDebug(FTPManagerLog) << "_clearStaleSessionsAckOrNak: Disregarding due to incorrect sequence actual:expected" << ackOrNak->hdr.seqNumber << _expectedIncomingSeqNumber;
1263 return;
1264 }
1265
1266 _ackOrNakTimeoutTimer.stop();
1267 if (ackOrNak->hdr.opcode == MavlinkFTP::kRspNak) {
1268 qCDebug(FTPManagerLog) << "_clearStaleSessionsAckOrNak: Nak -" << _errorMsgFromNak(ackOrNak) << "- continuing anyway";
1269 }
1270 _advanceStateMachine();
1271}
1272
1273void FTPManager::_clearStaleSessionsTimeout(void)
1274{
1275 if (++_clearStaleSessionsRetryCount <= 1) {
1276 qCDebug(FTPManagerLog) << "_clearStaleSessionsTimeout: retrying";
1277 _resetSessionsBegin();
1278 return;
1279 }
1280 qCDebug(FTPManagerLog) << "_clearStaleSessionsTimeout: no reply, continuing anyway";
1281 _advanceStateMachine();
1282}
1283
1284void FTPManager::_sendResetSessionsNoAck(void)
1285{
1286 SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock();
1287 if (!sharedLink) {
1288 return;
1289 }
1290
1291 MavlinkFTP::Request request{};
1292 request.hdr.opcode = MavlinkFTP::kCmdResetSessions;
1293 request.hdr.size = 0;
1294 request.hdr.seqNumber = _expectedIncomingSeqNumber + 1;
1295 _expectedIncomingSeqNumber += 2;
1296
1297 qCDebug(FTPManagerLog) << "_sendResetSessionsNoAck seqNumber:" << request.hdr.seqNumber;
1298
1299 mavlink_message_t message;
1300 mavlink_msg_file_transfer_protocol_pack_chan(MAVLinkProtocol::instance()->getSystemId(),
1302 sharedLink->mavlinkChannel(),
1303 &message,
1304 0,
1305 _vehicle->id(),
1306 _ftpCompId,
1307 (uint8_t*)&request);
1308 _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), message);
1309}
1310
1311void FTPManager::_sendRequestExpectAck(MavlinkFTP::Request* request)
1312{
1313 _ackOrNakTimeoutTimer.start();
1314
1315 SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock();
1316 if (sharedLink) {
1317 request->hdr.seqNumber = _expectedIncomingSeqNumber + 1; // Outgoing is 1 past last incoming
1318 _expectedIncomingSeqNumber += 2;
1319
1320 qCDebug(FTPManagerLog) << "_sendRequestExpectAck opcode:" << MavlinkFTP::opCodeToString(static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode)) << "seqNumber:" << request->hdr.seqNumber;
1321
1322 mavlink_message_t message;
1323 mavlink_msg_file_transfer_protocol_pack_chan(MAVLinkProtocol::instance()->getSystemId(),
1325 sharedLink->mavlinkChannel(),
1326 &message,
1327 0, // Target network, 0=broadcast?
1328 _vehicle->id(),
1329 _ftpCompId,
1330 (uint8_t*)request); // Payload
1331 _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), message);
1332 } else {
1333 qCDebug(FTPManagerLog) << "_sendRequestExpectAck No primary link. Allowing timeout to fail sequence.";
1334 }
1335}
1336
1337bool FTPManager::_parseURI(uint8_t fromCompId, const QString& uri, QString& parsedURI, uint8_t& compId)
1338{
1339 parsedURI = uri;
1340 compId = (fromCompId == MAV_COMP_ID_ALL) ? (uint8_t)MAV_COMP_ID_AUTOPILOT1 : fromCompId;
1341
1342 // Pull scheme off the front if there
1343 QString ftpPrefix(QStringLiteral("%1://").arg(mavlinkFTPScheme));
1344 if (parsedURI.startsWith(ftpPrefix, Qt::CaseInsensitive)) {
1345 parsedURI = parsedURI.mid(ftpPrefix.length());
1346 }
1347 if (parsedURI.contains("://")) {
1348 qCWarning(FTPManagerLog) << "Incorrect uri scheme or format" << uri;
1349 return false;
1350 }
1351
1352 // Pull component id off the front if there
1353 QRegularExpression regEx("^/??\\[\\;comp\\=(\\d+)\\]");
1354 QRegularExpressionMatch match = regEx.match(parsedURI);
1355 if (match.hasMatch()) {
1356 bool ok;
1357 compId = match.captured(1).toUInt(&ok);
1358 if (!ok) {
1359 qCWarning(FTPManagerLog) << "Incorrect format for component id" << uri;
1360 return false;
1361 }
1362
1363 qCDebug(FTPManagerLog) << "Found compId in MAVLink FTP URI: " << compId;
1364 parsedURI.replace(QRegularExpression("\\[\\;comp\\=\\d+\\]"), "");
1365 }
1366
1367 return true;
1368}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void cancelDownload()
bool listDirectory(uint8_t fromCompId, const QString &fromURI)
bool deleteFile(uint8_t fromCompId, const QString &fromURI)
static constexpr uint8_t kRadioReadChunkSize
Definition FTPManager.h:289
bool upload(uint8_t toCompId, const QString &toURI, const QString &fromFile)
Definition FTPManager.cc:89
static constexpr uint8_t kFullReadChunkSize
Bytes requested per ReadFile/BurstReadFile chunk on non-radio links: the full FTP payload.
Definition FTPManager.h:285
static constexpr const char * mavlinkFTPScheme
Definition FTPManager.h:77
void uploadComplete(const QString &file, const QString &errorMsg)
static constexpr int kMaxDownloadSessionReopens
Max times a download re-opens after the server expires the session (PX4 idle timer fires mid-burst)
Definition FTPManager.h:291
void cancelUpload()
void commandProgress(float value)
void downloadComplete(const QString &file, const QString &errorMsg)
void cancelListDirectory()
void deleteComplete(const QString &file, const QString &errorMsg)
void cancelDelete()
bool download(uint8_t fromCompId, const QString &fromURI, const QString &toDir, const QString &fileName="", bool checksize=true)
Definition FTPManager.cc:32
void listDirectoryComplete(const QStringList &dirList, const QString &errorMsg)
static int getComponentId()
static MAVLinkProtocol * instance()
int getSystemId() const
ErrorCode_t
Error codes returned in Nak response PayloadHeader.data[0].
Definition MAVLinkFTP.h:62
@ kErrFail
Unknown failure.
Definition MAVLinkFTP.h:64
@ kErrEOF
Offset past end of file for List and Read commands.
Definition MAVLinkFTP.h:69
@ kErrUnknownCommand
Unknown command opcode.
Definition MAVLinkFTP.h:70
@ kErrFailErrno
errno sent back in PayloadHeader.data[1]
Definition MAVLinkFTP.h:65
@ kErrInvalidSession
Session is not currently open.
Definition MAVLinkFTP.h:67
@ kErrFailFileNotFound
Definition MAVLinkFTP.h:73
static QString errorCodeToString(ErrorCode_t errorCode)
Definition MAVLinkFTP.cc:49
@ kCmdRemoveFile
Remove file at <path>
Definition MAVLinkFTP.h:47
@ kCmdWriteFile
Writes <size> bytes to <offset> in <session>
Definition MAVLinkFTP.h:46
@ kCmdCreateFile
Creates file at <path> for writing, returns <session>
Definition MAVLinkFTP.h:45
@ kCmdOpenFileRO
Opens file at <path> for reading, returns <session>
Definition MAVLinkFTP.h:43
@ kCmdListDirectoryWithTime
List directory as kCmdListDirectory, each entry additionally carrying trailing \t<modification time>
Definition MAVLinkFTP.h:55
@ kCmdReadFile
Reads <size> bytes from <offset> in <session>
Definition MAVLinkFTP.h:44
@ kCmdResetSessions
Terminates all open Read sessions.
Definition MAVLinkFTP.h:41
@ kRspAck
Ack response.
Definition MAVLinkFTP.h:57
@ kCmdBurstReadFile
Burst download session file.
Definition MAVLinkFTP.h:54
@ kCmdTerminateSession
Terminates open Read session.
Definition MAVLinkFTP.h:40
@ kRspNak
Nak response.
Definition MAVLinkFTP.h:58
@ kCmdListDirectory
List files in <path> from <offset>
Definition MAVLinkFTP.h:42
static QString opCodeToString(OpCode_t opCode)
Definition MAVLinkFTP.cc:3
WeakLinkInterfacePtr primaryLink() const
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:597
int id() const
Definition Vehicle.h:443
bool sendMessageOnLinkThreadSafe(LinkInterface *link, mavlink_message_t message)
Definition Vehicle.cc:1401
bool runningUnitTests()