/* * Copyright (C) 2019 James Turner * * This file is part of the program FlightGear. * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see . */ #include "test_hold_controller.hxx" #include #include #include "test_suite/FGTestApi/globals.hxx" #include "test_suite/FGTestApi/NavDataCache.hxx" #include "test_suite/FGTestApi/TestPilot.hxx" #include #include #include #include #include #include #include using namespace flightgear; ///////////////////////////////////////////////////////////////////////////// class TestFPDelegate : public FlightPlan::Delegate { public: FlightPlanRef thePlan; int sequenceCount = 0; void sequence() override { ++sequenceCount; int newIndex = thePlan->currentIndex() + 1; if (newIndex >= thePlan->numLegs()) { thePlan->finish(); return; } thePlan->setCurrentIndex(newIndex); } void currentWaypointChanged() override { } }; ///////////////////////////////////////////////////////////////////////////// // Set up function for each test. void HoldControllerTests::setUp() { FGTestApi::setUp::initTestGlobals("hold-ctl"); FGTestApi::setUp::initNavDataCache(); setupRouteManager(); } // Clean up after each test. void HoldControllerTests::tearDown() { FGTestApi::tearDown::shutdownTestGlobals(); } void HoldControllerTests::setPositionAndStabilise(const SGGeod& g) { FGTestApi::setPosition(g); for (int i=0; i<60; ++i) { m_gps->update(0.015); } } GPS* HoldControllerTests::setupStandardGPS(SGPropertyNode_ptr config, const std::string name, const int index) { SGPropertyNode_ptr configNode(config.valid() ? config : SGPropertyNode_ptr{new SGPropertyNode}); configNode->setStringValue("name", name); configNode->setIntValue("number", index); GPS* gps(new GPS(configNode)); m_gps = gps; m_gpsNode = globals->get_props()->getNode("instrumentation", true)->getChild(name, index, true); m_gpsNode->setBoolValue("serviceable", true); globals->get_props()->setDoubleValue("systems/electrical/outputs/gps", 6.0); gps->bind(); gps->init(); globals->add_subsystem("gps", gps, SGSubsystemMgr::POST_FDM); return gps; } void HoldControllerTests::setupRouteManager() { auto rm = globals->add_new_subsystem(); rm->bind(); rm->init(); rm->postinit(); } ///////////////////////////////////////////////////////////////////////////// void HoldControllerTests::testHoldEntryDirect() { FGTestApi::setUp::logPositionToKML("hold_direct_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -6.0); setPositionAndStabilise(nearDover); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(250); pilot->flyGPSCourse(m_gps); // ensure GPS speed is plausible FGTestApi::runForTime(1.0); fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"COA"}, std::string{m_gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // first outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // check where we are! ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(45.0); // reqeust hold exit m_gpsNode->setStringValue("command", "exit-hold"); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // and now we should exit the hold and sequence to TAWNY ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay FGTestApi::runForTime(45.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } void HoldControllerTests::testHoldEntryTeardrop() { FGTestApi::setUp::logPositionToKML("hold_teardrop_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); holdWpt->setHoldRadial(120.0); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -8.0); setPositionAndStabilise(nearDover); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(200); pilot->flyGPSCourse(m_gps); // ensure GPS speed is plausible FGTestApi::runForTime(5.0); // trigger entry to the hold fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "entry-teardrop") return true; return false; }); CPPUNIT_ASSERT(ok); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // check where we are ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(80.0); // run inbound a bit m_gpsNode->setStringValue("command", "exit-hold"); // check we exit ok ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay - will take a long time since we're pointing totally // the wrong way FGTestApi::runForTime(240.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } void HoldControllerTests::testHoldEntryParallel() { FGTestApi::setUp::logPositionToKML("hold_parallel_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); holdWpt->setHoldRadial(45.0); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -5.0); setPositionAndStabilise(nearDover); // trigger entry to the hold fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(250); pilot->flyGPSCourse(m_gps); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "entry-parallel") return true; return false; }); CPPUNIT_ASSERT(ok); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // check where we are // outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(80.0); // run inbound a bit m_gpsNode->setStringValue("command", "exit-hold"); // check we exit ok ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay - will take a long time since we're pointing totally // the wrong way FGTestApi::runForTime(240.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } ///////////////////////////////////////////////////////////////////////////// void HoldControllerTests::testLeftHoldEntryDirect() { FGTestApi::setUp::logPositionToKML("hold_left_direct_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); // puts us near the 110-degree limit for a direct entry on a left-handed-hold holdWpt->setHoldRadial(15.0); holdWpt->setLeftHanded(); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -4.0); setPositionAndStabilise(nearDover); fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"COA"}, std::string{m_gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(300); pilot->flyGPSCourse(m_gps); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // first outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // check where we are! ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(45.0); // reqeust hold exit m_gpsNode->setStringValue("command", "exit-hold"); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // and now we should exit the hold and sequence to TAWNY ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay FGTestApi::runForTime(120.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } void HoldControllerTests::testLeftHoldEntryTeardrop() { FGTestApi::setUp::logPositionToKML("hold_left_teardrop_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); holdWpt->setHoldRadial(25.0); holdWpt->setLeftHanded(); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -5.0); setPositionAndStabilise(nearDover); // trigger entry to the hold fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(250); pilot->flyGPSCourse(m_gps); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "entry-teardrop") return true; return false; }); CPPUNIT_ASSERT(ok); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // check where we are ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(80.0); // run inbound a bit m_gpsNode->setStringValue("command", "exit-hold"); // check we exit ok ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay - will take a long time since we're pointing totally // the wrong way FGTestApi::runForTime(240.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } void HoldControllerTests::testLeftHoldEntryParallel() { FGTestApi::setUp::logPositionToKML("hold_left_parallel_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); holdWpt->setHoldRadial(160); holdWpt->setLeftHanded(); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -5.0); setPositionAndStabilise(nearDover); // trigger entry to the hold fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(190); pilot->flyGPSCourse(m_gps); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "entry-parallel") return true; return false; }); CPPUNIT_ASSERT(ok); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // check where we are // outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(80.0); // run inbound a bit m_gpsNode->setStringValue("command", "exit-hold"); // check we exit ok ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay - will take a long time since we're pointing totally // the wrong way FGTestApi::runForTime(240.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } ///////////////////////////////////////////////////////////////////////////// void HoldControllerTests::testHoldNotEntered() { FGTestApi::setUp::logPositionToKML("hold_no_entry"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); holdWpt->setHoldRadial(15.0); // sequence to that waypoint SGGeod nearDover = fp->pointAlongRoute(3, -4.0); setPositionAndStabilise(nearDover); fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"COA"}, std::string{m_gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); // check course deviation / cross-track error - we should still be at zero auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(nearDover); pilot->setCourseTrue(crsToDover); // initial course is correct pilot->setSpeedKts(300); pilot->flyGPSCourse(m_gps); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); // now we reset the hold count, so we don't enter m_gpsNode->setStringValue("command", "exit-hold"); // this should not do anything for now CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-exiting") return true; return false; }); // and now we should exit the hold and sequence to TAWNY ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay FGTestApi::runForTime(120.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } ///////////////////////////////////////////////////////////////////////////// // test entering the hold from wildly off the leg's track void HoldControllerTests::testHoldEntryOffCourse() { FGTestApi::setUp::logPositionToKML("hold_entry_off-course"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); FGTestApi::writeFlightPlanToKML(fp); auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); setupStandardGPS(); m_gpsNode->setStringValue("command", "leg"); setPositionAndStabilise(fp->departureRunway()->pointOnCenterline(0.0)); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); auto leg = fp->legAtIndex(3); // DVR vor leg->setHoldCount(3); CPPUNIT_ASSERT_EQUAL(std::string{"hold"}, leg->waypoint()->type()); flightgear::Hold* holdWpt = static_cast(leg->waypoint()); // this should be a direct entry, but now we're going to go off course! holdWpt->setHoldRadial(330); // sequence to that waypoint auto doverVORPos = fp->legAtIndex(3)->waypoint()->position(); SGGeod nearDover = fp->pointAlongRoute(3, -6.0); SGGeod offCoursePos = SGGeodesy::direct(doverVORPos, 20, 3.0 * SG_NM_TO_METER); setPositionAndStabilise(nearDover); // check course deviation / cross-track error - we should still be at zero const double legCrs = SGGeodesy::courseDeg(nearDover, doverVORPos); // sequence to the COA -> DVR leg fp->setCurrentIndex(3); // assume we got wildly off course somehow setPositionAndStabilise(offCoursePos); // verify hold entry course auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->setCourseTrue(legCrs); // initial course is correct pilot->setSpeedKts(250); pilot->flyGPSCourse(m_gps); pilot->resetAtPosition(offCoursePos); const double crsToDover = SGGeodesy::courseDeg(globals->get_aircraft_position(), doverVORPos); pilot->setCourseTrue(crsToDover); // initial course is correct CPPUNIT_ASSERT_EQUAL(std::string{"COA"}, std::string{m_gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{m_gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover, m_gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCrs, m_gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); // CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToDover - legCrs, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); auto statusNode = m_gpsNode->getNode("rnav-controller-status"); CPPUNIT_ASSERT_EQUAL(std::string{"leg-to-hold"}, std::string{statusNode->getStringValue()}); // we should select parallel entry due to being off course bool ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "entry-parallel") return true; return false; }); CPPUNIT_ASSERT(ok); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); // first outbound leg ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-outbound") return true; return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(45.0); // reqeust hold exit m_gpsNode->setStringValue("command", "exit-hold"); ok = FGTestApi::runForTimeWithCheck(600.0, [statusNode]() { std::string s = statusNode->getStringValue(); if (s == "hold-inbound") return true; return false; }); CPPUNIT_ASSERT(ok); // and now we should exit the hold and sequence to TAWNY ok = FGTestApi::runForTimeWithCheck(600.0, [fp]() { if (fp->currentIndex() == 4) return true; return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{m_gpsNode->getStringValue("mode")}); // check we get back enroute okay FGTestApi::runForTime(45.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, m_gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); }