Commit 2b88c6e9 authored by Lorenz Meier's avatar Lorenz Meier

Merge pull request #733 from DonLakeFlyer/mavlink-ftp

Mavlink FTP: List and Download commands working end to end
parents e93fd779 70c5ce73
......@@ -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; i<cFileTestCases; i++) {
if (path == rgFileTestCases[i].filename) {
found = true;
_readFileLength = rgFileTestCases[i].length;
break;
}
}
if (!found) {
_sendNak(QGCUASFileManager::kErrNotFile);
return;
}
response.hdr.magic = 'f';
response.hdr.opcode = QGCUASFileManager::kRspAck;
response.hdr.session = _sessionId;
response.hdr.size = 0;
_emitResponse(&response);
}
/// @brief Handles Read command requests.
void MockMavlinkFileServer::_readCommand(QGCUASFileManager::Request* request)
{
QGCUASFileManager::Request response;
if (request->hdr.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 <path> for reading, returns <session>
case QGCUASFileManager::kCmdRead:
// reads <size> bytes from <offset> in <session>
_terminateCommand(request);
break;
// Remainder of commands are NYI
case QGCUASFileManager::kCmdCreate:
// creates <path> for writing, returns <session>
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;
......
......@@ -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 <don@thegagnes.com>
#include <QStringList>
......@@ -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
......@@ -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);
}
......
......@@ -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 <don@thegagnes.com>
......@@ -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; i<bytes.length(); i++) {
QCOMPARE((uint8_t)bytes[i], (uint8_t)((i-1) & 0xFF));
}
}
void QGCUASFileManagerUnitTest::_openTest(void)
{
Q_ASSERT(_fileManager);
Q_ASSERT(_multiSpy);
Q_ASSERT(_multiSpy->checkNoSignals() == 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; i<MockMavlinkFileServer::cFileTestCases; i++) {
QString filePath = QDir::temp().absoluteFilePath(MockMavlinkFileServer::rgFileTestCases[i].filename);
if (QFile::exists(filePath)) {
Q_ASSERT(QFile::remove(filePath));
}
}
// Run through the set of file test cases
// We setup a spy on the terminate command signal so that we can determine that a Terminate command was
// correctly sent after the Open/Read commands complete.
QSignalSpy terminateSpy(&_mockFileServer, SIGNAL(terminateCommandReceived()));
for (size_t i=0; i<MockMavlinkFileServer::cFileTestCases; i++) {
_fileManager->downloadPath(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);
}
}
......@@ -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,
......
This diff is collapsed.
......@@ -25,6 +25,7 @@
#define QGCUASFILEMANAGER_H
#include <QObject>
#include <QDir>
#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 <path> from <offset>
kCmdOpen, // opens <path> for reading, returns <session>
kCmdRead, // reads <size> bytes from <offset> in <session>
kCmdCreate, // creates <path> for writing, returns <session>
kCmdWrite, // appends <size> bytes at <offset> in <session>
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 <path> from <offset>
kCmdOpen, ///> opens <path> for reading, returns <session>
kCmdRead, ///> reads <size> bytes from <offset> in <session>
kCmdCreate, ///> creates <path> for writing, returns <session>
kCmdWrite, ///> appends <size> bytes at <offset> in <session>
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.
......
......@@ -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));
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment