diff --git a/qgroundcontrol.pro b/qgroundcontrol.pro index 192bc5545d0896ae19dcf7afc55d7f202695f3ed..f26f8053b49efff8fe6cad056adcdd092990cb17 100644 --- a/qgroundcontrol.pro +++ b/qgroundcontrol.pro @@ -186,22 +186,27 @@ DebugBuild { src/qgcunittest/MockUASManager.h \ src/qgcunittest/MockUAS.h \ src/qgcunittest/MockQGCUASParamManager.h \ + src/qgcunittest/MockMavlinkInterface.h \ + src/qgcunittest/MockMavlinkFileServer.h \ src/qgcunittest/MultiSignalSpy.h \ src/qgcunittest/FlightModeConfigTest.h \ src/qgcunittest/FlightGearTest.h \ src/qgcunittest/TCPLinkTest.h \ - src/qgcunittest/TCPLoopBackServer.h + src/qgcunittest/TCPLoopBackServer.h \ + src/qgcunittest/QGCUASFileManagerTest.h SOURCES += \ src/qgcunittest/UASUnitTest.cc \ src/qgcunittest/MockUASManager.cc \ src/qgcunittest/MockUAS.cc \ src/qgcunittest/MockQGCUASParamManager.cc \ + src/qgcunittest/MockMavlinkFileServer.cc \ src/qgcunittest/MultiSignalSpy.cc \ src/qgcunittest/FlightModeConfigTest.cc \ src/qgcunittest/FlightGearTest.cc \ src/qgcunittest/TCPLinkTest.cc \ - src/qgcunittest/TCPLoopBackServer.cc + src/qgcunittest/TCPLoopBackServer.cc \ + src/qgcunittest/QGCUASFileManagerTest.cc } # diff --git a/src/qgcunittest/MockMavlinkFileServer.cc b/src/qgcunittest/MockMavlinkFileServer.cc new file mode 100644 index 0000000000000000000000000000000000000000..b17aa898938fd306f6c3b7d436e24baf21cc0ea8 --- /dev/null +++ b/src/qgcunittest/MockMavlinkFileServer.cc @@ -0,0 +1,148 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "MockMavlinkFileServer.h" + +void MockMavlinkFileServer::sendMessage(mavlink_message_t message) +{ + QGCUASFileManager::Request ackResponse; + QString path; + + Q_ASSERT(message.msgid == MAVLINK_MSG_ID_ENCAPSULATED_DATA); + + mavlink_encapsulated_data_t requestEncapsulatedData; + mavlink_msg_encapsulated_data_decode(&message, &requestEncapsulatedData); + QGCUASFileManager::Request* request = (QGCUASFileManager::Request*)&requestEncapsulatedData.data[0]; + + // Validate CRC + if (request->hdr.crc32 != QGCUASFileManager::crc32(request)) { + _sendNak(QGCUASFileManager::kErrCrc); + } + + switch (request->hdr.opcode) { + case QGCUASFileManager::kCmdTestNoAck: + // ignored, ack not sent back, for testing only + break; + + case QGCUASFileManager::kCmdReset: + // terminates all sessions + // Fall through to send back Ack + + case QGCUASFileManager::kCmdNone: + // ignored, always acked + ackResponse.hdr.magic = 'f'; + ackResponse.hdr.opcode = QGCUASFileManager::kRspAck; + ackResponse.hdr.session = 0; + ackResponse.hdr.crc32 = 0; + ackResponse.hdr.size = 0; + _emitResponse(&ackResponse); + 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; + } + + _emitResponse(&ackResponse); + + 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 + case QGCUASFileManager::kCmdCreate: + // creates for writing, returns + case QGCUASFileManager::kCmdWrite: + // appends bytes at in + case QGCUASFileManager::kCmdRemove: + // remove file (only if created by server?) + default: + // nack for all NYI opcodes + _sendNak(QGCUASFileManager::kErrUnknownCommand); + break; + } +} + +void MockMavlinkFileServer::_sendNak(QGCUASFileManager::ErrorCode error) +{ + QGCUASFileManager::Request nakResponse; + + nakResponse.hdr.magic = 'f'; + nakResponse.hdr.opcode = QGCUASFileManager::kRspNak; + nakResponse.hdr.session = 0; + nakResponse.hdr.size = sizeof(nakResponse.data[0]); + nakResponse.data[0] = error; + + _emitResponse(&nakResponse); +} + +void MockMavlinkFileServer::_emitResponse(QGCUASFileManager::Request* request) +{ + mavlink_message_t mavlinkMessage; + + request->hdr.crc32 = QGCUASFileManager::crc32(request); + + mavlink_msg_encapsulated_data_pack(250, 0, &mavlinkMessage, 0 /*_encdata_seq*/, (uint8_t*)request); + + emit messageReceived(NULL, mavlinkMessage); +} \ No newline at end of file diff --git a/src/qgcunittest/MockMavlinkFileServer.h b/src/qgcunittest/MockMavlinkFileServer.h new file mode 100644 index 0000000000000000000000000000000000000000..0bccd3d4ab0915c27611210b6a686e3063904a68 --- /dev/null +++ b/src/qgcunittest/MockMavlinkFileServer.h @@ -0,0 +1,52 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef MOCKMAVLINKFILESERVER_H +#define MOCKMAVLINKFILESERVER_H + +#include "MockMavlinkInterface.h" +#include "QGCUASFileManager.h" + + +#include + +class MockMavlinkFileServer : public MockMavlinkInterface +{ + Q_OBJECT + +public: + MockMavlinkFileServer(void) { }; + + void setFileList(QStringList& fileList) { _fileList = fileList; } + + // From MockMavlinkInterface + virtual void sendMessage(mavlink_message_t message); + +private: + void _sendNak(QGCUASFileManager::ErrorCode error); + void _emitResponse(QGCUASFileManager::Request* request); + + QStringList _fileList; +}; + +#endif diff --git a/src/qgcunittest/MockMavlinkInterface.cc b/src/qgcunittest/MockMavlinkInterface.cc new file mode 100644 index 0000000000000000000000000000000000000000..d9313fa90294e5419604bc179243eb52709dc209 --- /dev/null +++ b/src/qgcunittest/MockMavlinkInterface.cc @@ -0,0 +1,9 @@ +// +// MockMavlinkInterface.cc +// QGroundControl +// +// Created by Donald Gagne on 6/19/14. +// Copyright (c) 2014 Donald Gagne. All rights reserved. +// + +#include "MockMavlinkInterface.h" diff --git a/src/qgcunittest/MockMavlinkInterface.h b/src/qgcunittest/MockMavlinkInterface.h new file mode 100644 index 0000000000000000000000000000000000000000..5aaab3d6a18809abaeb89fef3e8c0a8779118f2d --- /dev/null +++ b/src/qgcunittest/MockMavlinkInterface.h @@ -0,0 +1,44 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include + +#include "QGCMAVLink.h" +#include "LinkInterface.h" + +#ifndef MOCKMAVLINKINTERFACE_H +#define MOCKMAVLINKINTERFACE_H + +class MockMavlinkInterface : public QObject +{ + Q_OBJECT + +public: + virtual void sendMessage(mavlink_message_t message) = 0; + +signals: + // link argument will always be NULL + void messageReceived(LinkInterface* link, mavlink_message_t message); +}; + +#endif diff --git a/src/qgcunittest/MockUAS.cc b/src/qgcunittest/MockUAS.cc index ff3688e40ac100c529c023f6081e871fb4d9358e..443bb932c4383467f9c16565a7d576a0b667d4bb 100644 --- a/src/qgcunittest/MockUAS.cc +++ b/src/qgcunittest/MockUAS.cc @@ -27,7 +27,8 @@ QString MockUAS::_bogusStaticString; MockUAS::MockUAS(void) : _systemType(MAV_TYPE_QUADROTOR), - _systemId(1) + _systemId(1), + _mavlinkPlugin(NULL) { } @@ -41,4 +42,15 @@ void MockUAS::setMockParametersAndSignal(MockQGCUASParamManager::ParamMap_t& map i.next(); emit parameterChanged(_systemId, 0, i.key(), i.value()); } +} + +void MockUAS::sendMessage(mavlink_message_t message) +{ + Q_UNUSED(link); + + if (!_mavlinkPlugin) { + Q_ASSERT(false); + } + + _mavlinkPlugin->sendMessage(message); } \ No newline at end of file diff --git a/src/qgcunittest/MockUAS.h b/src/qgcunittest/MockUAS.h index 3c715c084312c921ed0923484170ed9ec6840ef6..5b3a529982517314a5dc18c861d2eb544cab6a4b 100644 --- a/src/qgcunittest/MockUAS.h +++ b/src/qgcunittest/MockUAS.h @@ -26,6 +26,7 @@ #include "UASInterface.h" #include "MockQGCUASParamManager.h" +#include "MockMavlinkInterface.h" #include @@ -50,6 +51,9 @@ public: virtual int getUASID(void) const { return _systemId; } virtual QGCUASParamManagerInterface* getParamManager() { return &_paramManager; }; + // sendMessage is only supported if a mavlink plugin is installed. + virtual void sendMessage(mavlink_message_t message); + public: // MockUAS methods MockUAS(void); @@ -63,12 +67,15 @@ public: /// allows you to simulate parameter input and validate parameter setting MockQGCUASParamManager* getMockQGCUASParamManager(void) { return &_paramManager; } - /// Sets the parameter map into the mock QGCUASParamManager and signals parameterChanged for + /// @brief Sets the parameter map into the mock QGCUASParamManager and signals parameterChanged for /// each param void setMockParametersAndSignal(MockQGCUASParamManager::ParamMap_t& map); void emitRemoteControlChannelRawChanged(int channel, float raw) { emit remoteControlChannelRawChanged(channel, raw); } + /// @brief Installs a mavlink plugin. Only a single mavlink plugin is supported at a time. + void setMockMavlinkPlugin(MockMavlinkInterface* mavlinkPlugin) { _mavlinkPlugin = mavlinkPlugin; }; + public: // Unimplemented UASInterface overrides virtual QString getUASName() const { Q_ASSERT(false); return _bogusString; }; @@ -112,9 +119,7 @@ public: virtual QGCUASFileManager* getFileManager() {Q_ASSERT(false); return NULL; } /** @brief Send a message over this link (to this or to all UAS on this link) */ - virtual void sendMessage(LinkInterface* link, mavlink_message_t message){Q_ASSERT(false);} - /** @brief Send a message over all links this UAS can be reached with (!= all links) */ - virtual void sendMessage(mavlink_message_t message) {Q_ASSERT(false);} + virtual void sendMessage(LinkInterface* link, mavlink_message_t message){ Q_UNUSED(link); Q_UNUSED(message); Q_ASSERT(false); } virtual QString getAutopilotTypeName() { Q_ASSERT(false); return _bogusString; }; virtual void setAutopilotType(int apType) { Q_UNUSED(apType); Q_ASSERT(false); }; virtual QMap getComponents() { Q_ASSERT(false); return _bogusMapIntQString; }; @@ -182,6 +187,8 @@ private: int _systemId; MockQGCUASParamManager _paramManager; + + MockMavlinkInterface* _mavlinkPlugin; ///< Mock Mavlink plugin, NULL for none // Bogus variables used for return types of NYI methods QString _bogusString; diff --git a/src/qgcunittest/QGCUASFileManagerTest.cc b/src/qgcunittest/QGCUASFileManagerTest.cc new file mode 100644 index 0000000000000000000000000000000000000000..fe325de78f24eb24077c47a4bf3f320a5317034d --- /dev/null +++ b/src/qgcunittest/QGCUASFileManagerTest.cc @@ -0,0 +1,156 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "QGCUASFileManagerTest.h" + +/// @file +/// @brief QGCUASFileManager unit test +/// +/// @author Don Gagne + +QGCUASFileManagerUnitTest::QGCUASFileManagerUnitTest(void) : + _fileManager(NULL), + _multiSpy(NULL) +{ + +} + +// Called once before all test cases are run +void QGCUASFileManagerUnitTest::initTestCase(void) +{ + _mockUAS.setMockMavlinkPlugin(&_mockFileServer); +} + +// Called before every test case +void QGCUASFileManagerUnitTest::init(void) +{ + Q_ASSERT(_multiSpy == NULL); + + _fileManager = new QGCUASFileManager(NULL, &_mockUAS); + Q_CHECK_PTR(_fileManager); + + bool connected = connect(&_mockFileServer, SIGNAL(messageReceived(LinkInterface*, mavlink_message_t)), _fileManager, SLOT(receiveMessage(LinkInterface*, mavlink_message_t))); + Q_ASSERT(connected); + + connected = connect(_fileManager, SIGNAL(statusMessage(const QString&)), this, SLOT(statusMessage(const QString&))); + Q_ASSERT(connected); + + _rgSignals[statusMessageSignalIndex] = SIGNAL(statusMessage(const QString&)); + _rgSignals[errorMessageSignalIndex] = SIGNAL(errorMessage(const QString&)); + _rgSignals[resetStatusMessagesSignalIndex] = SIGNAL(resetStatusMessages(void)); + + _multiSpy = new MultiSignalSpy(); + Q_CHECK_PTR(_multiSpy); + QCOMPARE(_multiSpy->init(_fileManager, _rgSignals, _cSignals), true); +} + +// Called after every test case +void QGCUASFileManagerUnitTest::cleanup(void) +{ + Q_ASSERT(_multiSpy); + Q_ASSERT(_fileManager); + + delete _fileManager; + delete _multiSpy; + + _fileManager = NULL; + _multiSpy = NULL; +} + +/// @brief Connected to QGCUASFileManager statusMessage signal in order to catch list command output +void QGCUASFileManagerUnitTest::statusMessage(const QString& msg) +{ + // Keep a list of all names received so we can test it for correctness + _fileListReceived += msg; +} + + +void QGCUASFileManagerUnitTest::_ackTest(void) +{ + Q_ASSERT(_fileManager); + Q_ASSERT(_multiSpy); + Q_ASSERT(_multiSpy->checkNoSignals() == true); + + // If the file manager doesn't receive an ack it will timeout and emit an error. So make sure + // we don't get any error signals. + QVERIFY(_fileManager->_sendCmdTestAck()); + QVERIFY(_multiSpy->checkNoSignals()); +} + +void QGCUASFileManagerUnitTest::_noAckTest(void) +{ + Q_ASSERT(_fileManager); + Q_ASSERT(_multiSpy); + Q_ASSERT(_multiSpy->checkNoSignals() == true); + + // This should not get the ack back and timeout. + QVERIFY(_fileManager->_sendCmdTestNoAck()); + QTest::qWait(2000); // Let the file manager timeout, magic number 2 secs must be larger than file manager ack timeout + QCOMPARE(_multiSpy->checkOnlySignalByMask(errorMessageSignalMask), true); +} + +void QGCUASFileManagerUnitTest::_resetTest(void) +{ + Q_ASSERT(_fileManager); + Q_ASSERT(_multiSpy); + Q_ASSERT(_multiSpy->checkNoSignals() == true); + + // Send a reset command + // We should not get any signals back from this + QVERIFY(_fileManager->_sendCmdReset()); + QVERIFY(_multiSpy->checkNoSignals()); +} + +void QGCUASFileManagerUnitTest::_listTest(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->listRecursively("/bogus"); + QCOMPARE(_multiSpy->checkOnlySignalByMask(errorMessageSignalMask | resetStatusMessagesSignalMask), true); + _multiSpy->clearAllSignals(); + + // Send a list command at the root of the directory tree + // We should get back a single resetStatusMessages signal + // We should not get back an errorMessage signal + // We should get back one or more statusMessage signals + // The returned list should match out inputs + + QStringList fileList; + fileList << "Ddir" << "Ffoo" << "Fbar"; + _mockFileServer.setFileList(fileList); + + QStringList fileListExpected; + fileListExpected << "dir/" << "foo" << "bar"; + + _fileListReceived.clear(); + + _fileManager->listRecursively("/"); + 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); +} diff --git a/src/qgcunittest/QGCUASFileManagerTest.h b/src/qgcunittest/QGCUASFileManagerTest.h new file mode 100644 index 0000000000000000000000000000000000000000..2a76fbb5d4bc4cfcd71e9645bb4e6671763ccbc5 --- /dev/null +++ b/src/qgcunittest/QGCUASFileManagerTest.h @@ -0,0 +1,92 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef QGCUASFILEMANAGERTEST_H +#define QGCUASFILEMANAGERTEST_H + +#include +#include + +#include "AutoTest.h" +#include "MockUAS.h" +#include "MockMavlinkFileServer.h" +#include "QGCUASFileManager.h" +#include "MultiSignalSpy.h" + +/// @file +/// @brief QGCUASFileManager unit test +/// +/// @author Don Gagne + +class QGCUASFileManagerUnitTest : public QObject +{ + Q_OBJECT + +public: + QGCUASFileManagerUnitTest(void); + +private slots: + // Test case initialization + void initTestCase(void); + void init(void); + void cleanup(void); + + // Test cases + void _ackTest(void); + void _noAckTest(void); + void _resetTest(void); + void _listTest(void); + + // Connected to QGCUASFileManager statusMessage signal + void statusMessage(const QString&); + +private: + enum { + statusMessageSignalIndex = 0, + errorMessageSignalIndex, + resetStatusMessagesSignalIndex, + + maxSignalIndex + }; + + enum { + statusMessageSignalMask = 1 << statusMessageSignalIndex, + errorMessageSignalMask = 1 << errorMessageSignalIndex, + resetStatusMessagesSignalMask = 1 << resetStatusMessagesSignalIndex, + }; + + MockUAS _mockUAS; + MockMavlinkFileServer _mockFileServer; + + QGCUASFileManager* _fileManager; + + MultiSignalSpy* _multiSpy; + static const size_t _cSignals = maxSignalIndex; + const char* _rgSignals[_cSignals]; + + QStringList _fileListReceived; +}; + +DECLARE_TEST(QGCUASFileManagerUnitTest) + +#endif diff --git a/src/uas/QGCUASFileManager.cc b/src/uas/QGCUASFileManager.cc index f03f84f23410992da9b8e31d8bce24b4b026eb46..5b48b305908c03c119a2adf73999fb674bea903b 100644 --- a/src/uas/QGCUASFileManager.cc +++ b/src/uas/QGCUASFileManager.cc @@ -1,3 +1,26 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + #include "QGCUASFileManager.h" #include "QGC.h" #include "MAVLinkProtocol.h" @@ -44,86 +67,106 @@ static const quint32 crctab[] = QGCUASFileManager::QGCUASFileManager(QObject* parent, UASInterface* uas) : QObject(parent), + _currentOperation(kCOIdle), _mav(uas), _encdata_seq(0) { + bool connected = connect(&_ackTimer, SIGNAL(timeout()), this, SLOT(_ackTimeout())); + Q_ASSERT(connected); } -quint32 -QGCUASFileManager::crc32(const uint8_t *src, unsigned len, unsigned state) +/// @brief Calculates a 32 bit CRC for the specified request. +/// @param request Request to calculate CRC for. request->size must be set correctly. +/// @param state previous crc state +/// @return Calculated CRC +quint32 QGCUASFileManager::crc32(Request* request, unsigned state) { - for (unsigned i = 0; i < len; i++) - state = crctab[(state ^ src[i]) & 0xff] ^ (state >> 8); + uint8_t* data = (uint8_t*)request; + size_t cbData = sizeof(RequestHeader) + request->hdr.size; + + // Always calculate CRC with 0 initial CRC value + quint32 crcSave = request->hdr.crc32; + request->hdr.crc32 = 0; + + for (size_t i=0; i < cbData; i++) + state = crctab[(state ^ data[i]) & 0xff] ^ (state >> 8); + + request->hdr.crc32 = crcSave; + return state; } void QGCUASFileManager::nothingMessage() { - mavlink_message_t message; - - RequestHeader hdr; - hdr.magic = 'f'; - hdr.session = 0; - hdr.crc32 = 0; - hdr.size = 0; - hdr.crc32 = crc32((uint8_t*)&hdr, sizeof(hdr) + hdr.size, 0); - - mavlink_msg_encapsulated_data_pack(250, 0, &message, _encdata_seq, (uint8_t*)&hdr); - - _mav->sendMessage(message); - qDebug() << "Sent message!"; + // FIXME: Connect ui correctly } void QGCUASFileManager::receiveMessage(LinkInterface* link, mavlink_message_t message) { Q_UNUSED(link); - + if (message.msgid != MAVLINK_MSG_ID_ENCAPSULATED_DATA) { // wtf, not for us return; } - - emit statusMessage("msg"); - qDebug() << "FTP GOT MESSAGE"; - + mavlink_encapsulated_data_t data; mavlink_msg_encapsulated_data_decode(&message, &data); - const RequestHeader *hdr = (const RequestHeader *)&data.data[0]; - unsigned seqnr = data.seqnr; - - switch (_current_operation) { - case kCOIdle: - // we should not be seeing anything here.. shut the other guy up - emit statusMessage("resetting file transfer session"); - sendReset(); - break; - - case kCOList: - if (hdr->opcode == kRspAck) { - listDecode(&hdr->data[0], hdr->size); - } else { - emit statusMessage("unexpected opcode in List mode"); + 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); + break; + + default: + _emitErrorMessage("Ack received in unexpected state"); + break; } - break; - - default: - emit statusMessage("message in unexpected state"); + } else if (request->hdr.opcode == kRspNak) { + _clearAckTimeout(); + _emitErrorMessage(QString("Nak received, error: ").append(errorString(request->data[0]))); + _currentOperation = kCOIdle; + } 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)); } } void QGCUASFileManager::listRecursively(const QString &from) { - if (_current_operation != kCOIdle) { - // XXX beep and don't do anything + if (_currentOperation != kCOIdle) { + _emitErrorMessage(tr("Command not sent. Waiting for previous command to complete.")); + return; } // clear the text widget emit resetStatusMessages(); // initialise the lister - _list_path = from; - _list_offset = 0; - _current_operation = kCOList; + _listPath = from; + _listOffset = 0; + _currentOperation = kCOList; // and send the initial request sendList(); @@ -136,15 +179,22 @@ void QGCUASFileManager::listDecode(const uint8_t *data, unsigned len) // 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((const char *)data + offset, len - offset); - if (nlen == 0) { + 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(QString((const char *)data + offset)); + emit statusMessage(s); // account for the name + NUL offset += nlen + 1; @@ -155,59 +205,35 @@ void QGCUASFileManager::listDecode(const uint8_t *data, unsigned len) // we have run out of files to list if (files == 0) { - _current_operation = kCOIdle; + _currentOperation = kCOIdle; } else { // update our state - _list_offset += files; + _listOffset += files; // and send another request sendList(); } } -void QGCUASFileManager::sendReset() -{ - mavlink_message_t message; - - RequestHeader hdr; - hdr.magic = 'f'; - hdr.session = 0; - hdr.opcode = kCmdReset; - hdr.crc32 = 0; - hdr.offset = 0; - hdr.size = 0; - hdr.crc32 = crc32((uint8_t*)&hdr, sizeof(hdr) + hdr.size, 0); - - mavlink_msg_encapsulated_data_pack(250, 0, &message, _encdata_seq, (uint8_t*)&hdr); // XXX 250 is a magic length - - _mav->sendMessage(message); -} - void QGCUASFileManager::sendList() { - mavlink_message_t message; - - RequestHeader hdr; - hdr.magic = 'f'; - hdr.session = 0; - hdr.opcode = kCmdList; - hdr.crc32 = 0; - hdr.offset = _list_offset; - strncpy((char *)&hdr.data[0], _list_path.toStdString().c_str(), 200); // XXX should have a real limit here - hdr.size = strlen((const char *)&hdr.data[0]); - - hdr.crc32 = crc32((uint8_t*)&hdr, sizeof(hdr) + hdr.size, 0); - - mavlink_msg_encapsulated_data_pack(250, 0, &message, _encdata_seq, (uint8_t*)&hdr); // XXX 250 is a magic length - - emit statusMessage("sending List request..."); - - _mav->sendMessage(message); + Request request; + + request.hdr.magic = 'f'; + request.hdr.session = 0; + 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)); + + _sendRequest(&request); } void QGCUASFileManager::downloadPath(const QString &from, const QString &to) { - + Q_UNUSED(from); + // Send path, e.g. /fs/microsd and download content // recursively into a local directory @@ -225,3 +251,110 @@ void QGCUASFileManager::downloadPath(const QString &from, const QString &to) emit statusMessage(QString("Downloaded: %1 to directory %2").arg(filename).arg(to)); } + +QString QGCUASFileManager::errorString(uint8_t errorCode) +{ + switch(errorCode) { + case kErrNone: + return QString("no error"); + case kErrNoRequest: + return QString("bad request"); + case kErrNoSession: + return QString("bad session"); + case kErrSequence: + return QString("bad sequence number"); + case kErrNotDir: + return QString("not a directory"); + case kErrNotFile: + return QString("not a file"); + case kErrEOF: + return QString("read beyond end of file"); + case kErrNotAppend: + return QString("write not at end of file"); + case kErrTooBig: + return QString("file too big"); + case kErrIO: + return QString("device I/O error"); + case kErrPerm: + return QString("permission denied"); + case kErrUnknownCommand: + return QString("unknown command"); + case kErrCrc: + return QString("bad crc"); + default: + return QString("unknown error code"); + } +} + +/// @brief Sends a command which only requires an opcode and no additional data +/// @param opcode Opcode to send +/// @param newOpState State to put state machine into +/// @return TRUE: command sent, FALSE: command not sent, waiting for previous command to finish +bool QGCUASFileManager::_sendOpcodeOnlyCmd(uint8_t opcode, OperationState newOpState) +{ + if (_currentOperation != kCOIdle) { + // Can't have multiple commands in play at the same time + return false; + } + + Request request; + request.hdr.magic = 'f'; + request.hdr.session = 0; + request.hdr.opcode = opcode; + request.hdr.offset = 0; + request.hdr.size = 0; + + _currentOperation = newOpState; + _setupAckTimeout(); + + _sendRequest(&request); + + return TRUE; +} + +/// @brief Starts the ack timeout timer +void QGCUASFileManager::_setupAckTimeout(void) +{ + qDebug() << "Setting Ack"; + + Q_ASSERT(!_ackTimer.isActive()); + + _ackTimer.setSingleShot(true); + _ackTimer.start(_ackTimerTimeoutMsecs); +} + +/// @brief Clears the ack timeout timer +void QGCUASFileManager::_clearAckTimeout(void) +{ + qDebug() << "Clearing Ack"; + + Q_ASSERT(_ackTimer.isActive()); + + _ackTimer.stop(); +} + +/// @brief Called when ack timeout timer fires +void QGCUASFileManager::_ackTimeout(void) +{ + _currentOperation = kCOIdle; + _emitErrorMessage(tr("Timeout waiting for ack")); +} + +void QGCUASFileManager::_emitErrorMessage(const QString& msg) +{ + qDebug() << "QGCUASFileManager:" << msg; + emit errorMessage(msg); +} + +/// @brief Sends the specified Request out to the UAS. +void QGCUASFileManager::_sendRequest(Request* request) +{ + mavlink_message_t message; + + _setupAckTimeout(); + + request->hdr.crc32 = crc32(request); + // FIXME: Send correct system id instead of harcoded 250 + mavlink_msg_encapsulated_data_pack(250, 0, &message, _encdata_seq, (uint8_t*)request); + _mav->sendMessage(message); +} \ No newline at end of file diff --git a/src/uas/QGCUASFileManager.h b/src/uas/QGCUASFileManager.h index 9b88f93c9ba15b35fc71b86779294102b56242ce..5c41c0bd157b6eee4877ab51b46b2c6b197f8939 100644 --- a/src/uas/QGCUASFileManager.h +++ b/src/uas/QGCUASFileManager.h @@ -1,7 +1,31 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + #ifndef QGCUASFILEMANAGER_H #define QGCUASFILEMANAGER_H #include + #include "UASInterface.h" class QGCUASFileManager : public QObject @@ -9,17 +33,22 @@ class QGCUASFileManager : public QObject Q_OBJECT public: QGCUASFileManager(QObject* parent, UASInterface* uas); + + /// These methods are only used for testing purposes. + bool _sendCmdTestAck(void) { return _sendOpcodeOnlyCmd(kCmdNone, kCOAck); }; + bool _sendCmdTestNoAck(void) { return _sendOpcodeOnlyCmd(kCmdTestNoAck, kCOAck); }; + bool _sendCmdReset(void) { return _sendOpcodeOnlyCmd(kCmdReset, kCOAck); }; signals: void statusMessage(const QString& msg); void resetStatusMessages(); + void errorMessage(const QString& ms); 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 timedOut(); protected: struct RequestHeader @@ -30,23 +59,32 @@ protected: uint8_t size; uint32_t crc32; uint32_t offset; - uint8_t data[]; }; + struct Request + { + struct RequestHeader hdr; + // The entire Request must fit into the data member of the mavlink_encapsulated_data_t structure. We use as many leftover bytes + // after we use up space for the RequestHeader for the data portion of the Request. + uint8_t data[sizeof(((mavlink_encapsulated_data_t*)0)->data) - sizeof(RequestHeader)]; + }; + enum Opcode { - kCmdNone, // ignored, always acked + 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?) + 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 + kRspNak, + + kCmdTestNoAck, // ignored, ack not sent back, for testing only, should timeout waiting for ack }; enum ErrorCode @@ -61,35 +99,49 @@ protected: kErrNotAppend, kErrTooBig, kErrIO, - kErrPerm + kErrPerm, + kErrUnknownCommand, + kErrCrc }; enum OperationState { kCOIdle, // not doing anything - + kCOAck, // waiting for an Ack kCOList, // waiting for a List response }; - - OperationState _current_operation; - unsigned _retry_counter; - - UASInterface* _mav; - quint16 _encdata_seq; - - unsigned _session_id; // session ID for current session - unsigned _list_offset; // offset for the current List operation - QString _list_path; // path for the current List operation - - void sendTerminate(); - void sendReset(); + + +protected slots: + void _ackTimeout(void); + +protected: + bool _sendOpcodeOnlyCmd(uint8_t opcode, OperationState newOpState); + void _setupAckTimeout(void); + void _clearAckTimeout(void); + void _emitErrorMessage(const QString& msg); + void _sendRequest(Request* request); void sendList(); void listDecode(const uint8_t *data, unsigned len); - static quint32 crc32(const uint8_t *src, unsigned len, unsigned state); + static quint32 crc32(Request* request, unsigned state = 0); + static QString errorString(uint8_t errorCode); + + OperationState _currentOperation; ///> Current operation of state machine + QTimer _ackTimer; ///> Used to signal a timeout waiting for an ack + static const int _ackTimerTimeoutMsecs = 1000; ///> Timeout in msecs for ack timer + + UASInterface* _mav; + quint16 _encdata_seq; + unsigned _listOffset; // offset for the current List operation + QString _listPath; // path for the current List operation + + // We give MockMavlinkFileServer friend access so that it can use the data structures and opcodes + // to build a mock mavlink file server for testing. + friend class MockMavlinkFileServer; }; #endif // QGCUASFILEMANAGER_H diff --git a/src/uas/UAS.cc b/src/uas/UAS.cc index 02259d84d7c0a56edc81cf9200ad2549c98b985d..f26acea0be38f773943d68a82747a6667f48001b 100644 --- a/src/uas/UAS.cc +++ b/src/uas/UAS.cc @@ -149,6 +149,8 @@ UAS::UAS(MAVLinkProtocol* protocol, QThread* thread, int id) : UASInterface(), pitch(0.0), yaw(0.0), + imagePackets(0), // We must initialize to 0, otherwise extended data packets maybe incorrectly thought to be images + blockHomePositionChanges(false), receivedMode(false), @@ -1371,6 +1373,7 @@ void UAS::receiveMessage(LinkInterface* link, mavlink_message_t message) // NO VALID TRANSACTION - ABORT // Restart statemachine imagePacketsArrived = 0; + break; } for (int i = 0; i < imagePayload; ++i) @@ -1387,6 +1390,8 @@ void UAS::receiveMessage(LinkInterface* link, mavlink_message_t message) if ((imagePacketsArrived >= imagePackets)) { // Restart statemachine + imagePackets = 0; + imagePacketsArrived = 0; emit imageReady(this); //qDebug() << "imageReady emitted. all packets arrived"; } diff --git a/src/ui/MainWindow.cc b/src/ui/MainWindow.cc index be1294afbfcb0802f6edfeaa05fe1df23809b6c1..5d89ce13c94d26d467cf0179a0570d8b8a10441f 100644 --- a/src/ui/MainWindow.cc +++ b/src/ui/MainWindow.cc @@ -820,6 +820,10 @@ void MainWindow::loadDockWidget(const QString& name) { createDockWidget(centerStack->currentWidget(),new ParameterInterface(this),tr("Onboard Parameters"),"PARAMETER_INTERFACE_DOCKWIDGET",currentView,Qt::RightDockWidgetArea); } + else if (name == "FILE_VIEW_DOCKWIDGET") + { + createDockWidget(centerStack->currentWidget(),new QGCUASFileViewMulti(this),tr("Onboard Files"),"FILE_VIEW_DOCKWIDGET",VIEW_ENGINEER,Qt::RightDockWidgetArea); + } else if (name == "UAS_STATUS_DETAILS_DOCKWIDGET") { createDockWidget(centerStack->currentWidget(),new UASInfoWidget(this),tr("Status Details"),"UAS_STATUS_DETAILS_DOCKWIDGET",currentView,Qt::RightDockWidgetArea); diff --git a/src/ui/QGCUASFileView.cc b/src/ui/QGCUASFileView.cc index 3d525cc05a003563c24c92d6dd4b547e0858c8d1..09ccc3b04ee3792619a898c8c0e656bfae428e1c 100644 --- a/src/ui/QGCUASFileView.cc +++ b/src/ui/QGCUASFileView.cc @@ -17,6 +17,7 @@ QGCUASFileView::QGCUASFileView(QWidget *parent, QGCUASFileManager *manager) : connect(ui->downloadButton, SIGNAL(clicked()), this, SLOT(downloadFiles())); connect(_manager, SIGNAL(statusMessage(QString)), ui->messageArea, SLOT(appendPlainText(QString))); + connect(_manager, SIGNAL(errorMessage(QString)), ui->messageArea, SLOT(appendPlainText(QString))); connect(_manager, SIGNAL(resetStatusMessages()), ui->messageArea, SLOT(clear())); }