QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MockLinkFTP.cc
Go to the documentation of this file.
1#include "MockLinkFTP.h"
2#include "MockLink.h"
4
5#include <QtCore/QDataStream>
6#include <QtCore/QDir>
7#include <QtCore/QTemporaryFile>
8#include <QtCore/QThread>
9
10QGC_LOGGING_CATEGORY(MockLinkFTPLog, "Comms.MockLink.MockLinkFTP")
11
12MockLinkFTP::MockLinkFTP(uint8_t systemIdServer, uint8_t componentIdServer, MockLink *mockLink)
13 : QObject(mockLink)
14 , _systemIdServer(systemIdServer)
15 , _componentIdServer(componentIdServer)
16 , _mockLink(mockLink)
17{
18 // qCDebug(MockLinkFTPLog) << Q_FUNC_INFO << this;
19}
20
22{
23 if (!_paramPckTempFile.isEmpty()) {
24 QFile::remove(_paramPckTempFile);
25 }
26 for (const QString &tempPath : std::as_const(_logFileTempPaths)) {
27 QFile::remove(tempPath);
28 }
29}
30
31void MockLinkFTP::ensureNullTemination(MavlinkFTP::Request *request)
32{
33 if (request->hdr.size < sizeof(request->data)) {
34 request->data[request->hdr.size] = '\0';
35 } else {
36 request->data[sizeof(request->data) - 1] = '\0';
37 }
38}
39
40void MockLinkFTP::_listCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber, bool withTime)
41{
42 MavlinkFTP::Request ackResponse{};
43 ensureNullTemination(request);
44
45 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
47
48 if (withTime && !_listDirectoryWithTimeSupported) {
49 // Simulate a server which doesn't implement the command. The client should fall back to kCmdListDirectory.
50 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrUnknownCommand, outgoingSeqNumber, listOpCode);
51 return;
52 }
53
54 // We support the root path and the @MAV_LOG virtual log directory
55 const QString path = reinterpret_cast<char*>(&request->data[0]);
56 QStringList entries;
57 if (path.isEmpty() || path == QStringLiteral("/")) {
58 // Default mock listing: 6 fixed entries
59 for (uint32_t index = 0; index < 6; index++) {
60 QString entry = QStringLiteral("Ffile%1.txt\t%2").arg(index).arg(1024 + index);
61 if (withTime) {
62 entry += QStringLiteral("\t%1").arg(kMockModificationTime + index);
63 }
64 entries.append(entry);
65 }
66 } else if ((path == QStringLiteral("@MAV_LOG")) || (path == QStringLiteral("@MAV_LOG/"))) {
67 for (const LogFile &logFile : std::as_const(_logFiles)) {
68 QString entry = QStringLiteral("F%1\t%2").arg(logFile.name).arg(logFile.size);
69 if (withTime) {
70 entry += QStringLiteral("\t%1").arg(logFile.mtime);
71 }
72 entries.append(entry);
73 }
74 } else {
75 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
76 return;
77 }
78
79 if (request->hdr.offset > 0) {
80 if (_errMode == errModeNakSecondResponse) {
81 // Nak error all subsequent requests
82 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
83 return;
84 }
85
86 if (_errMode == errModeNoSecondResponse) {
87 // No response for all subsequent requests
88 return;
89 }
90
91 if (_errMode == errModeNoSecondResponseAllowRetry) {
92 // No response to this request, subsequent requests will succeed
93 _errMode = errModeNone;
94 return;
95 }
96 }
97
98 ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
99 ackResponse.hdr.req_opcode = listOpCode;
100 ackResponse.hdr.session = 0;
101 ackResponse.hdr.offset = request->hdr.offset;
102 ackResponse.hdr.size = 0;
103
104 // Entry format is "F<name>\t<size>", plus a trailing "\t<modification time>" for kCmdListDirectoryWithTime.
105 // MockLink sends up to two directory entries per packet.
106 if (request->hdr.offset < static_cast<uint32_t>(entries.count())) {
107 char *bufPtr = reinterpret_cast<char*>(&ackResponse.data[0]);
108 for (uint32_t index = request->hdr.offset; (index < request->hdr.offset + 2) && (index < static_cast<uint32_t>(entries.count())); index++) {
109 const QByteArray dirEntry = entries[static_cast<qsizetype>(index)].toUtf8();
110 if (ackResponse.hdr.size + dirEntry.length() + 1 > static_cast<int>(sizeof(ackResponse.data))) {
111 break;
112 }
113 (void) strncpy(bufPtr, dirEntry.constData(), dirEntry.length());
114 ackResponse.hdr.size += dirEntry.length() + 1;
115 bufPtr += dirEntry.length() + 1;
116 }
117 } else {
118 ackResponse.hdr.opcode = MavlinkFTP::kRspNak;
119 ackResponse.data[0] = MavlinkFTP::kErrEOF;
120 ackResponse.hdr.size = 1;
121 }
122
123 _sendResponse(senderSystemId, senderComponentId, &ackResponse, outgoingSeqNumber);
124}
125
126void MockLinkFTP::_openCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
127{
128 MavlinkFTP::Request response{};
129 ensureNullTemination(request);
130 const QString path = reinterpret_cast<char*>(request->data);
131
132 _uploadSession.reset();
133
134 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
135
136 const size_t cchPath = strnlen(reinterpret_cast<char*>(request->data), sizeof(request->data));
137 Q_ASSERT(cchPath != sizeof(request->data));
138 Q_UNUSED(cchPath); // Fix initialized-but-not-referenced warning on release builds
139
140 _currentFile.close();
141
142 QString tmpFilename;
143 const QString sizePrefix = sizeFilenamePrefix;
144 if (path.startsWith(sizePrefix)) {
145 const QString sizeString = path.right(path.length() - sizePrefix.length());
146 tmpFilename = _createTestTempFile(sizeString.toInt());
147 } else if (path == "/general.json") {
148 tmpFilename = QStringLiteral(":MockLink/General.MetaData.json");
149 } else if (path == "/general.json.xz") {
150 tmpFilename = QStringLiteral(":MockLink/General.MetaData.json.xz");
151 } else if (path == "/parameter.json") {
152 tmpFilename = QStringLiteral(":MockLink/Parameter.MetaData.json");
153 } else if (path == "/parameter.json.xz") {
154 tmpFilename = QStringLiteral(":MockLink/Parameter.MetaData.json.xz");
155 } else if (path == "@PARAM/param.pck" || path.startsWith("@PARAM/param.pck?")) {
156 const bool withDefaults = path.contains(QStringLiteral("withdefaults=1"));
157 tmpFilename = _generateParamPck(withDefaults);
158 } else if (path.startsWith(QStringLiteral("@MAV_LOG/"))) {
159 tmpFilename = _logFileTempPath(path.mid(QStringLiteral("@MAV_LOG/").length()));
160 }
161
162 if (!tmpFilename.isEmpty()) {
163 _currentFile.setFileName(tmpFilename);
164 if (!_currentFile.open(QIODevice::ReadOnly)) {
165 _sendNakErrno(senderSystemId, senderComponentId, _currentFile.error(), outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
166 return;
167 }
168 } else {
169 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFailFileNotFound, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
170 return;
171 }
172
173 response.hdr.opcode = MavlinkFTP::kRspAck;
174 response.hdr.req_opcode = MavlinkFTP::kCmdOpenFileRO;
175 response.hdr.session = _sessionId;
176
177 // Data contains file length
178 response.hdr.size = sizeof(uint32_t);
179
180 // Ardupilot sends constant wrong file size for parameter file due to dynamic on the fly generation
181 response.openFileLength = ((path == "@PARAM/param.pck" || path.startsWith("@PARAM/param.pck?")) ? qPow(1024, 2) : _currentFile.size());
182
183 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
184}
185
186void MockLinkFTP::_createFileCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
187{
188 ensureNullTemination(request);
189
190 const QString path = reinterpret_cast<char*>(request->data);
191 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
192
193 if (path.isEmpty()) {
194 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdCreateFile);
195 return;
196 }
197
198 _uploadSession.reset();
199 _uploadSession.active = true;
200 _uploadSession.remotePath = path;
201
202 MavlinkFTP::Request response{};
203 response.hdr.opcode = MavlinkFTP::kRspAck;
204 response.hdr.req_opcode = MavlinkFTP::kCmdCreateFile;
205 response.hdr.session = _sessionId;
206 response.hdr.size = 0;
207
208 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
209}
210
211void MockLinkFTP::_openFileWOCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
212{
213 ensureNullTemination(request);
214
215 const QString path = reinterpret_cast<char*>(request->data);
216 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
217
218 if (path.isEmpty()) {
219 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileWO);
220 return;
221 }
222
223 _uploadSession.reset();
224 _uploadSession.active = true;
225 _uploadSession.remotePath = path;
226
227 MavlinkFTP::Request response{};
228 response.hdr.opcode = MavlinkFTP::kRspAck;
229 response.hdr.req_opcode = MavlinkFTP::kCmdOpenFileWO;
230 response.hdr.session = _sessionId;
231 response.hdr.size = 0;
232
233 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
234}
235
236void MockLinkFTP::_readCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
237{
238 MavlinkFTP::Request response{};
239 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
240
241 if (request->hdr.session != _sessionId) {
242 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
243 return;
244 }
245
246 const uint32_t readOffset = request->hdr.offset; // offset into file for reading
247 if (readOffset != 0) {
248 // If we get here it means the client is requesting additional data past the first request
249 if (_errMode == errModeNakSecondResponse) {
250 // Nak error all subsequent requests
251 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
252 return;
253 }
254
255 if (_errMode == errModeNoSecondResponse) {
256 // No rsponse for all subsequent requests
257 return;
258 }
259 }
260
261 if (readOffset >= _currentFile.size()) {
262 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
263 return;
264 }
265
266 const uint8_t cBytesToRead = static_cast<uint8_t>(qMin(static_cast<qint64>(sizeof(response.data)), _currentFile.size() - readOffset));
267 (void) _currentFile.seek(readOffset);
268 const QByteArray bytes = _currentFile.read(cBytesToRead);
269 (void) memcpy(response.data, bytes.constData(), cBytesToRead);
270
271 // We should always have written something, otherwise there is something wrong with the code above
272 Q_ASSERT(cBytesToRead);
273
274 response.hdr.session = _sessionId;
275 response.hdr.size = cBytesToRead;
276 response.hdr.offset = request->hdr.offset;
277 response.hdr.opcode = MavlinkFTP::kRspAck;
278 response.hdr.req_opcode = MavlinkFTP::kCmdReadFile;
279
280 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
281}
282
283void MockLinkFTP::_removeFileCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
284{
285 ensureNullTemination(request);
286 const QString path = reinterpret_cast<char*>(request->data);
287 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
288
289 const QString logPrefix = QStringLiteral("@MAV_LOG/");
290 if (path.startsWith(logPrefix)) {
291 const QString name = path.mid(logPrefix.length());
292 for (int i = 0; i < _logFiles.count(); i++) {
293 if (_logFiles[i].name == name) {
294 _logFiles.removeAt(i);
295
296 // Drop the cached temp file so the deleted log is no longer downloadable
297 const QString tempPath = _logFileTempPaths.take(name);
298 if (!tempPath.isEmpty()) {
299 QFile::remove(tempPath);
300 }
301
302 _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdRemoveFile);
303 return;
304 }
305 }
306 }
307
308 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFailFileNotFound, outgoingSeqNumber, MavlinkFTP::kCmdRemoveFile);
309}
310
311void MockLinkFTP::_burstReadCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
312{
313 if (_burstReadDelayMs > 0) {
314 QThread::msleep(_burstReadDelayMs);
315 }
316
317 MavlinkFTP::Request response{};
318 uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
319
320 if (request->hdr.session != _sessionId) {
321 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
322 return;
323 }
324
325 constexpr int burstMax = 10;
326 int burstCount = 1;
327 uint32_t burstOffset = request->hdr.offset;
328
329 while ((burstOffset < _currentFile.size()) && (burstCount++ < burstMax)) {
330 _currentFile.seek(burstOffset);
331
332 const uint8_t cBytes = static_cast<uint8_t>(qMin(static_cast<qint64>(sizeof(response.data)), _currentFile.size() - burstOffset));
333 const QByteArray bytes = _currentFile.read(cBytes);
334 Q_ASSERT(cBytes); // We should always have written something, otherwise there is something wrong with the code above
335
336 (void) memcpy(response.data, bytes.constData(), cBytes);
337
338 response.hdr.session = _sessionId;
339 response.hdr.size = cBytes;
340 response.hdr.offset = burstOffset;
341 response.hdr.opcode = MavlinkFTP::kRspAck;
342 response.hdr.req_opcode = MavlinkFTP::kCmdBurstReadFile;
343 response.hdr.burstComplete = (burstCount == burstMax) ? 1 : 0;
344
345 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
346
347 outgoingSeqNumber = _nextSeqNumber(outgoingSeqNumber);
348 burstOffset += cBytes;
349 }
350
351 if (burstOffset >= _currentFile.size()) {
352 // Burst is fully complete
353 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
354 }
355}
356
357void MockLinkFTP::_terminateCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
358{
359 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
360
361 if (request->hdr.session != _sessionId) {
362 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdTerminateSession);
363 return;
364 }
365
366 _currentFile.close();
367 _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdTerminateSession);
368
369 _finalizeActiveUpload();
370
372}
373
374void MockLinkFTP::_resetCommand(uint8_t senderSystemId, uint8_t senderComponentId, uint16_t seqNumber)
375{
376 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
377
378 _currentFile.close();
379 _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdResetSessions);
380
381 _finalizeActiveUpload();
382
384}
385
386void MockLinkFTP::_writeCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
387{
388 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
389
390 if ((request->hdr.session != _sessionId) || !_uploadSession.active) {
391 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
392 return;
393 }
394
395 if (request->hdr.offset > static_cast<uint32_t>(_uploadSession.buffer.size())) {
396 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
397 return;
398 }
399
400 if (request->hdr.offset != 0) {
401 if (_errMode == errModeNakSecondResponse) {
402 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
403 return;
404 }
405
406 if (_errMode == errModeNoSecondResponse) {
407 return;
408 }
409
410 if (_errMode == errModeNoSecondResponseAllowRetry) {
411 _errMode = errModeNone;
412 return;
413 }
414 }
415
416 const uint32_t bytesToWrite = request->hdr.size;
417 if (bytesToWrite > sizeof(request->data)) {
418 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
419 return;
420 }
421
422 const uint32_t requiredSize = request->hdr.offset + bytesToWrite;
423 if (requiredSize > static_cast<uint32_t>(_uploadSession.buffer.size())) {
424 _uploadSession.buffer.resize(requiredSize);
425 }
426
427 if (bytesToWrite > 0) {
428 (void) memcpy(_uploadSession.buffer.data() + request->hdr.offset, request->data, bytesToWrite);
429 }
430
431 MavlinkFTP::Request response{};
432 response.hdr.opcode = MavlinkFTP::kRspAck;
433 response.hdr.req_opcode = MavlinkFTP::kCmdWriteFile;
434 response.hdr.session = _sessionId;
435 response.hdr.size = 0;
436 response.hdr.offset = request->hdr.offset;
437
438 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
439}
440
441void MockLinkFTP::_finalizeActiveUpload()
442{
443 if (!_uploadSession.active) {
444 return;
445 }
446
447 if (!_uploadSession.remotePath.isEmpty()) {
448 _uploadedFiles.insert(_uploadSession.remotePath, _uploadSession.buffer);
449 }
450
451 _uploadSession.reset();
452}
453
455{
456 if (message.msgid != MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL) {
457 return;
458 }
459
460 mavlink_file_transfer_protocol_t requestFTP{};
461 mavlink_msg_file_transfer_protocol_decode(&message, &requestFTP);
462
463 if (requestFTP.target_system != _systemIdServer) {
464 return;
465 }
466
467 MavlinkFTP::Request *request = reinterpret_cast<MavlinkFTP::Request*>(&requestFTP.payload[0]);
468
469 // kCmdOpenFileRO and kCmdResetSessions don't support retry so we can't drop those
470 if (_randomDropsEnabled && (request->hdr.opcode != MavlinkFTP::kCmdOpenFileRO) && (request->hdr.opcode != MavlinkFTP::kCmdResetSessions)) {
471 if ((rand() % 5) == 0) {
472 qCDebug(MockLinkFTPLog) << "MockLinkFTP: Random drop of incoming packet";
473 return;
474 }
475 }
476
477 if (_lastReplyValid && (request->hdr.seqNumber == (_lastReplySequence - 1))) {
478 // This is the same request as the one we replied to last. It means the (n)ack got lost, and the GCS
479 // resent the request
480 qCDebug(MockLinkFTPLog) << "MockLinkFTP: resending response";
481 _mockLink->respondWithMavlinkMessage(_lastReply);
482 return;
483 }
484
485 const uint16_t incomingSeqNumber = request->hdr.seqNumber;
486 const uint16_t outgoingSeqNumber = _nextSeqNumber(incomingSeqNumber);
487
488 if ((request->hdr.opcode != MavlinkFTP::kCmdResetSessions) && (request->hdr.opcode != MavlinkFTP::kCmdTerminateSession)) {
489 if (_errMode == errModeNoResponse) {
490 // Don't respond to any requests, this shold cause the client to eventually timeout waiting for the ack
491 return;
492 }
493
494 if (_errMode == errModeNakResponse) {
495 // Nak all requests, the actual error send back doesn't really matter as long as it's an error
496 _sendNak(message.sysid, message.compid, MavlinkFTP::kErrFail, outgoingSeqNumber, static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode));
497 return;
498 }
499 }
500
501 MavlinkFTP::Request ackResponse{};
502 switch (request->hdr.opcode) {
504 // ignored, always acked
505 ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
506 ackResponse.hdr.session = 0;
507 ackResponse.hdr.size = 0;
508 _sendResponse(message.sysid, message.compid, &ackResponse, outgoingSeqNumber);
509 break;
511 _listCommand(message.sysid, message.compid, request, incomingSeqNumber, false /* withTime */);
512 break;
514 _listCommand(message.sysid, message.compid, request, incomingSeqNumber, true /* withTime */);
515 break;
517 _openCommand(message.sysid, message.compid, request, incomingSeqNumber);
518 break;
520 _createFileCommand(message.sysid, message.compid, request, incomingSeqNumber);
521 break;
523 _openFileWOCommand(message.sysid, message.compid, request, incomingSeqNumber);
524 break;
526 _readCommand(message.sysid, message.compid, request, incomingSeqNumber);
527 break;
529 _burstReadCommand(message.sysid, message.compid, request, incomingSeqNumber);
530 break;
532 _removeFileCommand(message.sysid, message.compid, request, incomingSeqNumber);
533 break;
535 _writeCommand(message.sysid, message.compid, request, incomingSeqNumber);
536 break;
538 _terminateCommand(message.sysid, message.compid, request, incomingSeqNumber);
539 break;
541 _resetCommand(message.sysid, message.compid, incomingSeqNumber);
542 break;
543 default:
544 // nack for all NYI opcodes
545 _sendNak(message.sysid, message.compid, MavlinkFTP::kErrUnknownCommand, outgoingSeqNumber, static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode));
546 break;
547 }
548}
549
550void MockLinkFTP::_sendAck(uint8_t targetSystemId, uint8_t targetComponentId, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
551{
552 MavlinkFTP::Request ackResponse{};
553
554 ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
555 ackResponse.hdr.req_opcode = reqOpcode;
556 ackResponse.hdr.session = _sessionId;
557 ackResponse.hdr.size = 0;
558
559 _sendResponse(targetSystemId, targetComponentId, &ackResponse, seqNumber);
560}
561
562void MockLinkFTP::_sendNak(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::ErrorCode_t error, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
563{
564 MavlinkFTP::Request nakResponse{};
565
566 nakResponse.hdr.opcode = MavlinkFTP::kRspNak;
567 nakResponse.hdr.req_opcode = reqOpcode;
568 nakResponse.hdr.session = _sessionId;
569 nakResponse.hdr.size = 1;
570 nakResponse.data[0] = error;
571
572 _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
573}
574
575void MockLinkFTP::_sendNakErrno(uint8_t targetSystemId, uint8_t targetComponentId, uint8_t nakErrno, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
576{
577 MavlinkFTP::Request nakResponse{};
578
579 nakResponse.hdr.opcode = MavlinkFTP::kRspNak;
580 nakResponse.hdr.req_opcode = reqOpcode;
581 nakResponse.hdr.session = _sessionId;
582 nakResponse.hdr.size = 2;
583 nakResponse.data[0] = MavlinkFTP::kErrFailErrno;
584 nakResponse.data[1] = nakErrno;
585
586 _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
587}
588
589
590void MockLinkFTP::_sendResponse(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
591{
592 request->hdr.seqNumber = seqNumber;
593 _lastReplySequence = seqNumber;
594 _lastReplyValid = true;
595
596 (void) mavlink_msg_file_transfer_protocol_pack_chan(
597 _systemIdServer, // System ID
598 _componentIdServer, // Component ID
599 _mockLink->outgoingMavlinkChannel(),
600 &_lastReply, // Mavlink Message to pack into
601 0, // Target network
602 targetSystemId,
603 targetComponentId,
604 reinterpret_cast<uint8_t*>(request) // Payload
605 );
606
607 // kCmdOpenFileRO and kCmdResetSessions don't support retry so we can't drop those
608 if (_randomDropsEnabled && (request->hdr.req_opcode != MavlinkFTP::kCmdOpenFileRO) && (request->hdr.req_opcode != MavlinkFTP::kCmdResetSessions)) {
609 if ((rand() % 5) == 0) {
610 qCDebug(MockLinkFTPLog) << "MockLinkFTP: Random drop of outgoing packet";
611 return;
612 }
613 }
614
615 _mockLink->respondWithMavlinkMessage(_lastReply);
616}
617
618
619uint16_t MockLinkFTP::_nextSeqNumber(uint16_t seqNumber) const
620{
621 uint16_t outgoingSeqNumber = seqNumber + 1;
622
623 if (_errMode == errModeBadSequence) {
624 outgoingSeqNumber++;
625 }
626
627 return outgoingSeqNumber;
628}
629
630QString MockLinkFTP::_createTestTempFile(int size)
631{
632 QTemporaryFile tmpFile(QDir::tempPath() + QStringLiteral("/MockLinkFTPTestCaseXXXXXX"));
633 tmpFile.setAutoRemove(false);
634
635 if (tmpFile.open()) {
636 for (int i = 0; i < size; i++) {
637 (void) tmpFile.write(QByteArray(1, static_cast<char>(i % 255)));
638 }
639 tmpFile.close();
640 }
641
642 return tmpFile.fileName();
643}
644
645QByteArray MockLinkFTP::logFileContents(const QString &name) const
646{
647 for (const LogFile &logFile : _logFiles) {
648 if (logFile.name == name) {
649 return _generateLogFileContents(name, logFile.size);
650 }
651 }
652 return QByteArray();
653}
654
655QByteArray MockLinkFTP::_generateLogFileContents(const QString &name, int size)
656{
657 const uint seed = qHash(name);
658 QByteArray contents(size, Qt::Uninitialized);
659 for (int i = 0; i < size; i++) {
660 contents[i] = static_cast<char>((seed + static_cast<uint>(i)) & 0xFF);
661 }
662 return contents;
663}
664
665void MockLinkFTP::setLogFiles(const QList<LogFile> &logFiles)
666{
667 // Invalidate cached temp files so a reused name is regenerated with the new contents
668 for (const QString &tempPath : std::as_const(_logFileTempPaths)) {
669 QFile::remove(tempPath);
670 }
671 _logFileTempPaths.clear();
672
673 _logFiles = logFiles;
674}
675
676QString MockLinkFTP::_logFileTempPath(const QString &name)
677{
678 if (_logFileTempPaths.contains(name)) {
679 return _logFileTempPaths.value(name);
680 }
681
682 for (const LogFile &logFile : std::as_const(_logFiles)) {
683 if (logFile.name != name) {
684 continue;
685 }
686
687 QTemporaryFile tmpFile(QDir::tempPath() + QStringLiteral("/MockLinkFTPLogXXXXXX"));
688 tmpFile.setAutoRemove(false);
689 if (!tmpFile.open()) {
690 return QString();
691 }
692 (void) tmpFile.write(_generateLogFileContents(name, logFile.size));
693 tmpFile.close();
694
695 _logFileTempPaths.insert(name, tmpFile.fileName());
696 return tmpFile.fileName();
697 }
698
699 return QString();
700}
701
702QString MockLinkFTP::_generateParamPck(bool withDefaults)
703{
704 // AP_PARAM types used in the binary param.pck format
705 enum APParamType : quint8 {
706 AP_PARAM_INT8 = 1,
707 AP_PARAM_INT16 = 2,
708 AP_PARAM_INT32 = 3,
709 AP_PARAM_FLOAT = 4,
710 };
711
712 // ArduPilot binary param.pck format magic numbers.
713 // See AP_Filesystem_Param.cpp in ArduPilot source (AP_PARAM_HEADER_MAGIC / AP_PARAM_HEADER_MAGIC_DEFAULT).
714 constexpr quint16 magicStandard = 0x671B; // param.pck without defaults
715 constexpr quint16 magicWithDefaults = 0x671C; // param.pck?withdefaults=1
716 constexpr int readSize = 239; // Max data bytes per MAVLink FTP burst: MAVLink payload (251 bytes) minus FTP header (12 bytes) = 239.
717 // This matches the MAVLink FTP spec / PX4-ArduPilot implementations and is critical to correct chunking and padding.
718
719 // Get params for component 1 (MAV_COMP_ID_AUTOPILOT1)
720 constexpr int compId = 1;
721 const auto &valueMap = _mockLink->_mapParamName2Value;
722 const auto &typeMap = _mockLink->_mapParamName2MavParamType;
723 if (!valueMap.contains(compId) || !typeMap.contains(compId)) {
724 qCWarning(MockLinkFTPLog) << "_generateParamPck: no parameters for component" << compId;
725 return QString();
726 }
727
728 const auto &values = valueMap[compId];
729 const auto &types = typeMap[compId];
730
731 // Sort parameter names alphabetically (matching ArduPilot behavior)
732 QStringList paramNames = values.keys();
733 paramNames.sort();
734
735 const quint16 numParams = static_cast<quint16>(paramNames.size());
736
737 // Map MAV_PARAM_TYPE to AP_PARAM type and value size
738 auto mapType = [](MAV_PARAM_TYPE mavType, quint8 &apType, int &valueSize) {
739 switch (mavType) {
740 case MAV_PARAM_TYPE_UINT8:
741 case MAV_PARAM_TYPE_INT8:
742 apType = AP_PARAM_INT8;
743 valueSize = 1;
744 break;
745 case MAV_PARAM_TYPE_UINT16:
746 case MAV_PARAM_TYPE_INT16:
747 apType = AP_PARAM_INT16;
748 valueSize = 2;
749 break;
750 case MAV_PARAM_TYPE_UINT32:
751 case MAV_PARAM_TYPE_INT32:
752 apType = AP_PARAM_INT32;
753 valueSize = 4;
754 break;
755 case MAV_PARAM_TYPE_REAL32:
756 apType = AP_PARAM_FLOAT;
757 valueSize = 4;
758 break;
759 default:
760 apType = AP_PARAM_FLOAT;
761 valueSize = 4;
762 break;
763 }
764 };
765
766 QByteArray data;
767 QDataStream stream(&data, QIODevice::WriteOnly);
768 stream.setByteOrder(QDataStream::LittleEndian);
769
770 // Write header
771 const quint16 magic = withDefaults ? magicWithDefaults : magicStandard;
772 stream << magic << numParams << numParams;
773
774 QByteArray previousName;
775 constexpr int headerSize = 6; // 2 bytes magic + 2 bytes numParams + 2 bytes totalParams
776
777 for (const QString &name : paramNames) {
778 const QVariant &value = values[name];
779 const MAV_PARAM_TYPE mavType = types[name];
780 const QByteArray nameBytes = name.toLatin1();
781
782 quint8 apType = 0;
783 int valueSize = 0;
784 mapType(mavType, apType, valueSize);
785
786 // Compute common prefix with previous parameter name
787 const int previousNameLen = previousName.size();
788 const int currentNameLen = nameBytes.size();
789 int commonLen = 0;
790 // common_len is limited to 15 because common_len + name_len <= 16 and name_len must be >= 1
791 const int maxCommon = std::min({previousNameLen, currentNameLen, 15});
792 while (commonLen < maxCommon && nameBytes[commonLen] == previousName[commonLen]) {
793 commonLen++;
794 }
795
796 int nameLen = currentNameLen - commonLen;
797 // name_len is encoded as (name_len - 1) in 4 bits, so it must be >= 1.
798 // If the entire name matched the common prefix, steal one char back.
799 if (nameLen == 0 && commonLen > 0) {
800 nameLen = 1;
801 commonLen--;
802 }
803 // Ensure name_len + common_len <= 16
804 if (nameLen + commonLen > 16) {
805 commonLen = 16 - nameLen;
806 }
807 // name_len must fit in 4 bits as (name_len - 1)
808 if (nameLen > 16) {
809 nameLen = 16;
810 commonLen = 0;
811 }
812
813 // For ArduPilot, calibration-indicator parameters have a firmware default of 0
814 // (uncalibrated). All other parameters use their current value as the default,
815 // since MockLink params are loaded from a snapshot and we have no separate
816 // defaults file.
817 const bool useZeroDefault = withDefaults
818 && (_mockLink->getFirmwareType() == MAV_AUTOPILOT_ARDUPILOTMEGA)
819 && MockLink::kAPMCalOffsetParams.contains(name);
820 const bool addDefault = withDefaults;
821 const quint8 flags = addDefault ? 0x01 : 0x00;
822 const int packedLen = 2 + nameLen + valueSize + (addDefault ? valueSize : 0);
823
824 // FTP reads data in fixed-size blocks (readSize = 239 bytes, the MAVLink FTP
825 // payload). If a multi-byte value straddles a block boundary, a retransmitted
826 // block could overwrite part of the value with stale data, corrupting it.
827 // To prevent this, ArduPilot inserts zero-byte padding before the entry so
828 // the value bytes land entirely within one block. The parser skips leading
829 // zeros between entries. See AP_Filesystem_Param::pack_param.
830 if (valueSize > 1) {
831 const quint32 dataOfs = static_cast<quint32>(data.size()) - headerSize;
832 const quint32 endOfs = dataOfs + static_cast<quint32>(packedLen);
833 const quint32 endMod = endOfs % readSize;
834 if (endMod > 0 && endMod < static_cast<quint32>(valueSize)) {
835 const int pad = valueSize - static_cast<int>(endMod);
836 for (int i = 0; i < pad; i++) {
837 stream << static_cast<quint8>(0);
838 }
839 }
840 }
841
842 // Type + flags byte
843 stream << static_cast<quint8>(apType | (flags << 4));
844 // Name encoding byte: upper 4 bits = (name_len - 1), lower 4 bits = common_len
845 stream << static_cast<quint8>(((nameLen - 1) << 4) | commonLen);
846 // Write the unique suffix of the name
847 stream.writeRawData(nameBytes.constData() + commonLen, nameLen);
848
849 // Write parameter value
850 switch (apType) {
851 case AP_PARAM_INT8: {
852 const qint8 v = static_cast<qint8>(value.toInt());
853 stream << v;
854 if (addDefault) stream << (useZeroDefault ? qint8(0) : v);
855 break;
856 }
857 case AP_PARAM_INT16: {
858 const qint16 v = static_cast<qint16>(value.toInt());
859 stream << v;
860 if (addDefault) stream << (useZeroDefault ? qint16(0) : v);
861 break;
862 }
863 case AP_PARAM_INT32: {
864 const qint32 v = value.toInt();
865 stream << v;
866 if (addDefault) stream << (useZeroDefault ? qint32(0) : v);
867 break;
868 }
869 case AP_PARAM_FLOAT: {
870 const float f = value.toFloat();
871 qint32 raw;
872 memcpy(&raw, &f, sizeof(raw));
873 stream << raw;
874 if (addDefault) {
875 if (useZeroDefault) {
876 stream << qint32(0);
877 } else {
878 stream << raw;
879 }
880 }
881 break;
882 }
883 }
884
885 previousName = nameBytes;
886 }
887
888 // Clean up previous temp file
889 if (!_paramPckTempFile.isEmpty()) {
890 QFile::remove(_paramPckTempFile);
891 _paramPckTempFile.clear();
892 }
893
894 // Write to temp file
895 QTemporaryFile tmpFile(QDir::temp().filePath(QStringLiteral("MockLinkParamPckXXXXXX")));
896 tmpFile.setAutoRemove(false);
897
898 if (!tmpFile.open()) {
899 qCWarning(MockLinkFTPLog) << "_generateParamPck: failed to create temp file";
900 return QString();
901 }
902
903 tmpFile.write(data);
904 tmpFile.close();
905 _paramPckTempFile = tmpFile.fileName();
906
907 qCDebug(MockLinkFTPLog) << "_generateParamPck: generated" << numParams << "params,"
908 << data.size() << "bytes, withDefaults:" << withDefaults
909 << "file:" << tmpFile.fileName();
910
911 return tmpFile.fileName();
912}
uint qHash(const FirmwareUpgradeController::FirmwareIdentifier &firmwareId)
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
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
@ 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
@ kCmdOpenFileWO
Opens file at <path> for writing, returns <session>
Definition MAVLinkFTP.h:50
@ kCmdResetSessions
Terminates all open Read sessions.
Definition MAVLinkFTP.h:41
@ kCmdNone
ignored, always acked
Definition MAVLinkFTP.h:39
@ 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
Mock implementation of Mavlink FTP server.
Definition MockLinkFTP.h:16
void resetCommandReceived()
You can connect to this signal to be notified when the server receives a Reset command.
static constexpr uint32_t kMockModificationTime
Definition MockLinkFTP.h:99
void terminateCommandReceived()
You can connect to this signal to be notified when the server receives a Terminate command.
QList< LogFile > logFiles() const
Returns the log files served from the @MAV_LOG virtual directory.
Definition MockLinkFTP.h:39
void setLogFiles(const QList< LogFile > &logFiles)
static constexpr const char * sizeFilenamePrefix
Definition MockLinkFTP.h:95
void mavlinkMessageReceived(const mavlink_message_t &message)
Called to handle an FTP message.
QByteArray logFileContents(const QString &name) const
@ errModeBadSequence
Return response with bad sequence number.
Definition MockLinkFTP.h:71
@ errModeNoSecondResponseAllowRetry
No response to subsequent request to initial command, error will be cleared after this so retry will ...
Definition MockLinkFTP.h:69
@ errModeNakSecondResponse
Nak subsequent request to initial command.
Definition MockLinkFTP.h:70
@ errModeNoResponse
No response to any request, client should eventually time out with no Ack.
Definition MockLinkFTP.h:66
@ errModeNone
No error, respond correctly.
Definition MockLinkFTP.h:65
@ errModeNoSecondResponse
No response to subsequent request to initial command.
Definition MockLinkFTP.h:68
@ errModeNakResponse
Nak all requests.
Definition MockLinkFTP.h:67
Describes a simulated onboard log file served from the @MAV_LOG virtual directory.
Definition MockLinkFTP.h:28