5#include <QtCore/QDataStream>
7#include <QtCore/QTemporaryFile>
13 , _systemIdServer(systemIdServer)
14 , _componentIdServer(componentIdServer)
22 if (!_paramPckTempFile.isEmpty()) {
23 QFile::remove(_paramPckTempFile);
27void MockLinkFTP::ensureNullTemination(MavlinkFTP::Request *request)
29 if (request->hdr.size <
sizeof(request->data)) {
30 request->data[request->hdr.size] =
'\0';
32 request->data[
sizeof(request->data) - 1] =
'\0';
36void MockLinkFTP::_listCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber,
bool withTime)
38 MavlinkFTP::Request ackResponse{};
39 ensureNullTemination(request);
41 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
44 if (withTime && !_listDirectoryWithTimeSupported) {
51 const QString path =
reinterpret_cast<char*
>(&request->data[0]);
52 if (!path.isEmpty() && path !=
"/") {
53 _sendNak(senderSystemId, senderComponentId,
MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
57 if (request->hdr.offset > 0) {
60 _sendNak(senderSystemId, senderComponentId,
MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
77 ackResponse.hdr.req_opcode = listOpCode;
78 ackResponse.hdr.session = 0;
79 ackResponse.hdr.offset = request->hdr.offset;
80 ackResponse.hdr.size = 0;
83 const auto makeEntry = [withTime](uint32_t index) {
84 QString entry = QStringLiteral(
"Ffile%1.txt\t%2").arg(index).arg(1024 + index);
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;
106 ackResponse.hdr.size = 1;
109 _sendResponse(senderSystemId, senderComponentId, &ackResponse, outgoingSeqNumber);
112void MockLinkFTP::_openCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
114 MavlinkFTP::Request response{};
115 ensureNullTemination(request);
116 const QString path =
reinterpret_cast<char*
>(request->data);
118 _uploadSession.reset();
120 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
122 const size_t cchPath = strnlen(
reinterpret_cast<char*
>(request->data),
sizeof(request->data));
123 Q_ASSERT(cchPath !=
sizeof(request->data));
126 _currentFile.close();
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);
146 if (!tmpFilename.isEmpty()) {
147 _currentFile.setFileName(tmpFilename);
148 if (!_currentFile.open(QIODevice::ReadOnly)) {
159 response.hdr.session = _sessionId;
162 response.hdr.size =
sizeof(uint32_t);
165 response.openFileLength = ((path ==
"@PARAM/param.pck" || path.startsWith(
"@PARAM/param.pck?")) ? qPow(1024, 2) : _currentFile.size());
167 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
170void MockLinkFTP::_createFileCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
172 ensureNullTemination(request);
174 const QString path =
reinterpret_cast<char*
>(request->data);
175 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
177 if (path.isEmpty()) {
182 _uploadSession.reset();
183 _uploadSession.active =
true;
184 _uploadSession.remotePath = path;
186 MavlinkFTP::Request response{};
189 response.hdr.session = _sessionId;
190 response.hdr.size = 0;
192 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
195void MockLinkFTP::_openFileWOCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
197 ensureNullTemination(request);
199 const QString path =
reinterpret_cast<char*
>(request->data);
200 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
202 if (path.isEmpty()) {
207 _uploadSession.reset();
208 _uploadSession.active =
true;
209 _uploadSession.remotePath = path;
211 MavlinkFTP::Request response{};
214 response.hdr.session = _sessionId;
215 response.hdr.size = 0;
217 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
220void MockLinkFTP::_readCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
222 MavlinkFTP::Request response{};
223 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
225 if (request->hdr.session != _sessionId) {
230 const uint32_t readOffset = request->hdr.offset;
231 if (readOffset != 0) {
245 if (readOffset >= _currentFile.size()) {
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);
256 Q_ASSERT(cBytesToRead);
258 response.hdr.session = _sessionId;
259 response.hdr.size = cBytesToRead;
260 response.hdr.offset = request->hdr.offset;
264 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
267void MockLinkFTP::_burstReadCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
269 MavlinkFTP::Request response{};
270 uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
272 if (request->hdr.session != _sessionId) {
277 constexpr int burstMax = 10;
279 uint32_t burstOffset = request->hdr.offset;
281 while ((burstOffset < _currentFile.size()) && (burstCount++ < burstMax)) {
282 _currentFile.seek(burstOffset);
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);
288 (void) memcpy(response.data, bytes.constData(), cBytes);
290 response.hdr.session = _sessionId;
291 response.hdr.size = cBytes;
292 response.hdr.offset = burstOffset;
295 response.hdr.burstComplete = (burstCount == burstMax) ? 1 : 0;
297 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
299 outgoingSeqNumber = _nextSeqNumber(outgoingSeqNumber);
300 burstOffset += cBytes;
303 if (burstOffset >= _currentFile.size()) {
309void MockLinkFTP::_terminateCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
311 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
313 if (request->hdr.session != _sessionId) {
318 _currentFile.close();
321 _finalizeActiveUpload();
326void MockLinkFTP::_resetCommand(uint8_t senderSystemId, uint8_t senderComponentId, uint16_t seqNumber)
328 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
330 _currentFile.close();
333 _finalizeActiveUpload();
338void MockLinkFTP::_writeCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
340 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
342 if ((request->hdr.session != _sessionId) || !_uploadSession.active) {
347 if (request->hdr.offset >
static_cast<uint32_t
>(_uploadSession.buffer.size())) {
352 if (request->hdr.offset != 0) {
368 const uint32_t bytesToWrite = request->hdr.size;
369 if (bytesToWrite >
sizeof(request->data)) {
374 const uint32_t requiredSize = request->hdr.offset + bytesToWrite;
375 if (requiredSize >
static_cast<uint32_t
>(_uploadSession.buffer.size())) {
376 _uploadSession.buffer.resize(requiredSize);
379 if (bytesToWrite > 0) {
380 (void) memcpy(_uploadSession.buffer.data() + request->hdr.offset, request->data, bytesToWrite);
383 MavlinkFTP::Request response{};
386 response.hdr.session = _sessionId;
387 response.hdr.size = 0;
388 response.hdr.offset = request->hdr.offset;
390 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
393void MockLinkFTP::_finalizeActiveUpload()
395 if (!_uploadSession.active) {
399 if (!_uploadSession.remotePath.isEmpty()) {
400 _uploadedFiles.insert(_uploadSession.remotePath, _uploadSession.buffer);
403 _uploadSession.reset();
408 if (message.msgid != MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL) {
412 mavlink_file_transfer_protocol_t requestFTP{};
413 mavlink_msg_file_transfer_protocol_decode(&message, &requestFTP);
415 if (requestFTP.target_system != _systemIdServer) {
419 MavlinkFTP::Request *request =
reinterpret_cast<MavlinkFTP::Request*
>(&requestFTP.payload[0]);
423 if ((rand() % 5) == 0) {
424 qCDebug(MockLinkFTPLog) <<
"MockLinkFTP: Random drop of incoming packet";
429 if (_lastReplyValid && (request->hdr.seqNumber == (_lastReplySequence - 1))) {
432 qCDebug(MockLinkFTPLog) <<
"MockLinkFTP: resending response";
437 const uint16_t incomingSeqNumber = request->hdr.seqNumber;
438 const uint16_t outgoingSeqNumber = _nextSeqNumber(incomingSeqNumber);
453 MavlinkFTP::Request ackResponse{};
454 switch (request->hdr.opcode) {
458 ackResponse.hdr.session = 0;
459 ackResponse.hdr.size = 0;
460 _sendResponse(message.sysid, message.compid, &ackResponse, outgoingSeqNumber);
463 _listCommand(message.sysid, message.compid, request, incomingSeqNumber,
false );
466 _listCommand(message.sysid, message.compid, request, incomingSeqNumber,
true );
469 _openCommand(message.sysid, message.compid, request, incomingSeqNumber);
472 _createFileCommand(message.sysid, message.compid, request, incomingSeqNumber);
475 _openFileWOCommand(message.sysid, message.compid, request, incomingSeqNumber);
478 _readCommand(message.sysid, message.compid, request, incomingSeqNumber);
481 _burstReadCommand(message.sysid, message.compid, request, incomingSeqNumber);
484 _writeCommand(message.sysid, message.compid, request, incomingSeqNumber);
487 _terminateCommand(message.sysid, message.compid, request, incomingSeqNumber);
490 _resetCommand(message.sysid, message.compid, incomingSeqNumber);
499void MockLinkFTP::_sendAck(uint8_t targetSystemId, uint8_t targetComponentId, uint16_t seqNumber,
MavlinkFTP::OpCode_t reqOpcode)
501 MavlinkFTP::Request ackResponse{};
504 ackResponse.hdr.req_opcode = reqOpcode;
505 ackResponse.hdr.session = _sessionId;
506 ackResponse.hdr.size = 0;
508 _sendResponse(targetSystemId, targetComponentId, &ackResponse, seqNumber);
513 MavlinkFTP::Request nakResponse{};
516 nakResponse.hdr.req_opcode = reqOpcode;
517 nakResponse.hdr.session = _sessionId;
518 nakResponse.hdr.size = 1;
519 nakResponse.data[0] =
error;
521 _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
524void MockLinkFTP::_sendNakErrno(uint8_t targetSystemId, uint8_t targetComponentId, uint8_t nakErrno, uint16_t seqNumber,
MavlinkFTP::OpCode_t reqOpcode)
526 MavlinkFTP::Request nakResponse{};
529 nakResponse.hdr.req_opcode = reqOpcode;
530 nakResponse.hdr.session = _sessionId;
531 nakResponse.hdr.size = 2;
533 nakResponse.data[1] = nakErrno;
535 _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
539void MockLinkFTP::_sendResponse(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
541 request->hdr.seqNumber = seqNumber;
542 _lastReplySequence = seqNumber;
543 _lastReplyValid =
true;
545 (void) mavlink_msg_file_transfer_protocol_pack_chan(
553 reinterpret_cast<uint8_t*
>(request)
558 if ((rand() % 5) == 0) {
559 qCDebug(MockLinkFTPLog) <<
"MockLinkFTP: Random drop of outgoing packet";
568uint16_t MockLinkFTP::_nextSeqNumber(uint16_t seqNumber)
const
570 uint16_t outgoingSeqNumber = seqNumber + 1;
576 return outgoingSeqNumber;
579QString MockLinkFTP::_createTestTempFile(
int size)
581 QTemporaryFile tmpFile(QDir::tempPath() + QStringLiteral(
"/MockLinkFTPTestCaseXXXXXX"));
582 tmpFile.setAutoRemove(
false);
584 if (tmpFile.open()) {
585 for (
int i = 0; i < size; i++) {
586 (void) tmpFile.write(QByteArray(1,
static_cast<char>(i % 255)));
591 return tmpFile.fileName();
594QString MockLinkFTP::_generateParamPck(
bool withDefaults)
597 enum APParamType : quint8 {
606 constexpr quint16 magicStandard = 0x671B;
607 constexpr quint16 magicWithDefaults = 0x671C;
608 constexpr int readSize = 239;
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;
620 const auto &values = valueMap[compId];
621 const auto &types = typeMap[compId];
624 QStringList paramNames = values.keys();
627 const quint16 numParams =
static_cast<quint16
>(paramNames.size());
630 auto mapType = [](MAV_PARAM_TYPE mavType, quint8 &apType,
int &valueSize) {
632 case MAV_PARAM_TYPE_UINT8:
633 case MAV_PARAM_TYPE_INT8:
634 apType = AP_PARAM_INT8;
637 case MAV_PARAM_TYPE_UINT16:
638 case MAV_PARAM_TYPE_INT16:
639 apType = AP_PARAM_INT16;
642 case MAV_PARAM_TYPE_UINT32:
643 case MAV_PARAM_TYPE_INT32:
644 apType = AP_PARAM_INT32;
647 case MAV_PARAM_TYPE_REAL32:
648 apType = AP_PARAM_FLOAT;
652 apType = AP_PARAM_FLOAT;
659 QDataStream stream(&data, QIODevice::WriteOnly);
660 stream.setByteOrder(QDataStream::LittleEndian);
663 const quint16 magic = withDefaults ? magicWithDefaults : magicStandard;
664 stream << magic << numParams << numParams;
666 QByteArray previousName;
667 constexpr int headerSize = 6;
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();
676 mapType(mavType, apType, valueSize);
679 const int previousNameLen = previousName.size();
680 const int currentNameLen = nameBytes.size();
683 const int maxCommon = std::min({previousNameLen, currentNameLen, 15});
684 while (commonLen < maxCommon && nameBytes[commonLen] == previousName[commonLen]) {
688 int nameLen = currentNameLen - commonLen;
691 if (nameLen == 0 && commonLen > 0) {
696 if (nameLen + commonLen > 16) {
697 commonLen = 16 - nameLen;
709 const bool useZeroDefault = withDefaults
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);
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);
735 stream << static_cast<quint8>(apType | (flags << 4));
737 stream << static_cast<quint8>(((nameLen - 1) << 4) | commonLen);
739 stream.writeRawData(nameBytes.constData() + commonLen, nameLen);
743 case AP_PARAM_INT8: {
744 const qint8 v =
static_cast<qint8
>(value.toInt());
746 if (addDefault) stream << (useZeroDefault ? qint8(0) : v);
749 case AP_PARAM_INT16: {
750 const qint16 v =
static_cast<qint16
>(value.toInt());
752 if (addDefault) stream << (useZeroDefault ? qint16(0) : v);
755 case AP_PARAM_INT32: {
756 const qint32 v = value.toInt();
758 if (addDefault) stream << (useZeroDefault ? qint32(0) : v);
761 case AP_PARAM_FLOAT: {
762 const float f = value.toFloat();
764 memcpy(&raw, &f,
sizeof(raw));
767 if (useZeroDefault) {
777 previousName = nameBytes;
781 if (!_paramPckTempFile.isEmpty()) {
782 QFile::remove(_paramPckTempFile);
783 _paramPckTempFile.clear();
787 QTemporaryFile tmpFile(QDir::temp().filePath(QStringLiteral(
"MockLinkParamPckXXXXXX")));
788 tmpFile.setAutoRemove(
false);
790 if (!tmpFile.open()) {
791 qCWarning(MockLinkFTPLog) <<
"_generateParamPck: failed to create temp file";
797 _paramPckTempFile = tmpFile.fileName();
799 qCDebug(MockLinkFTPLog) <<
"_generateParamPck: generated" << numParams <<
"params,"
800 << data.size() <<
"bytes, withDefaults:" << withDefaults
801 <<
"file:" << tmpFile.fileName();
803 return tmpFile.fileName();
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].
@ kErrFail
Unknown failure.
@ kErrEOF
Offset past end of file for List and Read commands.
@ kErrUnknownCommand
Unknown command opcode.
@ kErrFailErrno
errno sent back in PayloadHeader.data[1]
@ kErrInvalidSession
Session is not currently open.
@ kCmdWriteFile
Writes <size> bytes to <offset> in <session>
@ kCmdCreateFile
Creates file at <path> for writing, returns <session>
@ kCmdOpenFileRO
Opens file at <path> for reading, returns <session>
@ kCmdListDirectoryWithTime
List directory as kCmdListDirectory, each entry additionally carrying trailing \t<modification time>
@ kCmdReadFile
Reads <size> bytes from <offset> in <session>
@ kCmdOpenFileWO
Opens file at <path> for writing, returns <session>
@ kCmdResetSessions
Terminates all open Read sessions.
@ kCmdNone
ignored, always acked
@ kCmdBurstReadFile
Burst download session file.
@ kCmdTerminateSession
Terminates open Read session.
@ kCmdListDirectory
List files in <path> from <offset>
Mock implementation of Mavlink FTP server.
void resetCommandReceived()
You can connect to this signal to be notified when the server receives a Reset command.
static constexpr uint32_t kMockModificationTime
void terminateCommandReceived()
You can connect to this signal to be notified when the server receives a Terminate command.
static constexpr const char * sizeFilenamePrefix
void mavlinkMessageReceived(const mavlink_message_t &message)
Called to handle an FTP message.
@ errModeBadSequence
Return response with bad sequence number.
@ errModeNoSecondResponseAllowRetry
No response to subsequent request to initial command, error will be cleared after this so retry will ...
@ errModeNakSecondResponse
Nak subsequent request to initial command.
@ errModeNoResponse
No response to any request, client should eventually time out with no Ack.
@ errModeNone
No error, respond correctly.
@ errModeNoSecondResponse
No response to subsequent request to initial command.
@ errModeNakResponse
Nak all requests.
MAV_AUTOPILOT getFirmwareType() const
void respondWithMavlinkMessage(const mavlink_message_t &msg)
Sends the specified mavlink message to QGC.