diff --git a/test_suite/unit_tests/Instrumentation/CMakeLists.txt b/test_suite/unit_tests/Instrumentation/CMakeLists.txt
index 610ed2867..ee8078269 100644
--- a/test_suite/unit_tests/Instrumentation/CMakeLists.txt
+++ b/test_suite/unit_tests/Instrumentation/CMakeLists.txt
@@ -2,11 +2,13 @@ set(TESTSUITE_SOURCES
${TESTSUITE_SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/TestSuite.cxx
${CMAKE_CURRENT_SOURCE_DIR}/test_navRadio.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/test_gps.cxx
PARENT_SCOPE
)
set(TESTSUITE_HEADERS
${TESTSUITE_HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/test_navRadio.hxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/test_gps.hxx
PARENT_SCOPE
)
diff --git a/test_suite/unit_tests/Instrumentation/TestSuite.cxx b/test_suite/unit_tests/Instrumentation/TestSuite.cxx
index e51d4c35c..8a77785df 100644
--- a/test_suite/unit_tests/Instrumentation/TestSuite.cxx
+++ b/test_suite/unit_tests/Instrumentation/TestSuite.cxx
@@ -18,6 +18,10 @@
*/
#include "test_navRadio.hxx"
+#include "test_gps.hxx"
// Set up the unit tests.
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(NavRadioTests, "Unit tests");
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(GPSTests, "Unit tests");
+
+
diff --git a/test_suite/unit_tests/Instrumentation/test_gps.cxx b/test_suite/unit_tests/Instrumentation/test_gps.cxx
new file mode 100644
index 000000000..daaf020ee
--- /dev/null
+++ b/test_suite/unit_tests/Instrumentation/test_gps.cxx
@@ -0,0 +1,547 @@
+/*
+ * 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_gps.hxx"
+
+#include
+#include
+
+#include "test_suite/FGTestApi/globals.hxx"
+#include "test_suite/FGTestApi/NavDataCache.hxx"
+
+#include
+#include
+#include
+
+#include
+#include
+
+namespace {
+
+ class GPSShutdowner {
+ public:
+ GPSShutdowner(GPS* g) : gps(g) { }
+ ~GPSShutdowner() {
+ gps->unbind();
+ }
+ private:
+ GPS* gps;
+ };
+}
+
+// Set up function for each test.
+void GPSTests::setUp()
+{
+ FGTestApi::setUp::initTestGlobals("gps");
+ FGTestApi::setUp::initNavDataCache();
+}
+
+
+// Clean up after each test.
+void GPSTests::tearDown()
+{
+ FGTestApi::tearDown::shutdownTestGlobals();
+}
+
+void GPSTests::setPositionAndStabilise(GPS* gps, const SGGeod& g)
+{
+ FGTestApi::setPosition(g);
+ for (int i=0; i<60; ++i) {
+ gps->update(0.015);
+ }
+}
+
+std::unique_ptr GPSTests::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);
+
+ std::unique_ptr gps(new GPS(configNode));
+
+ SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation", true)->getChild(name, index, true);
+ node->setBoolValue("serviceable", true);
+ globals->get_props()->setDoubleValue("systems/electrical/outputs/gps", 6.0);
+
+ gps->bind();
+ gps->init();
+
+ return gps;
+}
+
+void GPSTests::testBasic()
+{
+ auto gps = setupStandardGPS();
+ GPSShutdowner gs(gps.get());
+
+ FGPositioned::TypeFilter f{FGPositioned::VOR};
+ auto bodrumVOR = fgpositioned_cast(FGPositioned::findClosestWithIdent("BDR", SGGeod::fromDeg(27.6, 37), &f));
+ SGGeod p1 = SGGeodesy::direct(bodrumVOR->geod(), 45.0, 5.0 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(gps.get(), p1);
+
+ auto gpsNode = globals->get_props()->getNode("instrumentation/gps");
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLongitudeDeg(), gpsNode->getDoubleValue("indicated-longitude-deg"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLatitudeDeg(), gpsNode->getDoubleValue("indicated-latitude-deg"), 0.01);
+}
+
+void GPSTests::testOBSMode()
+{
+ auto gps = setupStandardGPS();
+ GPSShutdowner gs(gps.get());
+
+ FGPositioned::TypeFilter f{FGPositioned::VOR};
+ auto bodrumVOR = fgpositioned_cast(FGPositioned::findClosestWithIdent("BDR", SGGeod::fromDeg(27.6, 37), &f));
+ SGGeod p1 = SGGeodesy::direct(bodrumVOR->geod(), 45.0, 5.0 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(gps.get(), p1);
+
+ auto gpsNode = globals->get_props()->getNode("instrumentation/gps");
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLongitudeDeg(), gpsNode->getDoubleValue("indicated-longitude-deg"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLatitudeDeg(), gpsNode->getDoubleValue("indicated-latitude-deg"), 0.01);
+
+ gpsNode->setDoubleValue("selected-course-deg", 225);
+
+ // query BDR from the GPS database
+ gpsNode->setStringValue("scratch/query", "BDR");
+ gpsNode->setStringValue("scratch/type", "vor");
+ gpsNode->setStringValue("command", "search");
+
+ CPPUNIT_ASSERT_EQUAL(true, gpsNode->getBoolValue("scratch/valid"));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("scratch/true-bearing-deg"), 0.5);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(5.0, gpsNode->getDoubleValue("scratch/distance-nm"), 0.1);
+
+ // select OBS mode one it
+ gpsNode->setStringValue("command", "obs");
+
+ setPositionAndStabilise(gps.get(), p1);
+
+ CPPUNIT_ASSERT_EQUAL(std::string{"obs"}, std::string{gpsNode->getStringValue("mode")});
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(5.0, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(bodrumVOR->get_lon(), gpsNode->getDoubleValue("wp/wp[1]/longitude-deg"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(bodrumVOR->get_lat(), gpsNode->getDoubleValue("wp/wp[1]/latitude-deg"), 0.01);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("desired-course-deg"), 0.01);
+
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05);
+
+}
+
+void GPSTests::testNavRadioSlave()
+{
+ SGPropertyNode_ptr radioConfigNode(new SGPropertyNode);
+ radioConfigNode->setStringValue("name", "navtest");
+ radioConfigNode->setIntValue("number", 2);
+ std::unique_ptr r(new FGNavRadio(radioConfigNode));
+}
+
+void GPSTests::testConfigAutopilotDrive()
+{
+}
+
+void GPSTests::testTurnAnticipation()
+{
+}
+
+
+#if 0
+ SGPropertyNode_ptr configNode(new SGPropertyNode);
+ configNode->setStringValue("name", "navtest");
+ configNode->setIntValue("number", 2);
+ std::unique_ptr r(new FGNavRadio(configNode));
+
+ r->bind();
+ r->init();
+
+ SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation/navtest[2]");
+ node->setBoolValue("serviceable", true);
+ // needed for the radio to power up
+ globals->get_props()->setDoubleValue("systems/electrical/outputs/navtest", 6.0);
+ node->setDoubleValue("frequencies/selected-mhz", 113.8);
+
+ SGGeod pos = SGGeod::fromDegFt(-3.352780, 55.499199, 20000);
+ setPositionAndStabilise(r.get(), pos);
+
+ CPPUNIT_ASSERT(!strcmp("TLA", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_EQUAL(true, node->getBoolValue("in-range"));
+}
+
+void NavRadioTests::testCDIDeflection()
+{
+ SGPropertyNode_ptr configNode(new SGPropertyNode);
+ configNode->setStringValue("name", "navtest");
+ configNode->setIntValue("number", 2);
+ std::unique_ptr r(new FGNavRadio(configNode));
+
+ r->bind();
+ r->init();
+
+ SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation/navtest[2]");
+ node->setBoolValue("serviceable", true);
+ // needed for the radio to power up
+ globals->get_props()->setDoubleValue("systems/electrical/outputs/navtest", 6.0);
+ node->setDoubleValue("frequencies/selected-mhz", 113.55);
+
+ node->setDoubleValue("radials/selected-deg", 25);
+
+ FGPositioned::TypeFilter f{FGPositioned::VOR};
+ FGNavRecordRef nav = fgpositioned_cast(FGPositioned::findClosestWithIdent("MCT", SGGeod::fromDeg(-2.26, 53.3), &f));
+
+ // twist of MCT is 5.0, so we use a bearing of 20 here, not 25
+ SGGeod posOnRadial = SGGeodesy::direct(nav->geod(), 20.0, 10 * SG_NM_TO_METER);
+ posOnRadial.setElevationFt(10000);
+ setPositionAndStabilise(r.get(), posOnRadial);
+
+ CPPUNIT_ASSERT(!strcmp("MCT", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("crosstrack-error-m"), 0.01);
+ CPPUNIT_ASSERT(node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(!node->getBoolValue("to-flag"));
+
+
+ // move off course
+ SGGeod posOffRadial = SGGeodesy::direct(nav->geod(), 15.0, 20 * SG_NM_TO_METER); // 5 degress off
+ posOffRadial.setElevationFt(12000);
+ setPositionAndStabilise(r.get(), posOffRadial);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(5.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.5, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+
+ double xtkE = sin(5.0 * SG_DEGREES_TO_RADIANS) * 20 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+ CPPUNIT_ASSERT(node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(!node->getBoolValue("to-flag"));
+
+ posOffRadial = SGGeodesy::direct(nav->geod(), 28.0, 30 * SG_NM_TO_METER); // 8 degress off
+ posOffRadial.setElevationFt(16000);
+ setPositionAndStabilise(r.get(), posOffRadial);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-8.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-0.8, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+
+ xtkE = sin(-8.0 * SG_DEGREES_TO_RADIANS) * 30 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+
+ // move more than ten degrees off course
+ posOffRadial = SGGeodesy::direct(nav->geod(), 33.0, 40 * SG_NM_TO_METER); // 13 degress off
+ posOffRadial.setElevationFt(16000);
+ setPositionAndStabilise(r.get(), posOffRadial);
+
+ // pegged to full deflection
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-10.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+
+ // cross track error is computed based on true deflection, not clamped
+ xtkE = sin(-13.0 * SG_DEGREES_TO_RADIANS) * 40 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+ CPPUNIT_ASSERT(node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(!node->getBoolValue("to-flag"));
+
+// try on the TO side of the station
+ // let's use Perth VOR, but the Australian one to check southern
+ // hemisphere operation
+ node->setDoubleValue("frequencies/selected-mhz", 113.7);
+ node->setDoubleValue("radials/selected-deg", 42.0); // twist is -2.0
+ CPPUNIT_ASSERT(!strcmp("113.70", node->getStringValue("frequencies/selected-mhz-fmt")));
+
+ auto perthVOR = fgpositioned_cast(
+ FGPositioned::findClosestWithIdent("PH", SGGeod::fromDeg(115.95, -31.9), &f));
+
+ SGGeod p = SGGeodesy::direct(perthVOR->geod(), 220.0, 20 * SG_NM_TO_METER); // on the reciprocal radial
+ p.setElevationFt(12000);
+ setPositionAndStabilise(r.get(), p);
+
+ CPPUNIT_ASSERT(!strcmp("PH", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(40.0, node->getDoubleValue("heading-deg"), 0.5);
+
+ // actual radial has twist subtracted
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(222.0, node->getDoubleValue("radials/actual-deg"), 0.01);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("crosstrack-error-m"), 50.0);
+ CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+
+// off course on the TO side
+ p = SGGeodesy::direct(perthVOR->geod(), 227.0, 100 * SG_NM_TO_METER);
+ p.setElevationFt(18000);
+ setPositionAndStabilise(r.get(), p);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(47.0, node->getDoubleValue("heading-deg"), 1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(229.0, node->getDoubleValue("radials/actual-deg"), 0.01);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(7.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.7, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+
+ xtkE = sin(7.0 * SG_DEGREES_TO_RADIANS) * 100 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+ CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+}
+
+void NavRadioTests::testILSBasic()
+{
+ // radio setup
+ SGPropertyNode_ptr configNode(new SGPropertyNode);
+ configNode->setStringValue("name", "navtest");
+ configNode->setIntValue("number", 2);
+ std::unique_ptr r(new FGNavRadio(configNode));
+ r->bind();
+ r->init();
+
+ SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation/navtest[2]");
+ node->setBoolValue("serviceable", true);
+ globals->get_props()->setDoubleValue("systems/electrical/outputs/navtest", 6.0);
+
+ // test basic ILS: KSFO 28L
+ FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::ILS, FGPositioned::LOC}};
+ FGNavRecordRef ils = fgpositioned_cast(
+ FGPositioned::findClosestWithIdent("ISFO", SGGeod::fromDeg(-112, 37.6), &f));
+ CPPUNIT_ASSERT(ils->type() == FGPositioned::ILS);
+
+ node->setDoubleValue("frequencies/selected-mhz", 109.55);
+ // node->setDoubleValue("radials/selected-deg", 42.0); // twist is -2.0
+ CPPUNIT_ASSERT(!strcmp("109.55", node->getStringValue("frequencies/selected-mhz-fmt")));
+
+ // note we need full precision here, due to ILS sensitivity
+ SGGeod p = SGGeodesy::direct(ils->geod(), 117.932, 10 * SG_NM_TO_METER);
+ p.setElevationFt(2500);
+ setPositionAndStabilise(r.get(), p);
+
+ CPPUNIT_ASSERT(!strcmp("ISFO", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(297.9, node->getDoubleValue("radials/target-radial-deg"), 0.1);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(297.9, node->getDoubleValue("heading-deg"), 1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(117.932, node->getDoubleValue("radials/actual-deg"), 0.1);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("crosstrack-error-m"), 10.0);
+ CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+
+ // 1 degree offset
+ p = SGGeodesy::direct(ils->geod(), 116.932, 6 * SG_NM_TO_METER);
+ p.setElevationFt(1500);
+ setPositionAndStabilise(r.get(), p);
+
+ const double locWidth = ils->localizerWidth();
+ const double deflectionScale = 20.0 / locWidth; // 20 degrees is full VOR swing (-10 to +10 degrees)
+
+ CPPUNIT_ASSERT(!strcmp("ISFO", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(297.9, node->getDoubleValue("radials/target-radial-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(296.9, node->getDoubleValue("heading-deg"), 1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(116.932, node->getDoubleValue("radials/actual-deg"), 0.1);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.0 * deflectionScale, node->getDoubleValue("heading-needle-deflection"), 0.1);
+
+ double xtkE = sin(-1.0 * SG_DEGREES_TO_RADIANS) * 6.0 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 1.0);
+ CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+
+
+ // test pegged (4 degrees off course)
+ p = SGGeodesy::direct(ils->geod(), 121.932, 3 * SG_NM_TO_METER);
+ p.setElevationFt(600);
+ setPositionAndStabilise(r.get(), p);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(301.9, node->getDoubleValue("heading-deg"), 1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(121.932, node->getDoubleValue("radials/actual-deg"), 0.1);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(10.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+
+ xtkE = sin(4.0 * SG_DEGREES_TO_RADIANS) * 3.0 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 1.0);
+ CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+
+
+ // also check ILS back course
+ // 1 degree offset on the BC
+ p = SGGeodesy::direct(ils->geod(), 298.932, 4 * SG_NM_TO_METER);
+ p.setElevationFt(1500);
+ setPositionAndStabilise(r.get(), p);
+
+ CPPUNIT_ASSERT(!strcmp("ISFO", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(297.9, node->getDoubleValue("radials/target-radial-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(118.9, node->getDoubleValue("heading-deg"), 1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(298.932, node->getDoubleValue("radials/actual-deg"), 0.1);
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.0 * deflectionScale, node->getDoubleValue("heading-needle-deflection"), 0.1);
+
+ xtkE = sin(-1.0 * SG_DEGREES_TO_RADIANS) * 4.0 * SG_NM_TO_METER;
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 1.0);
+
+ // these don't change for an ILS
+ CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+ CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+}
+
+
+
+void NavRadioTests::testGS()
+{
+ // radio setup
+ SGPropertyNode_ptr configNode(new SGPropertyNode);
+ configNode->setStringValue("name", "navtest");
+ configNode->setIntValue("number", 2);
+ std::unique_ptr r(new FGNavRadio(configNode));
+ r->bind();
+ r->init();
+
+ SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation/navtest[2]");
+ node->setBoolValue("serviceable", true);
+ globals->get_props()->setDoubleValue("systems/electrical/outputs/navtest", 6.0);
+
+ // EDDT 28R
+ FGPositioned::TypeFilter f{FGPositioned::GS};
+ FGNavRecordRef gs = fgpositioned_cast(
+ FGPositioned::findClosestWithIdent("ITLW", SGGeod::fromDeg(13, 52), &f));
+ CPPUNIT_ASSERT(gs->type() == FGPositioned::GS);
+ node->setDoubleValue("frequencies/selected-mhz", 110.10);
+ CPPUNIT_ASSERT(!strcmp("110.10", node->getStringValue("frequencies/selected-mhz-fmt")));
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(gs->glideSlopeAngleDeg(), 3.0, 0.001);
+ double gsAngleRad = gs->glideSlopeAngleDeg() * SG_DEGREES_TO_RADIANS;
+
+ /////////////
+ // derive the GS geometry in cartesian vectors, to match what
+ // navradio.cxx does
+ SGGeod aboveGS = gs->geod();
+ aboveGS.setElevationM(gs->geod().getElevationM() + 100.0);
+ SGVec3d gsVerticalAxis = SGVec3d::fromGeod(aboveGS) - gs->cart();
+ // intentionally different approach to what navradio uses
+
+ gsVerticalAxis *= 0.01; // make it per meter, since we used 100m above
+
+ // dervice the baseline
+ SGQuatd baseLineRot = SGQuatd::fromLonLat(gs->geod()) * SGQuatd::fromHeadAttBankDeg(80.828, 0, 0);
+ SGVec3d gsAltAxis = baseLineRot.backTransform(SGVec3d(1.0, 0.0, 0.0));
+
+ const SGVec3d gsCart = gs->cart();
+
+ //////////////////
+
+ SGVec3d radioPos = gsCart;
+ radioPos += (gsVerticalAxis * tan(gsAngleRad) * 8 * SG_NM_TO_METER);
+ radioPos += (gsAltAxis * 8 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
+
+ CPPUNIT_ASSERT(!strcmp("ITLW", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(3.0, node->getDoubleValue("gs-direct-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("gs-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("gs-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT(node->getBoolValue("gs-in-range"));
+
+// 0.5 degree offset above
+ gsAngleRad = (gs->glideSlopeAngleDeg() + 0.5) * SG_DEGREES_TO_RADIANS;
+ radioPos = gsCart;
+ radioPos += (gsVerticalAxis * tan(gsAngleRad) * 4 * SG_NM_TO_METER);
+ radioPos += (gsAltAxis * 4 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
+
+ CPPUNIT_ASSERT(!strcmp("ITLW", node->getStringValue("nav-id")));
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(3.5, node->getDoubleValue("gs-direct-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-2.5, node->getDoubleValue("gs-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-0.714, node->getDoubleValue("gs-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT(node->getBoolValue("gs-in-range"));
+
+// 1 degree below (danger!)
+ gsAngleRad = (gs->glideSlopeAngleDeg() - 1.0) * SG_DEGREES_TO_RADIANS;
+ radioPos = gsCart;
+ radioPos += (gsVerticalAxis * tan(gsAngleRad) * 2 * SG_NM_TO_METER);
+ radioPos += (gsAltAxis * 2 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(2.0, node->getDoubleValue("gs-direct-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(3.5, node->getDoubleValue("gs-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("gs-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT(node->getBoolValue("gs-in-range"));
+
+// false course above, reversed
+ gsAngleRad = (gs->glideSlopeAngleDeg() + 3.0) * SG_DEGREES_TO_RADIANS;
+ radioPos = gsCart;
+ radioPos += (gsVerticalAxis * tan(gsAngleRad) * 5 * SG_NM_TO_METER);
+ radioPos += (gsAltAxis * 5 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(6.0, node->getDoubleValue("gs-direct-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("gs-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("gs-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT(node->getBoolValue("gs-in-range"));
+
+ // false course above, reversed, 0.35 offset below
+ gsAngleRad = (gs->glideSlopeAngleDeg() + 2.65) * SG_DEGREES_TO_RADIANS;
+ radioPos = gsCart;
+ radioPos += (gsVerticalAxis * tan(gsAngleRad) * 3 * SG_NM_TO_METER);
+ radioPos += (gsAltAxis * 3 * SG_NM_TO_METER);
+
+ setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
+
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(5.65, node->getDoubleValue("gs-direct-deg"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.75, node->getDoubleValue("gs-needle-deflection"), 0.1);
+ CPPUNIT_ASSERT_DOUBLES_EQUAL(-0.5, node->getDoubleValue("gs-needle-deflection-norm"), 0.01);
+ CPPUNIT_ASSERT(node->getBoolValue("gs-in-range"));
+}
+
+void NavRadioTests::testILSFalseCourse()
+{
+
+ // also GS false lobes
+}
+
+void NavRadioTests::testILSPaired()
+{
+ // EGPH and countless more
+}
+
+void NavRadioTests::testILSAdjacentPaired()
+{
+ // eg KJFK
+}
+
+#endif
+
diff --git a/test_suite/unit_tests/Instrumentation/test_gps.hxx b/test_suite/unit_tests/Instrumentation/test_gps.hxx
new file mode 100644
index 000000000..f129d04e8
--- /dev/null
+++ b/test_suite/unit_tests/Instrumentation/test_gps.hxx
@@ -0,0 +1,68 @@
+/*
+ * 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 .
+ */
+
+
+#ifndef _FG_GPS_UNIT_TESTS_HXX
+#define _FG_GPS_UNIT_TESTS_HXX
+
+
+#include
+#include
+
+#include
+
+#include
+
+class SGGeod;
+class GPS;
+
+// The flight plan unit tests.
+class GPSTests : public CppUnit::TestFixture
+{
+ // Set up the test suite.
+ CPPUNIT_TEST_SUITE(GPSTests);
+ CPPUNIT_TEST(testBasic);
+ CPPUNIT_TEST(testNavRadioSlave);
+ CPPUNIT_TEST(testConfigAutopilotDrive);
+ CPPUNIT_TEST(testTurnAnticipation);
+ CPPUNIT_TEST(testOBSMode);
+
+ CPPUNIT_TEST_SUITE_END();
+
+ void setPositionAndStabilise(GPS* gps, const SGGeod& g);
+
+ std::unique_ptr setupStandardGPS(SGPropertyNode_ptr config = {},
+ const std::string name = "gps", const int index = 0);
+
+public:
+ // Set up function for each test.
+ void setUp();
+
+ // Clean up after each test.
+ void tearDown();
+
+ // The tests.
+ void testBasic();
+ void testNavRadioSlave();
+ void testConfigAutopilotDrive();
+ void testTurnAnticipation();
+ void testOBSMode();
+};
+
+#endif // _FG_GPS_UNIT_TESTS_HXX