Files
2025-01-16 19:57:23 -05:00

414 lines
19 KiB
C++

/****************************************************************************
*
* (c) 2009-2020 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
*
* QGroundControl is licensed according to the terms in the file
* COPYING.md in the root of the source code directory.
*
****************************************************************************/
#include "LandingComplexItemTest.h"
#include "CameraSectionTest.h"
#include "JsonHelper.h"
#include "PlanMasterController.h"
#include "MultiSignalSpy.h"
#include "CameraSection.h"
#include "SimpleMissionItem.h"
#include <QtTest/QTest>
const char* SimpleLandingComplexItem::settingsGroup = "SimpleLandingComplexItemUnitTest";
const char* SimpleLandingComplexItem::jsonComplexItemTypeValue = "utSimpleLandingPattern";
LandingComplexItemTest::LandingComplexItemTest(void)
{
rgSignals[finalApproachCoordinateChangedIndex] = SIGNAL(finalApproachCoordinateChanged(QGeoCoordinate));
rgSignals[loiterTangentCoordinateChangedIndex] = SIGNAL(loiterTangentCoordinateChanged(QGeoCoordinate));
rgSignals[landingCoordinateChangedIndex] = SIGNAL(landingCoordinateChanged(QGeoCoordinate));
rgSignals[landingCoordSetChangedIndex] = SIGNAL(landingCoordSetChanged(bool));
rgSignals[altitudesAreRelativeChangedIndex] = SIGNAL(altitudesAreRelativeChanged(bool));
rgSignals[_updateFlightPathSegmentsSignalIndex] = SIGNAL(_updateFlightPathSegmentsSignal());
}
void LandingComplexItemTest::init(void)
{
VisualMissionItemTest::init();
_item = new SimpleLandingComplexItem(_masterController, false /* flyView */);
// Start in a clean state
QVERIFY(!_item->dirty());
_item->setLandingCoordinate(QGeoCoordinate(47, -122));
_item->setDirty(false);
QVERIFY(!_item->dirty());
VisualMissionItemTest::_createSpy(_item, &_viMultiSpy);
_multiSpy = new MultiSignalSpy();
QCOMPARE(_multiSpy->init(_item, rgSignals, cSignals), true);
_validStopVideoItem = CameraSectionTest::createValidStopTimeItem(_masterController);
_validStopDistanceItem = CameraSectionTest::createValidStopTimeItem(_masterController);
_validStopTimeItem = CameraSectionTest::createValidStopTimeItem(_masterController);
}
void LandingComplexItemTest::cleanup(void)
{
delete _multiSpy;
_multiSpy = nullptr;
VisualMissionItemTest::cleanup();
// These items go away when _masterController is deleted
_item = nullptr;
_validStopVideoItem = nullptr;
_validStopDistanceItem = nullptr;
_validStopTimeItem = nullptr;
}
void LandingComplexItemTest::_testDirty(void)
{
QVERIFY(!_item->dirty());
_item->setDirty(true);
QVERIFY(_item->dirty());
QVERIFY(_viMultiSpy->checkOnlySignalByMask(dirtyChangedMask));
QVERIFY(_viMultiSpy->pullBoolFromSignalIndex(dirtyChangedIndex));
_item->setDirty(false);
_viMultiSpy->clearAllSignals();
// These facts should set dirty when changed
QList<Fact*> rgFacts;
rgFacts << _item->finalApproachAltitude()
<< _item->useDoChangeSpeed()
<< _item->finalApproachSpeed()
<< _item->landingHeading()
<< _item->loiterRadius()
<< _item->loiterClockwise()
<< _item->landingAltitude()
<< _item->landingDistance()
<< _item->useLoiterToAlt()
<< _item->stopTakingPhotos()
<< _item->stopTakingVideo();
for(Fact* fact: rgFacts) {
qDebug() << fact->name();
QVERIFY(!_item->dirty());
changeFactValue(fact);
QVERIFY(_viMultiSpy->checkSignalByMask(dirtyChangedMask));
QVERIFY(_viMultiSpy->pullBoolFromSignalIndex(dirtyChangedIndex));
_item->setDirty(false);
_viMultiSpy->clearAllSignals();
}
// These bool properties should set dirty when changed
QList<const char*> rgBoolNames;
rgBoolNames << "altitudesAreRelative";
const QMetaObject* metaObject = _item->metaObject();
for(const char* boolName: rgBoolNames) {
qDebug() << boolName;
QVERIFY(!_item->dirty());
QMetaProperty boolProp = metaObject->property(metaObject->indexOfProperty(boolName));
QVERIFY(boolProp.write(_item, !boolProp.read(_item).toBool()));
QVERIFY(_viMultiSpy->checkSignalByMask(dirtyChangedMask));
QVERIFY(_viMultiSpy->pullBoolFromSignalIndex(dirtyChangedIndex));
_item->setDirty(false);
_viMultiSpy->clearAllSignals();
}
// These coordinates should set dirty when changed
QVERIFY(!_item->dirty());
_item->setFinalApproachCoordinate(changeCoordinateValue(_item->finalApproachCoordinate()));
QVERIFY(_viMultiSpy->checkSignalByMask(dirtyChangedMask));
QVERIFY(_viMultiSpy->pullBoolFromSignalIndex(dirtyChangedIndex));
_item->setDirty(false);
_viMultiSpy->clearAllSignals();
QVERIFY(!_item->dirty());
_item->setLandingCoordinate(changeCoordinateValue(_item->landingCoordinate()));
QVERIFY(_viMultiSpy->checkSignalByMask(dirtyChangedMask));
QVERIFY(_viMultiSpy->pullBoolFromSignalIndex(dirtyChangedIndex));
_item->setDirty(false);
_viMultiSpy->clearAllSignals();
}
void LandingComplexItemTest::_testItemCount(void)
{
QList<MissionItem*> items;
struct TestCase_s {
bool stopTakingPhotos;
bool stopTakingVideo;
} rgTestCases[] = {
{ false, false },
{ false, true },
{ true, false },
{ true, true },
};
for (size_t i=0; i<sizeof(rgTestCases)/sizeof(rgTestCases[0]); i++) {
TestCase_s& testCase = rgTestCases[i];
_item->stopTakingPhotos()->setRawValue(testCase.stopTakingPhotos);
_item->stopTakingVideo()->setRawValue(testCase.stopTakingVideo);
_item->appendMissionItems(items, this);
QCOMPARE(items.count(), 3 + (testCase.stopTakingPhotos * CameraSection::stopTakingPhotosCommandCount()) + (testCase.stopTakingVideo * CameraSection::stopTakingVideoCommandCount()));
QCOMPARE(items.count() - 1, _item->lastSequenceNumber());
items.clear();
}
}
void LandingComplexItemTest::_testAppendSectionItems(void)
{
QList<MissionItem*> rgMissionItems;
struct TestCase_s {
bool stopTakingPhotos;
bool stopTakingVideo;
bool useLoiterToAlt;
} rgTestCases[] = {
{ false, false, false },
{ false, false, true },
{ false, true, false },
{ false, true, true },
{ true, false, false },
{ true, false, true },
{ true, true, false },
{ true, true, true },
};
for (size_t i=0; i<sizeof(rgTestCases)/sizeof(rgTestCases[0]); i++) {
TestCase_s& testCase = rgTestCases[i];
qDebug() << "stopTakingPhotos" << testCase.stopTakingPhotos << "stopTakingVideo" << testCase.stopTakingVideo << "useLoiterToAlt" << testCase.useLoiterToAlt;
_item->stopTakingPhotos()->setRawValue(testCase.stopTakingPhotos);
_item->stopTakingVideo()->setRawValue(testCase.stopTakingVideo);
_item->useLoiterToAlt()->setRawValue(testCase.useLoiterToAlt);
_item->appendMissionItems(rgMissionItems, this);
// First item should be DO_LAND_START always
QCOMPARE(rgMissionItems[0]->command(), MAV_CMD_DO_LAND_START);
// Next come stop photo/video
// Convert to simple visual items for verification
QmlObjectListModel* simpleItems = new QmlObjectListModel(this);
for (MissionItem* item: rgMissionItems) {
SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, false /* flyView */, false /* forLoad */);
simpleItem->missionItem() = *item;
simpleItems->append(simpleItem);
}
// Validate stop commands
QCOMPARE(CameraSection::scanStopTakingPhotos(simpleItems, 1, false /* removeScannedItems */), testCase.stopTakingPhotos);
QCOMPARE(CameraSection::scanStopTakingVideo(simpleItems, 1 + (testCase.stopTakingPhotos ? CameraSection::stopTakingPhotosCommandCount() : 0), false /* removeScannedItems */), testCase.stopTakingVideo);
// Lastly is final approach item and land
int finalApproachIndex = 1 + (testCase.stopTakingPhotos ? CameraSection::stopTakingPhotosCommandCount() : 0) + (testCase.stopTakingVideo ? CameraSection::stopTakingVideoCommandCount() : 0);
QCOMPARE(rgMissionItems[finalApproachIndex]->command(), testCase.useLoiterToAlt ? MAV_CMD_NAV_LOITER_TO_ALT : MAV_CMD_NAV_WAYPOINT);
qDebug() << rgMissionItems[finalApproachIndex+1]->command();
QCOMPARE(rgMissionItems[finalApproachIndex+1]->command(), MAV_CMD_NAV_LAND);
simpleItems->deleteLater();
rgMissionItems.clear();
}
}
void LandingComplexItemTest::_testScanForItems(void)
{
QList<MissionItem*> rgMissionItems;
struct TestCase_s {
bool stopTakingPhotos;
bool stopTakingVideo;
bool useLoiterToAlt;
} rgTestCases[] = {
{ false, false, false },
{ false, false, true },
{ false, true, false },
{ false, true, true },
{ true, false, false },
{ true, false, true },
{ true, true, false },
{ true, true, true },
};
for (size_t i=0; i<sizeof(rgTestCases)/sizeof(rgTestCases[0]); i++) {
TestCase_s& testCase = rgTestCases[i];
qDebug() << "stopTakingPhotos" << testCase.stopTakingPhotos << "stopTakingVideo" << testCase.stopTakingVideo << "useLoiterToAlt" << testCase.useLoiterToAlt;
_item->stopTakingPhotos()->setRawValue(testCase.stopTakingPhotos);
_item->stopTakingVideo()->setRawValue(testCase.stopTakingVideo);
_item->useLoiterToAlt()->setRawValue(testCase.useLoiterToAlt);
_item->appendMissionItems(rgMissionItems, this);
// Convert to simple visual items for _scan
QmlObjectListModel* visualItems = new QmlObjectListModel(this);
for (MissionItem* item: rgMissionItems) {
SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, false /* flyView */, false /* forLoad */);
simpleItem->missionItem() = *item;
visualItems->append(simpleItem);
}
QVERIFY(LandingComplexItem::_scanForItem(visualItems, false /* flyView */, _masterController, &SimpleLandingComplexItem::_isValidLandItem, &SimpleLandingComplexItem::_createItem));
QCOMPARE(visualItems->count(), 1);
SimpleLandingComplexItem* scannedItem = visualItems->value<SimpleLandingComplexItem*>(0);
QVERIFY(scannedItem);
_validateItem(scannedItem, _item);
visualItems->deleteLater();
rgMissionItems.clear();
}
}
void LandingComplexItemTest::_testSaveLoad(void)
{
QString errorString;
QJsonObject saveObject = _item->_save();
saveObject[JsonHelper::jsonVersionKey] = 1;
saveObject[VisualMissionItem::jsonTypeKey] = VisualMissionItem::jsonTypeComplexItemValue;
saveObject[ComplexMissionItem::jsonComplexItemTypeKey] = SimpleLandingComplexItem::jsonComplexItemTypeValue;
// Test useDeprecatedRelAltKeys = false
SimpleLandingComplexItem* newItem = new SimpleLandingComplexItem(_masterController, false /* flyView */);
bool loadSuccess = newItem->_load(saveObject, 0 /* sequenceNumber */, SimpleLandingComplexItem::jsonComplexItemTypeValue, false /* useDeprecatedRelAltKeys */, errorString);
if (!loadSuccess) {
qDebug() << "_load failed" << errorString;
}
QVERIFY(loadSuccess);
QVERIFY(errorString.isEmpty());
_validateItem(newItem, _item);
newItem->deleteLater();
// Test useDeprecatedRelAltKeys = true
bool relAlt = saveObject[LandingComplexItem::_jsonAltitudesAreRelativeKey].toBool();
saveObject[LandingComplexItem::_jsonDeprecatedLoiterAltitudeRelativeKey] = relAlt;
saveObject[LandingComplexItem::_jsonDeprecatedLandingAltitudeRelativeKey] = relAlt;
saveObject.remove(LandingComplexItem::_jsonAltitudesAreRelativeKey);
newItem = new SimpleLandingComplexItem(_masterController, false /* flyView */);
loadSuccess = newItem->_load(saveObject, 0 /* sequenceNumber */, SimpleLandingComplexItem::jsonComplexItemTypeValue, true /* useDeprecatedRelAltKeys */, errorString);
if (!loadSuccess) {
qDebug() << "_load failed" << errorString;
}
QVERIFY(loadSuccess);
QVERIFY(errorString.isEmpty());
_validateItem(newItem, _item);
newItem->deleteLater();
// Test for _jsonDeprecatedLoiterCoordinateKey support
saveObject = _item->_save();
saveObject[JsonHelper::jsonVersionKey] = 1;
saveObject[VisualMissionItem::jsonTypeKey] = VisualMissionItem::jsonTypeComplexItemValue;
saveObject[ComplexMissionItem::jsonComplexItemTypeKey] = SimpleLandingComplexItem::jsonComplexItemTypeValue;
saveObject[LandingComplexItem::_jsonDeprecatedLoiterCoordinateKey] = saveObject[LandingComplexItem::_jsonFinalApproachCoordinateKey];
saveObject.remove(LandingComplexItem::_jsonFinalApproachCoordinateKey);
newItem = new SimpleLandingComplexItem(_masterController, false /* flyView */);
loadSuccess = newItem->_load(saveObject, 0 /* sequenceNumber */, SimpleLandingComplexItem::jsonComplexItemTypeValue, false /* useDeprecatedRelAltKeys */, errorString);
if (!loadSuccess) {
qDebug() << "_load failed" << errorString;
}
QVERIFY(loadSuccess);
QVERIFY(errorString.isEmpty());
_validateItem(newItem, _item);
newItem->deleteLater();
}
void LandingComplexItemTest::_validateItem(LandingComplexItem* actualItem, LandingComplexItem* expectedItem)
{
QVERIFY(actualItem);
QCOMPARE(actualItem->stopTakingPhotos()->rawValue().toBool(), expectedItem->stopTakingPhotos()->rawValue().toBool());
QCOMPARE(actualItem->stopTakingVideo()->rawValue().toBool(), expectedItem->stopTakingVideo()->rawValue().toBool());
QCOMPARE(actualItem->useLoiterToAlt()->rawValue().toBool(), expectedItem->useLoiterToAlt()->rawValue().toBool());
QCOMPARE(actualItem->finalApproachAltitude()->rawValue().toInt(), expectedItem->finalApproachAltitude()->rawValue().toInt());
QCOMPARE(actualItem->useDoChangeSpeed()->rawValue().toBool(), expectedItem->useDoChangeSpeed()->rawValue().toBool());
QCOMPARE(actualItem->finalApproachSpeed()->rawValue().toInt(), expectedItem->finalApproachSpeed()->rawValue().toInt());
QCOMPARE(actualItem->landingAltitude()->rawValue().toInt(), expectedItem->landingAltitude()->rawValue().toInt());
QCOMPARE(actualItem->landingHeading()->rawValue().toInt(), expectedItem->landingHeading()->rawValue().toInt());
QCOMPARE(actualItem->landingDistance()->rawValue().toInt(), expectedItem->landingDistance()->rawValue().toInt());
QCOMPARE(actualItem->altitudesAreRelative(), expectedItem->altitudesAreRelative());
QCOMPARE(actualItem->landingCoordSet(), expectedItem->landingCoordSet());
QVERIFY(fuzzyCompareLatLon(actualItem->finalApproachCoordinate(), expectedItem->finalApproachCoordinate()));
QVERIFY(fuzzyCompareLatLon(actualItem->landingCoordinate(), expectedItem->landingCoordinate()));
if (actualItem->useLoiterToAlt()->rawValue().toBool()) {
QVERIFY(fuzzyCompareLatLon(actualItem->loiterTangentCoordinate(), expectedItem->loiterTangentCoordinate()));
QCOMPARE(actualItem->loiterRadius()->rawValue().toInt(), expectedItem->loiterRadius()->rawValue().toInt());
QCOMPARE(actualItem->loiterClockwise()->rawValue().toBool(), expectedItem->loiterClockwise()->rawValue().toBool());
}
}
SimpleLandingComplexItem::SimpleLandingComplexItem(PlanMasterController* masterController, bool flyView)
: LandingComplexItem (masterController, flyView)
, _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/VTOLLandingPattern.FactMetaData.json"), this))
, _landingDistanceFact (settingsGroup, _metaDataMap[finalApproachToLandDistanceName])
, _finalApproachAltitudeFact(settingsGroup, _metaDataMap[finalApproachAltitudeName])
, _useDoChangeSpeedFact (settingsGroup, _metaDataMap[useDoChangeSpeedName])
, _finalApproachSpeedFact (settingsGroup, _metaDataMap[finalApproachSpeedName])
, _loiterRadiusFact (settingsGroup, _metaDataMap[loiterRadiusName])
, _loiterClockwiseFact (settingsGroup, _metaDataMap[loiterClockwiseName])
, _landingHeadingFact (settingsGroup, _metaDataMap[landingHeadingName])
, _landingAltitudeFact (settingsGroup, _metaDataMap[landingAltitudeName])
, _useLoiterToAltFact (settingsGroup, _metaDataMap[useLoiterToAltName])
, _stopTakingPhotosFact (settingsGroup, _metaDataMap[stopTakingPhotosName])
, _stopTakingVideoFact (settingsGroup, _metaDataMap[stopTakingVideoName])
{
_isIncomplete = false;
_init();
_recalcFromHeadingAndDistanceChange();
setDirty(false);
}
MissionItem* SimpleLandingComplexItem::_createLandItem(int seqNum, bool altRel, double lat, double lon, double alt, QObject* parent)
{
return new MissionItem(seqNum,
MAV_CMD_NAV_LAND,
altRel ? MAV_FRAME_GLOBAL_RELATIVE_ALT : MAV_FRAME_GLOBAL,
0.0, 0.0, 0.0, 0.0,
lat, lon, alt,
true, // autoContinue
false, // isCurrentItem
parent);
}
bool SimpleLandingComplexItem::_isValidLandItem(const MissionItem& missionItem)
{
if (missionItem.command() != MAV_CMD_NAV_LAND ||
!(missionItem.frame() == MAV_FRAME_GLOBAL_RELATIVE_ALT || missionItem.frame() == MAV_FRAME_GLOBAL) ||
missionItem.param1() != 0 || missionItem.param2() != 0 || missionItem.param3() != 0 || missionItem.param4() != 0 ||
qIsNaN(missionItem.param5()) || qIsNaN(missionItem.param6()) || qIsNaN(missionItem.param7())) {
return false;
} else {
return true;
}
}
// Never call this method directly. If you want to update the flight segments you emit _updateFlightPathSegmentsSignal()
void SimpleLandingComplexItem::_updateFlightPathSegmentsDontCallDirectly(void)
{
if (_cTerrainCollisionSegments != 0) {
_cTerrainCollisionSegments = 0;
emit terrainCollisionChanged(false);
}
_flightPathSegments.beginReset();
_flightPathSegments.clearAndDeleteContents();
if (useLoiterToAlt()->rawValue().toBool()) {
_appendFlightPathSegment(FlightPathSegment::SegmentTypeGeneric, finalApproachCoordinate(), amslEntryAlt(), loiterTangentCoordinate(), amslEntryAlt()); // Best we can do to simulate loiter circle terrain profile
_appendFlightPathSegment(FlightPathSegment::SegmentTypeLand, loiterTangentCoordinate(), amslEntryAlt(), landingCoordinate(), amslExitAlt());
} else {
_appendFlightPathSegment(FlightPathSegment::SegmentTypeLand, finalApproachCoordinate(), amslEntryAlt(), landingCoordinate(), amslExitAlt());
}
_flightPathSegments.endReset();
if (_cTerrainCollisionSegments != 0) {
emit terrainCollisionChanged(true);
}
_masterController->missionController()->recalcTerrainProfile();
}