diff --git a/src/qgcunittest/MockMavlinkFileServer.cc b/src/qgcunittest/MockMavlinkFileServer.cc index b17aa898938fd306f6c3b7d436e24baf21cc0ea8..65776119fcacd2e3dc61856bfe4a88f31e835679 100644 --- a/src/qgcunittest/MockMavlinkFileServer.cc +++ b/src/qgcunittest/MockMavlinkFileServer.cc @@ -23,11 +23,168 @@ #include "MockMavlinkFileServer.h" -void MockMavlinkFileServer::sendMessage(mavlink_message_t message) +const MockMavlinkFileServer::FileTestCase MockMavlinkFileServer::rgFileTestCases[MockMavlinkFileServer::cFileTestCases] = { + // File fits one Read Ack packet, partially filling data + { "partial.qgc", sizeof(((QGCUASFileManager::Request*)0)->data) - 1 }, + // File fits one Read Ack packet, exactly filling all data + { "exact.qgc", sizeof(((QGCUASFileManager::Request*)0)->data) }, + // File is larger than a single Read Ack packets, requires multiple Reads + { "multi.qgc", sizeof(((QGCUASFileManager::Request*)0)->data) + 1 }, +}; + +// We only support a single fixed session +const uint8_t MockMavlinkFileServer::_sessionId = 1; + +MockMavlinkFileServer::MockMavlinkFileServer(void) +{ + +} + + + +/// @brief Handles List command requests. Only supports root folder paths. +/// File list returned is set using the setFileList method. +void MockMavlinkFileServer::_listCommand(QGCUASFileManager::Request* request) { + // FIXME: Does not support directories that span multiple packets + QGCUASFileManager::Request ackResponse; QString path; + // We only support root path + path = (char *)&request->data[0]; + if (!path.isEmpty() && path != "/") { + _sendNak(QGCUASFileManager::kErrNotDir); + return; + } + + // Offset requested is past the end of the list + if (request->hdr.offset > (uint32_t)_fileList.size()) { + _sendNak(QGCUASFileManager::kErrEOF); + return; + } + + ackResponse.hdr.magic = 'f'; + ackResponse.hdr.opcode = QGCUASFileManager::kRspAck; + ackResponse.hdr.session = 0; + ackResponse.hdr.offset = request->hdr.offset; + ackResponse.hdr.size = 0; + + if (request->hdr.offset == 0) { + // Requesting first batch of file names + Q_ASSERT(_fileList.size()); + char *bufPtr = (char *)&ackResponse.data[0]; + for (int i=0; i<_fileList.size(); i++) { + strcpy(bufPtr, _fileList[i].toStdString().c_str()); + size_t cchFilename = strlen(bufPtr); + Q_ASSERT(cchFilename); + ackResponse.hdr.size += cchFilename + 1; + bufPtr += cchFilename + 1; + } + + _emitResponse(&ackResponse); + } else { + // FIXME: Does not support directories that span multiple packets + _sendNak(QGCUASFileManager::kErrEOF); + } +} + +/// @brief Handles Open command requests. +void MockMavlinkFileServer::_openCommand(QGCUASFileManager::Request* request) +{ + QGCUASFileManager::Request response; + QString path; + + size_t cchPath = strnlen((char *)request->data, sizeof(request->data)); + Q_ASSERT(cchPath != sizeof(request->data)); + path = (char *)request->data; + + // Check path against one of our known test cases + + bool found = false; + for (size_t i=0; ihdr.session != _sessionId) { + _sendNak(QGCUASFileManager::kErrNoSession); + return; + } + + uint32_t readOffset = request->hdr.offset; // offset into file for reading + uint8_t cDataBytes = 0; // current number of data bytes used + + if (readOffset >= _readFileLength) { + _sendNak(QGCUASFileManager::kErrEOF); + return; + } + + // Write length byte if needed + if (readOffset == 0) { + response.data[0] = _readFileLength; + readOffset++; + cDataBytes++; + } + + // Write file bytes. Data is a repeating sequence of 0x00, 0x01, .. 0xFF. + for (; cDataBytes < sizeof(response.data) && readOffset < _readFileLength; readOffset++, cDataBytes++) { + // Subtract one from readOffset to take into account length byte and start file data a 0x00 + response.data[cDataBytes] = (readOffset - 1) & 0xFF; + } + + // We should always have written something, otherwise there is something wrong with the code above + Q_ASSERT(cDataBytes); + + response.hdr.magic = 'f'; + response.hdr.session = _sessionId; + response.hdr.size = cDataBytes; + response.hdr.offset = request->hdr.offset; + response.hdr.opcode = QGCUASFileManager::kRspAck; + + _emitResponse(&response); +} + +/// @brief Handles Terminate commands +void MockMavlinkFileServer::_terminateCommand(QGCUASFileManager::Request* request) +{ + if (request->hdr.session != _sessionId) { + _sendNak(QGCUASFileManager::kErrNoSession); + return; + } + + _sendAck(); + + // Let our test harness know that we got a terminate command. This is used to validate the a Terminate is correctly + // sent after an Open. + emit terminateCommandReceived(); +} + +/// @brief Handles messages sent to the FTP server. +void MockMavlinkFileServer::sendMessage(mavlink_message_t message) +{ + QGCUASFileManager::Request ackResponse; + Q_ASSERT(message.msgid == MAVLINK_MSG_ID_ENCAPSULATED_DATA); mavlink_encapsulated_data_t requestEncapsulatedData; @@ -59,57 +216,23 @@ void MockMavlinkFileServer::sendMessage(mavlink_message_t message) break; case QGCUASFileManager::kCmdList: - // We only support root path - path = (char *)&request->data[0]; - if (!path.isEmpty() && path != "/") { - _sendNak(QGCUASFileManager::kErrNotDir); - break; - } - - if (request->hdr.offset > (uint32_t)_fileList.size()) { - _sendNak(QGCUASFileManager::kErrEOF); - break; - } - - ackResponse.hdr.magic = 'f'; - ackResponse.hdr.opcode = QGCUASFileManager::kRspAck; - ackResponse.hdr.session = 0; - ackResponse.hdr.size = 0; - - if (request->hdr.offset == 0) { - // Requesting first batch of file names - Q_ASSERT(_fileList.size()); - char *bufPtr = (char *)&ackResponse.data[0]; - for (int i=0; i<_fileList.size(); i++) { - const char *filename = _fileList[i].toStdString().c_str(); - size_t cchFilename = strlen(filename); - strcpy(bufPtr, filename); - ackResponse.hdr.size += cchFilename + 1; - bufPtr += cchFilename + 1; - } - - // Final double termination - *bufPtr = 0; - ackResponse.hdr.size++; - - } else { - // All filenames fit in first ack, send final null terminated ack - ackResponse.data[0] = 0; - ackResponse.hdr.size = 1; - } + _listCommand(request); + break; - _emitResponse(&ackResponse); + case QGCUASFileManager::kCmdOpen: + _openCommand(request); + break; + case QGCUASFileManager::kCmdRead: + _readCommand(request); break; - - // Remainder of commands are NYI case QGCUASFileManager::kCmdTerminate: - // releases sessionID, closes file - case QGCUASFileManager::kCmdOpen: - // opens for reading, returns - case QGCUASFileManager::kCmdRead: - // reads bytes from in + _terminateCommand(request); + break; + + // Remainder of commands are NYI + case QGCUASFileManager::kCmdCreate: // creates for writing, returns case QGCUASFileManager::kCmdWrite: @@ -123,6 +246,20 @@ void MockMavlinkFileServer::sendMessage(mavlink_message_t message) } } +/// @brief Sends an Ack +void MockMavlinkFileServer::_sendAck(void) +{ + QGCUASFileManager::Request ackResponse; + + ackResponse.hdr.magic = 'f'; + ackResponse.hdr.opcode = QGCUASFileManager::kRspAck; + ackResponse.hdr.session = 0; + ackResponse.hdr.size = 0; + + _emitResponse(&ackResponse); +} + +/// @brief Sends a Nak with the specified error code. void MockMavlinkFileServer::_sendNak(QGCUASFileManager::ErrorCode error) { QGCUASFileManager::Request nakResponse; @@ -130,12 +267,13 @@ void MockMavlinkFileServer::_sendNak(QGCUASFileManager::ErrorCode error) nakResponse.hdr.magic = 'f'; nakResponse.hdr.opcode = QGCUASFileManager::kRspNak; nakResponse.hdr.session = 0; - nakResponse.hdr.size = sizeof(nakResponse.data[0]); + nakResponse.hdr.size = 1; nakResponse.data[0] = error; _emitResponse(&nakResponse); } +/// @brief Emits a Request through the messageReceived signal. void MockMavlinkFileServer::_emitResponse(QGCUASFileManager::Request* request) { mavlink_message_t mavlinkMessage; diff --git a/src/qgcunittest/MockMavlinkFileServer.h b/src/qgcunittest/MockMavlinkFileServer.h index 0bccd3d4ab0915c27611210b6a686e3063904a68..7c581c788b26dce8b36aa3a77f75ccc4f6a3267d 100644 --- a/src/qgcunittest/MockMavlinkFileServer.h +++ b/src/qgcunittest/MockMavlinkFileServer.h @@ -27,6 +27,11 @@ #include "MockMavlinkInterface.h" #include "QGCUASFileManager.h" +/// @file +/// @brief Mock implementation of Mavlink FTP server. Used as mavlink plugin to MockUAS. +/// Only root directory access is supported. +/// +/// @author Don Gagne #include @@ -35,18 +40,39 @@ class MockMavlinkFileServer : public MockMavlinkInterface Q_OBJECT public: - MockMavlinkFileServer(void) { }; + MockMavlinkFileServer(void); + /// @brief Sets the list of files returned by the List command. Prepend names with F or D + /// to indicate (F)ile or (D)irectory. void setFileList(QStringList& fileList) { _fileList = fileList; } // From MockMavlinkInterface virtual void sendMessage(mavlink_message_t message); + struct FileTestCase { + const char* filename; + uint8_t length; + }; + + static const size_t cFileTestCases = 3; + static const FileTestCase rgFileTestCases[cFileTestCases]; + +signals: + void terminateCommandReceived(void); + private: + void _sendAck(void); void _sendNak(QGCUASFileManager::ErrorCode error); void _emitResponse(QGCUASFileManager::Request* request); + void _listCommand(QGCUASFileManager::Request* request); + void _openCommand(QGCUASFileManager::Request* request); + void _readCommand(QGCUASFileManager::Request* request); + void _terminateCommand(QGCUASFileManager::Request* request); + + QStringList _fileList; ///< List of files returned by List command - QStringList _fileList; + static const uint8_t _sessionId; + uint8_t _readFileLength; ///< Length of active file being read }; #endif diff --git a/src/qgcunittest/MockUAS.cc b/src/qgcunittest/MockUAS.cc index 443bb932c4383467f9c16565a7d576a0b667d4bb..82dedffc2f10f1a7ff87b4647184e4d50faa43f9 100644 --- a/src/qgcunittest/MockUAS.cc +++ b/src/qgcunittest/MockUAS.cc @@ -46,8 +46,6 @@ void MockUAS::setMockParametersAndSignal(MockQGCUASParamManager::ParamMap_t& map void MockUAS::sendMessage(mavlink_message_t message) { - Q_UNUSED(link); - if (!_mavlinkPlugin) { Q_ASSERT(false); } diff --git a/src/qgcunittest/QGCUASFileManagerTest.cc b/src/qgcunittest/QGCUASFileManagerTest.cc index fe325de78f24eb24077c47a4bf3f320a5317034d..7da1dbdab6e78af49fa93ffccaa6795dbcd7246a 100644 --- a/src/qgcunittest/QGCUASFileManagerTest.cc +++ b/src/qgcunittest/QGCUASFileManagerTest.cc @@ -24,7 +24,9 @@ #include "QGCUASFileManagerTest.h" /// @file -/// @brief QGCUASFileManager unit test +/// @brief QGCUASFileManager unit test. Note: All code here assumes all work between +/// the unit test, mack mavlink file server and file manager is happening on +/// the same thread. /// /// @author Don Gagne @@ -130,7 +132,7 @@ void QGCUASFileManagerUnitTest::_listTest(void) // Send a bogus path // We should get a single resetStatusMessages signal // We should get a single errorMessage signal - _fileManager->listRecursively("/bogus"); + _fileManager->listDirectory("/bogus"); QCOMPARE(_multiSpy->checkOnlySignalByMask(errorMessageSignalMask | resetStatusMessagesSignalMask), true); _multiSpy->clearAllSignals(); @@ -149,8 +151,74 @@ void QGCUASFileManagerUnitTest::_listTest(void) _fileListReceived.clear(); - _fileManager->listRecursively("/"); + _fileManager->listDirectory("/"); QCOMPARE(_multiSpy->checkSignalByMask(resetStatusMessagesSignalMask), true); // We should be told to reset status messages QCOMPARE(_multiSpy->checkNoSignalByMask(errorMessageSignalMask), true); // We should not get an error signals QVERIFY(_fileListReceived == fileListExpected); } + +void QGCUASFileManagerUnitTest::_validateFileContents(const QString& filePath, uint8_t length) +{ + QFile file(filePath); + + // Make sure file size is correct + QCOMPARE(file.size(), (qint64)length); + + // Read data + QVERIFY(file.open(QIODevice::ReadOnly)); + QByteArray bytes = file.readAll(); + file.close(); + + // Validate length byte + QCOMPARE((uint8_t)bytes[0], length); + + // Validate file contents: + // Repeating 0x00, 0x01 .. 0xFF until file is full + for (uint8_t i=1; icheckNoSignals() == true); + + // Send a bogus path + // We should get a single resetStatusMessages signal + // We should get a single errorMessage signal + _fileManager->downloadPath("bogus", QDir::temp()); + QCOMPARE(_multiSpy->checkOnlySignalByMask(errorMessageSignalMask | resetStatusMessagesSignalMask), true); + _multiSpy->clearAllSignals(); + + // Clean previous downloads + for (size_t i=0; idownloadPath(MockMavlinkFileServer::rgFileTestCases[i].filename, QDir::temp()); + + // We should get a single resetStatusMessages signal + // We should get a single statusMessage signal, which indicated download completion + QCOMPARE(_multiSpy->checkOnlySignalByMask(statusMessageSignalMask | resetStatusMessagesSignalMask), true); + _multiSpy->clearAllSignals(); + + // We should get a single Terminate command + QCOMPARE(terminateSpy.count(), 1); + terminateSpy.clear(); + + QString filePath = QDir::temp().absoluteFilePath(MockMavlinkFileServer::rgFileTestCases[i].filename); + _validateFileContents(filePath, MockMavlinkFileServer::rgFileTestCases[i].length); + } +} diff --git a/src/qgcunittest/QGCUASFileManagerTest.h b/src/qgcunittest/QGCUASFileManagerTest.h index 2a76fbb5d4bc4cfcd71e9645bb4e6671763ccbc5..a2c6ac32e00fe165d983bde6a89f652b4d1f9d19 100644 --- a/src/qgcunittest/QGCUASFileManagerTest.h +++ b/src/qgcunittest/QGCUASFileManagerTest.h @@ -56,11 +56,14 @@ private slots: void _noAckTest(void); void _resetTest(void); void _listTest(void); + void _openTest(void); // Connected to QGCUASFileManager statusMessage signal void statusMessage(const QString&); private: + void _validateFileContents(const QString& filePath, uint8_t length); + enum { statusMessageSignalIndex = 0, errorMessageSignalIndex, diff --git a/src/uas/QGCUASFileManager.cc b/src/uas/QGCUASFileManager.cc index 41d27d4ba146ad3e7d92eb35641f8ba5e950f372..8eaee04fa81321647cc77a0b69864599b91c2fd0 100644 --- a/src/uas/QGCUASFileManager.cc +++ b/src/uas/QGCUASFileManager.cc @@ -65,16 +65,17 @@ static const quint32 crctab[] = 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d }; + QGCUASFileManager::QGCUASFileManager(QObject* parent, UASInterface* uas) : QObject(parent), _currentOperation(kCOIdle), _mav(uas), - _encdata_seq(0) + _encdata_seq(0), + _activeSession(0) { bool connected = connect(&_ackTimer, SIGNAL(timeout()), this, SLOT(_ackTimeout())); Q_ASSERT(connected); - - Q_UNUSED(connected); + Q_UNUSED(connected); // Silence retail unused variable error } /// @brief Calculates a 32 bit CRC for the specified request. @@ -103,6 +104,153 @@ void QGCUASFileManager::nothingMessage() // FIXME: Connect ui correctly } +/// @brief Respond to the Ack associated with the Open command with the next Read command. +void QGCUASFileManager::_openAckResponse(Request* openAck) +{ + _currentOperation = kCORead; + _activeSession = openAck->hdr.session; + + _readOffset = 0; // Start reading at beginning of file + _readFileAccumulator.clear(); // Start with an empty file + + Request request; + request.hdr.magic = 'f'; + request.hdr.session = _activeSession; + request.hdr.opcode = kCmdRead; + request.hdr.offset = _readOffset; + request.hdr.size = sizeof(request.data); + + _sendRequest(&request); +} + +/// @brief Closes out a read session by writing the file and doing cleanup. +/// @param success true: successful download completion, false: error during download +void QGCUASFileManager::_closeReadSession(bool success) +{ + if (success) { + QString downloadFilePath = _readFileDownloadDir.absoluteFilePath(_readFileDownloadFilename); + + QFile file(downloadFilePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + _emitErrorMessage(tr("Unable to open local file for writing (%1)").arg(downloadFilePath)); + return; + } + + qint64 bytesWritten = file.write((const char *)_readFileAccumulator, _readFileAccumulator.length()); + if (bytesWritten != _readFileAccumulator.length()) { + file.close(); + _emitErrorMessage(tr("Unable to write data to local file (%1)").arg(downloadFilePath)); + return; + } + file.close(); + + _emitStatusMessage(tr("Download complete '%1'").arg(downloadFilePath)); + } + + // Close the open session + _sendTerminateCommand(); +} + +/// @brief Respond to the Ack associated with the Read command. +void QGCUASFileManager::_readAckResponse(Request* readAck) +{ + if (readAck->hdr.session != _activeSession) { + _currentOperation = kCOIdle; + _readFileAccumulator.clear(); + _emitErrorMessage(tr("Read: Incorrect session returned")); + return; + } + + if (readAck->hdr.offset != _readOffset) { + _currentOperation = kCOIdle; + _readFileAccumulator.clear(); + _emitErrorMessage(tr("Read: Offset returned (%1) differs from offset requested (%2)").arg(readAck->hdr.offset).arg(_readOffset)); + return; + } + + _readFileAccumulator.append((const char*)readAck->data, readAck->hdr.size); + + if (readAck->hdr.size == sizeof(readAck->data)) { + // Possibly still more data to read, send next read request + + _currentOperation = kCORead; + + _readOffset += readAck->hdr.size; + + Request request; + request.hdr.magic = 'f'; + request.hdr.session = _activeSession; + request.hdr.opcode = kCmdRead; + request.hdr.offset = _readOffset; + + _sendRequest(&request); + } else { + // We only receieved a partial buffer back. These means we are at EOF + _currentOperation = kCOIdle; + _closeReadSession(true /* success */); + } +} + +/// @brief Respond to the Ack associated with the List command. +void QGCUASFileManager::_listAckResponse(Request* listAck) +{ + if (listAck->hdr.offset != _listOffset) { + _currentOperation = kCOIdle; + _emitErrorMessage(tr("List: Offset returned (%1) differs from offset requested (%2)").arg(listAck->hdr.offset).arg(_listOffset)); + return; + } + + uint8_t offset = 0; + uint8_t cListEntries = 0; + uint8_t cBytes = listAck->hdr.size; + + // parse filenames out of the buffer + while (offset < cBytes) { + const char * ptr = ((const char *)listAck->data) + offset; + + // get the length of the name + uint8_t cBytesLeft = cBytes - offset; + size_t nlen = strnlen(ptr, cBytesLeft); + if (nlen < 2) { + _currentOperation = kCOIdle; + _emitErrorMessage(tr("Incorrectly formed list entry: '%1'").arg(ptr)); + return; + } else if (nlen == cBytesLeft) { + _currentOperation = kCOIdle; + _emitErrorMessage(tr("Missing NULL termination in list entry")); + return; + } + + // Returned names are prepended with D for directory, F for file, U for unknown + + QString s(ptr + 1); + if (*ptr == 'D') { + s.append('/'); + } + + if (*ptr == 'F' || *ptr == 'D') { + // put it in the view + _emitStatusMessage(s); + } + + // account for the name + NUL + offset += nlen + 1; + + cListEntries++; + } + + if (listAck->hdr.size == 0) { + // Directory is empty, we're done + Q_ASSERT(listAck->hdr.opcode == kRspAck); + _currentOperation = kCOIdle; + } else { + // Possibly more entries to come, need to keep trying till we get EOF + _currentOperation = kCOList; + _listOffset += cListEntries; + _sendListCommand(); + } +} + void QGCUASFileManager::receiveMessage(LinkInterface* link, mavlink_message_t message) { Q_UNUSED(link); @@ -112,50 +260,74 @@ void QGCUASFileManager::receiveMessage(LinkInterface* link, mavlink_message_t me return; } + _clearAckTimeout(); + mavlink_encapsulated_data_t data; mavlink_msg_encapsulated_data_decode(&message, &data); Request* request = (Request*)&data.data[0]; - qDebug() << "FTP: opcode:" << request->hdr.opcode; - // FIXME: Check CRC if (request->hdr.opcode == kRspAck) { - _clearAckTimeout(); switch (_currentOperation) { case kCOIdle: // we should not be seeing anything here.. shut the other guy up - qDebug() << "FTP resetting file transfer session"; _sendCmdReset(); break; case kCOAck: // We are expecting an ack back - _clearAckTimeout(); _currentOperation = kCOIdle; break; case kCOList: - listDecode(&request->data[0], request->hdr.size); + _listAckResponse(request); + break; + + case kCOOpen: + _openAckResponse(request); break; + case kCORead: + _readAckResponse(request); + break; + default: _emitErrorMessage("Ack received in unexpected state"); break; } } else if (request->hdr.opcode == kRspNak) { - _clearAckTimeout(); - _emitErrorMessage(QString("Nak received, error: ").append(errorString(request->data[0]))); + Q_ASSERT(request->hdr.size == 1); // Should only have one byte of error code + + OperationState previousOperation = _currentOperation; + uint8_t errorCode = request->data[0]; + _currentOperation = kCOIdle; + + if (previousOperation == kCOList && errorCode == kErrEOF) { + // This is not an error, just the end of the read loop + return; + } else if (previousOperation == kCORead && errorCode == kErrEOF) { + // This is not an error, just the end of the read loop + _closeReadSession(true /* success */); + return; + } else { + // Generic Nak handling + if (previousOperation == kCORead) { + // Nak error during read loop, download failed + _closeReadSession(false /* failure */); + } + _emitErrorMessage(tr("Nak received, error: %1").arg(errorString(request->data[0]))); + } } else { // Note that we don't change our operation state. If something goes wrong beyond this, the operation // will time out. - _emitErrorMessage(tr("Unknown opcode returned server: %1").arg(request->hdr.opcode)); + _emitErrorMessage(tr("Unknown opcode returned from server: %1").arg(request->hdr.opcode)); } } -void QGCUASFileManager::listRecursively(const QString &from) +void QGCUASFileManager::listDirectory(const QString& dirPath) { if (_currentOperation != kCOIdle) { _emitErrorMessage(tr("Command not sent. Waiting for previous command to complete.")); @@ -166,58 +338,21 @@ void QGCUASFileManager::listRecursively(const QString &from) emit resetStatusMessages(); // initialise the lister - _listPath = from; + _listPath = dirPath; _listOffset = 0; _currentOperation = kCOList; // and send the initial request - sendList(); + _sendListCommand(); } -void QGCUASFileManager::listDecode(const uint8_t *data, unsigned len) +void QGCUASFileManager::_fillRequestWithString(Request* request, const QString& str) { - unsigned offset = 0; - unsigned files = 0; - - // parse filenames out of the buffer - while (offset < len) { - const char * ptr = (const char *)data + offset; - - // get the length of the name - unsigned nlen = strnlen(ptr, len - offset); - if (nlen < 2) { - break; - } - - // Returned names are prepended with D for directory or F for file - QString s(ptr + 1); - if (*ptr == 'D') { - s.append('/'); - } - - // put it in the view - emit statusMessage(s); - - // account for the name + NUL - offset += nlen + 1; - - // account for the file - files++; - } - - // we have run out of files to list - if (files == 0) { - _currentOperation = kCOIdle; - } else { - // update our state - _listOffset += files; - - // and send another request - sendList(); - } + strncpy((char *)&request->data[0], str.toStdString().c_str(), sizeof(request->data)); + request->hdr.size = strnlen((const char *)&request->data[0], sizeof(request->data)); } -void QGCUASFileManager::sendList() +void QGCUASFileManager::_sendListCommand(void) { Request request; @@ -226,32 +361,44 @@ void QGCUASFileManager::sendList() request.hdr.opcode = kCmdList; request.hdr.offset = _listOffset; - strncpy((char *)&request.data[0], _listPath.toStdString().c_str(), sizeof(request.data)); - request.hdr.size = strnlen((const char *)&request.data[0], sizeof(request.data)); + _fillRequestWithString(&request, _listPath); _sendRequest(&request); } -void QGCUASFileManager::downloadPath(const QString &from, const QString &to) +/// @brief Downloads the specified file. +/// @param from File to download from UAS, fully qualified path +/// @param downloadDir Local directory to download file to +void QGCUASFileManager::downloadPath(const QString& from, const QDir& downloadDir) { - Q_UNUSED(from); + if (from.isEmpty()) { + return; + } - // Send path, e.g. /fs/microsd and download content - // recursively into a local directory - - char buf[255]; - unsigned len = 10; - - QByteArray data(buf, len); - QString filename = "log001.bin"; // XXX get this from onboard - - // Qt takes care of slash conversions in paths - QFile file(to + QDir::separator() + filename); - file.open(QIODevice::WriteOnly); - file.write(data); - file.close(); + _readFileDownloadDir.setPath(downloadDir.absolutePath()); + + // We need to strip off the file name from the fully qualified path. We can't use the usual QDir + // routines because this path does not exist locally. + int i; + for (i=from.size()-1; i>=0; i--) { + if (from[i] == '/') { + break; + } + } + i++; // move past slash + _readFileDownloadFilename = from.right(from.size() - i); + + emit resetStatusMessages(); + + _currentOperation = kCOOpen; - emit statusMessage(QString("Downloaded: %1 to directory %2").arg(filename).arg(to)); + Request request; + request.hdr.magic = 'f'; + request.hdr.session = 0; + request.hdr.opcode = kCmdOpen; + request.hdr.offset = 0; + _fillRequestWithString(&request, from); + _sendRequest(&request); } QString QGCUASFileManager::errorString(uint8_t errorCode) @@ -307,7 +454,6 @@ bool QGCUASFileManager::_sendOpcodeOnlyCmd(uint8_t opcode, OperationState newOpS request.hdr.size = 0; _currentOperation = newOpState; - _setupAckTimeout(); _sendRequest(&request); @@ -317,8 +463,6 @@ bool QGCUASFileManager::_sendOpcodeOnlyCmd(uint8_t opcode, OperationState newOpS /// @brief Starts the ack timeout timer void QGCUASFileManager::_setupAckTimeout(void) { - qDebug() << "Setting Ack"; - Q_ASSERT(!_ackTimer.isActive()); _ackTimer.setSingleShot(true); @@ -328,8 +472,6 @@ void QGCUASFileManager::_setupAckTimeout(void) /// @brief Clears the ack timeout timer void QGCUASFileManager::_clearAckTimeout(void) { - qDebug() << "Clearing Ack"; - Q_ASSERT(_ackTimer.isActive()); _ackTimer.stop(); @@ -338,16 +480,40 @@ void QGCUASFileManager::_clearAckTimeout(void) /// @brief Called when ack timeout timer fires void QGCUASFileManager::_ackTimeout(void) { - _currentOperation = kCOIdle; _emitErrorMessage(tr("Timeout waiting for ack")); + + switch (_currentOperation) { + case kCORead: + _currentOperation = kCOAck; + _sendTerminateCommand(); + break; + default: + _currentOperation = kCOIdle; + break; + } +} + +void QGCUASFileManager::_sendTerminateCommand(void) +{ + Request request; + request.hdr.magic = 'f'; + request.hdr.session = _activeSession; + request.hdr.opcode = kCmdTerminate; + _sendRequest(&request); } void QGCUASFileManager::_emitErrorMessage(const QString& msg) { - qDebug() << "QGCUASFileManager:" << msg; + qDebug() << "QGCUASFileManager: Error" << msg; emit errorMessage(msg); } +void QGCUASFileManager::_emitStatusMessage(const QString& msg) +{ + qDebug() << "QGCUASFileManager: Status" << msg; + emit statusMessage(msg); +} + /// @brief Sends the specified Request out to the UAS. void QGCUASFileManager::_sendRequest(Request* request) { diff --git a/src/uas/QGCUASFileManager.h b/src/uas/QGCUASFileManager.h index 5c41c0bd157b6eee4877ab51b46b2c6b197f8939..77cf33f503cac381d0734628e96db276058ebed4 100644 --- a/src/uas/QGCUASFileManager.h +++ b/src/uas/QGCUASFileManager.h @@ -25,6 +25,7 @@ #define QGCUASFILEMANAGER_H #include +#include #include "UASInterface.h" @@ -47,18 +48,18 @@ signals: public slots: void receiveMessage(LinkInterface* link, mavlink_message_t message); void nothingMessage(); - void listRecursively(const QString &from); - void downloadPath(const QString& from, const QString& to); + void listDirectory(const QString& dirPath); + void downloadPath(const QString& from, const QDir& downloadDir); protected: struct RequestHeader { - uint8_t magic; - uint8_t session; - uint8_t opcode; - uint8_t size; - uint32_t crc32; - uint32_t offset; + uint8_t magic; ///> Magic byte 'f' to idenitfy FTP protocol + uint8_t session; ///> Session id for read and write commands + uint8_t opcode; ///> Command opcode + uint8_t size; ///> Size of data + uint32_t crc32; ///> CRC for entire Request structure, with crc32 set to 0 + uint32_t offset; ///> Offsets for List and Read commands }; struct Request @@ -71,20 +72,23 @@ protected: enum Opcode { - kCmdNone, // ignored, always acked - kCmdTerminate, // releases sessionID, closes file - kCmdReset, // terminates all sessions - kCmdList, // list files in from - kCmdOpen, // opens for reading, returns - kCmdRead, // reads bytes from in - kCmdCreate, // creates for writing, returns - kCmdWrite, // appends bytes at in - kCmdRemove, // remove file (only if created by server?) - - kRspAck, - kRspNak, + // Commands + kCmdNone, ///> ignored, always acked + kCmdTerminate, ///> releases sessionID, closes file + kCmdReset, ///> terminates all sessions + kCmdList, ///> list files in from + kCmdOpen, ///> opens for reading, returns + kCmdRead, ///> reads bytes from in + kCmdCreate, ///> creates for writing, returns + kCmdWrite, ///> appends bytes at in + kCmdRemove, ///> remove file (only if created by server?) + + // Responses + kRspAck, ///> positive acknowledgement of previous command + kRspNak, ///> negative acknowledgement of previous command - kCmdTestNoAck, // ignored, ack not sent back, for testing only, should timeout waiting for ack + // Used for testing only, not part of protocol + kCmdTestNoAck, // ignored, ack not sent back, should timeout waiting for ack }; enum ErrorCode @@ -109,7 +113,9 @@ protected: { kCOIdle, // not doing anything kCOAck, // waiting for an Ack - kCOList, // waiting for a List response + kCOList, // waiting for List response + kCOOpen, // waiting for Open response + kCORead, // waiting for Read response }; @@ -121,11 +127,16 @@ protected: void _setupAckTimeout(void); void _clearAckTimeout(void); void _emitErrorMessage(const QString& msg); + void _emitStatusMessage(const QString& msg); void _sendRequest(Request* request); - - void sendList(); - void listDecode(const uint8_t *data, unsigned len); - + void _fillRequestWithString(Request* request, const QString& str); + void _openAckResponse(Request* openAck); + void _readAckResponse(Request* readAck); + void _listAckResponse(Request* listAck); + void _sendListCommand(void); + void _sendTerminateCommand(void); + void _closeReadSession(bool success); + static quint32 crc32(Request* request, unsigned state = 0); static QString errorString(uint8_t errorCode); @@ -136,8 +147,14 @@ protected: UASInterface* _mav; quint16 _encdata_seq; - unsigned _listOffset; // offset for the current List operation - QString _listPath; // path for the current List operation + unsigned _listOffset; ///> offset for the current List operation + QString _listPath; ///> path for the current List operation + + uint8_t _activeSession; ///> currently active session, 0 for none + uint32_t _readOffset; ///> current read offset + QByteArray _readFileAccumulator; ///> Holds file being downloaded + QDir _readFileDownloadDir; ///> Directory to download file to + QString _readFileDownloadFilename; ///> Filename (no path) for download file // We give MockMavlinkFileServer friend access so that it can use the data structures and opcodes // to build a mock mavlink file server for testing. diff --git a/src/ui/QGCUASFileView.cc b/src/ui/QGCUASFileView.cc index 09ccc3b04ee3792619a898c8c0e656bfae428e1c..55412e18f815ba17135efee732b3de831b6c125f 100644 --- a/src/ui/QGCUASFileView.cc +++ b/src/ui/QGCUASFileView.cc @@ -28,15 +28,15 @@ QGCUASFileView::~QGCUASFileView() void QGCUASFileView::listFiles() { - _manager->listRecursively(ui->pathLineEdit->text()); + _manager->listDirectory(ui->pathLineEdit->text()); } void QGCUASFileView::downloadFiles() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Directory"), + QString dir = QFileDialog::getExistingDirectory(this, tr("Download Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); // And now download to this location - _manager->downloadPath(ui->pathLineEdit->text(), dir); + _manager->downloadPath(ui->pathLineEdit->text(), QDir(dir)); }