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