1
0
Fork 0

Extended NavRadio tests by Huntley Palmer

Also covers the new nav radio code, as well as the old version.
This commit is contained in:
James Turner 2021-01-24 16:09:18 +00:00
parent 548b9d8bc0
commit 71c09f0bcd
2 changed files with 582 additions and 239 deletions

View file

@ -6,14 +6,11 @@
#include "test_suite/FGTestApi/testGlobals.hxx"
#include "test_suite/FGTestApi/NavDataCache.hxx"
#include <Main/globals.hxx>
#include <Navaids/NavDataCache.hxx>
#include <Navaids/navrecord.hxx>
#include <Navaids/navlist.hxx>
#include <Instrumentation/navradio.hxx>
#include <Instrumentation/newnavradio.hxx>
// Set up function for each test.
void NavRadioTests::setUp()
@ -29,7 +26,7 @@ void NavRadioTests::tearDown()
FGTestApi::tearDown::shutdownTestGlobals();
}
void NavRadioTests::setPositionAndStabilise(SGSubsystem* r, const SGGeod& g)
void NavRadioTests::setPositionAndStabilise(FGNavRadio* r, const SGGeod& g)
{
FGTestApi::setPosition(g);
for (int i=0; i<60; ++i) {
@ -37,6 +34,13 @@ void NavRadioTests::setPositionAndStabilise(SGSubsystem* r, const SGGeod& g)
}
}
std::string NavRadioTests::formatFrequency(double f)
{
char buf[16];
::snprintf(buf, 16, "%3.2f", f);
return buf;
}
void NavRadioTests::testBasic()
{
SGPropertyNode_ptr configNode(new SGPropertyNode);
@ -61,232 +65,587 @@ void NavRadioTests::testBasic()
CPPUNIT_ASSERT_EQUAL(true, node->getBoolValue("in-range"));
}
void NavRadioTests::testCDIDeflection()
{
static const struct {
int nvType;
double nvLat, nvLon, nvAlt, nvFreq, nvRnge, nvTwst;
const string& nvIden;
double onRose, atDstNM, atAltFt, atHdg, rSele;
bool vOpnl, vToFlag;
double vSigNorm, vSigTolr, vHdgDefl, vDeflTolr, vHdgNorm, vDefnTolr, xtkTolr;
const string& tDesc;
} CDITestRoll[] = {
//
// Test Items: Add test cases here:
// nv<= fields are copied direct from nav dat => <= Rx pos wrt navaid, Radial =><= v- Values expected / tested => <= Line / Desc for Mesg =>
//Type Lat Lon Alt Freq Rnge Twist Iden] onRose atNm atAlt atHdg rSele Op To sigN - Tolr Defl - Tolr DNrm -Tolr xtkTolr
//
{ 3, 53.3, -2.26, 282, 113.55, 130, -5.0, "MCT", 25, 10.0, 4000, 200, 25, 1, 0, 1.0, 0.01, 0.0, 9.01, 0.0, 0.01, 50.0, "1: MCT EGCC On Radial" },
{ 3, 53.3, -2.26, 282, 113.55, 130, -5.0, "MCT", 25, 10.0, 4000, 200, 25, 1, 0, 1.0, 0.01, 0.0, 9.01, 0.0, 0.01, 50.0, "2: MCT EGCC On Again " },
{ 3, 53.3, -2.26, 282, 113.55, 130, -5.0, "MCT", 20, 20.0, 12000, 20, 25, 1, 0, 1.0, 0.01, 5.0, 0.1, 0.5, 0.01, 50.0, "3: MCT 5deg Off radial" },
{ 3, 53.3, -2.26, 282, 113.55, 130, -5.0, "MCT", 33, 30.0, 16000, 100, 25, 1, 0, 1.0, 0.01, -8.0, 0.1, -0.8, 0.01, 50.0, "4: MCT 8deg Off radial" },
{ 3, 53.3, -2.26, 282, 113.55, 130, -5.0, "MCT", 38, 40.0, 16000, 280, 25, 1, 0, 1.0, 0.01, -10.0, 0.1, -1.0, 0.01, 50.0, "5: MCT >10 Off radial" },
{ 3, -31.9, 115.95, 87, 113.70, 130, -2.0, "PH", 222, 20.0, 12000, 220, 42, 1, 1, 1.0, 0.01, 0.0, 0.01, 0.0, 0.01, 50.0, "6: PH Perth W.Aus On Radial"},
{ 3, -31.9, 115.95, 87, 113.70, 130, -2.0, "PH", 225, 20.0, 18000, 220, 42, 1, 1, 1.0, 0.01, 3.0, 0.01, 0.3, 0.01, 50.0, "7: PH +3deg Off radial" }
};
void NavRadioTests::callNavRadioCDI() {
//
//2021Ja15 set flag for newnavradio
//
fgSetBool("/instrumentation/use-new-navradio", true);
// setup
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<FGNavRadio> 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/nav", 6.0);
node->setDoubleValue("frequencies/selected-mhz", 113.55);
node->setDoubleValue("radials/selected-deg", 25);
FGPositioned::TypeFilter f{FGPositioned::VOR};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(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_EQUAL(true, node->getBoolValue("operable"));
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<FGNavRecord>(
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"));
//
int tale = sizeof(CDITestRoll) / sizeof(CDITestRoll[0]);
for (int i = 0; (i < tale); i++) {
// prep error message
const string& itemDesc = " navradioCDI Item " + CDITestRoll[i].tDesc + " @ ";
// Txmitting navaid
node->setDoubleValue("frequencies/selected-mhz", CDITestRoll[i].nvFreq);
node->setDoubleValue("radials/selected-deg", CDITestRoll[i].rSele);
// tbd Filter on type as defined in nav dat
//FGPositioned::TypeFilter f{FGPositioned::VOR};
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::ILS, FGPositioned::LOC}};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent(CDITestRoll[i].nvIden,
SGGeod::fromDeg(CDITestRoll[i].nvLon, CDITestRoll[i].nvLat), &f));
//
// For VOR nav dat field 7: 'Twist' == Easterly rotation of Txmitter's 360 wrt True North c.f for Compass: 'Deviation West Rose is Best'
// Rx posn is specified according to navaid's radials as printed on chart: True Bng = ( Radial on Rose + Twist ( Deviation ))
// ( ftr: Both MCT -5 and PH -2 Are Negative Twists )
SGGeod posWrtRadial = SGGeodesy::direct(nav->geod(), (CDITestRoll[i].onRose + CDITestRoll[i].nvTwst),
(CDITestRoll[i].atDstNM * SG_NM_TO_METER));
posWrtRadial.setElevationFt(CDITestRoll[i].atAltFt);
setPositionAndStabilise(r.get(), posWrtRadial);
// heading-deg property below means bearing to txmitter; calc copied from navradio.cxx !!!
double bngToNavaid, az2, s;
SGGeodesy::inverse(posWrtRadial, (nav->geod()), bngToNavaid, az2, s);
// calc XTrack error
double xtkE = sin((CDITestRoll[i].rSele - CDITestRoll[i].onRose) * SG_DEGREES_TO_RADIANS) * (CDITestRoll[i].atDstNM * SG_NM_TO_METER);
// Verify expected vs Result
string tMesg = itemDesc + "VOR type";
CPPUNIT_ASSERT_MESSAGE(tMesg, nav->type() == FGPositioned::VOR);
tMesg = itemDesc + "Operable";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vOpnl, node->getBoolValue("operable"));
tMesg = itemDesc + "TO Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vToFlag, node->getBoolValue("to-flag"));
tMesg = itemDesc + "FROM Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vToFlag, !node->getBoolValue("from-flag"));
//
tMesg = itemDesc + "nav-id";
CPPUNIT_ASSERT_MESSAGE(tMesg, !strcmp((CDITestRoll[i].nvIden).c_str(), node->getStringValue("nav-id")));
//tbd VOR seems to not set selected-mhz-fmt
// Converting nvFreq to string results in trailing zeros
tMesg = itemDesc + "selected-mhz-fmt";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, formatFrequency(CDITestRoll[i].nvFreq), string{node->getStringValue("frequencies/selected-mhz-fmt")});
// actual-deg means: bearing seen on intstrument's dial: actual == onRose
tMesg = itemDesc + "actual-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].onRose, node->getDoubleValue("radials/actual-deg"), CDITestRoll[i].vDefnTolr);
// heading-deg means true bearing to navaid, not affected by plane's heading
tMesg = itemDesc + "heading-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, bngToNavaid, node->getDoubleValue("heading-deg"), 1);
//
tMesg = itemDesc + "Sig Norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vSigNorm, node->getDoubleValue("signal-quality-norm"), CDITestRoll[i].vSigTolr);
tMesg = itemDesc + "needle defl";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vHdgDefl, node->getDoubleValue("heading-needle-deflection"), CDITestRoll[i].vDeflTolr);
tMesg = itemDesc + "defl norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vHdgNorm, node->getDoubleValue("heading-needle-deflection-norm"), CDITestRoll[i].vDefnTolr);
tMesg = itemDesc + "xTrack error";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xtkE, node->getDoubleValue("crosstrack-error-m"), CDITestRoll[i].xtkTolr);
}
}
void NavRadioTests::testILSBasic()
void NavRadioTests::callNewNavRadioCDI()
{
// radio setup
//
//2021Ja15 set flag for newnavradio
//
fgSetBool("/instrumentation/use-new-navradio", true);
// setup
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<FGNavRadio> 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/nav", 6.0);
// test basic ILS: KSFO 28L
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::ILS, FGPositioned::LOC}};
FGNavRecordRef ils = fgpositioned_cast<FGNavRecord>(
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")));
//
int tale = sizeof(CDITestRoll) / sizeof(CDITestRoll[0]);
for (int i = 0; (i < tale); i++) {
// prep error message
const string& itemDesc = " navradioCDI Item " + CDITestRoll[i].tDesc + " @ ";
// Txmitting navaid
node->setDoubleValue("frequencies/selected-mhz", CDITestRoll[i].nvFreq);
node->setDoubleValue("radials/selected-deg", CDITestRoll[i].rSele);
// tbd Filter on type as defined in nav dat
//FGPositioned::TypeFilter f{FGPositioned::VOR};
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::ILS, FGPositioned::LOC}};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent(CDITestRoll[i].nvIden,
SGGeod::fromDeg(CDITestRoll[i].nvLon, CDITestRoll[i].nvLat), &f));
//
// For VOR nav dat field 7: 'Twist' == Easterly rotation of Txmitter's 360 wrt True North c.f for Compass: 'Deviation West Rose is Best'
// Rx posn is specified according to navaid's radials as printed on chart: True Bng = ( Radial on Rose + Twist ( Deviation ))
// ( ftr: Both MCT -5 and PH -2 Are Negative Twists )
SGGeod posWrtRadial = SGGeodesy::direct(nav->geod(), (CDITestRoll[i].onRose + CDITestRoll[i].nvTwst),
(CDITestRoll[i].atDstNM * SG_NM_TO_METER));
posWrtRadial.setElevationFt(CDITestRoll[i].atAltFt);
setPositionAndStabilise(r.get(), posWrtRadial);
// heading-deg property below means bearing to txmitter; calc copied from navradio.cxx !!!
double bngToNavaid, az2, s;
SGGeodesy::inverse(posWrtRadial, (nav->geod()), bngToNavaid, az2, s);
// calc XTrack error
double xtkE = sin((CDITestRoll[i].rSele - CDITestRoll[i].onRose) * SG_DEGREES_TO_RADIANS) * (CDITestRoll[i].atDstNM * SG_NM_TO_METER);
// Verify expected vs Result
string tMesg = itemDesc + "VOR type";
CPPUNIT_ASSERT_MESSAGE(tMesg, nav->type() == FGPositioned::VOR);
tMesg = itemDesc + "Operable";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vOpnl, node->getBoolValue("operable"));
tMesg = itemDesc + "TO Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vToFlag, node->getBoolValue("to-flag"));
tMesg = itemDesc + "FROM Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vToFlag, !node->getBoolValue("from-flag"));
//
tMesg = itemDesc + "nav-id";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, CDITestRoll[i].nvIden, string{node->getStringValue("nav-id")});
// 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);
// Converting nvFreq to string results in trailing zeros
tMesg = itemDesc + "selected-mhz-fmt";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, formatFrequency(CDITestRoll[i].nvFreq), string{node->getStringValue("frequencies/selected-mhz-fmt")});
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"));
// actual-deg means: bearing seen on intstrument's dial: actual == onRose
tMesg = itemDesc + "actual-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].onRose, node->getDoubleValue("radials/actual-deg"), CDITestRoll[i].vDefnTolr);
// heading-deg means true bearing to navaid, not affected by plane's heading
tMesg = itemDesc + "heading-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, bngToNavaid, node->getDoubleValue("heading-deg"), 1);
//
tMesg = itemDesc + "Sig Norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vSigNorm, node->getDoubleValue("signal-quality-norm"), CDITestRoll[i].vSigTolr);
tMesg = itemDesc + "needle defl";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vHdgDefl, node->getDoubleValue("heading-needle-deflection"), CDITestRoll[i].vDeflTolr);
tMesg = itemDesc + "defl norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, CDITestRoll[i].vHdgNorm, node->getDoubleValue("heading-needle-deflection-norm"), CDITestRoll[i].vDefnTolr);
tMesg = itemDesc + "xTrack error";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xtkE, node->getDoubleValue("crosstrack-error-m"), CDITestRoll[i].xtkTolr);
}
}
static const struct {
int nvType;
double nvLat, nvLon, nvAlt, nvFreq, nvRnge, nvTwst;
const string& nvIden;
double onRose, atDstNM, atAltFt, atHdg, rSele;
bool vOpnl, vToFlag;
double vSigNorm, vSigTolr, vHdgDefl, vDeflTolr, vHdgNorm, vDefnTolr, xtkTolr;
const string& tDesc;
} ILSTestRoll[] = {
//
// Ref pilotscafe.com ILS width: 700ft wide at thrsh. WIthin range, sensed at +-35dg @ 10NM +-10dg @ 18NM
//
// Test Items: Add test cases here:
// nv<= fields are copied direct from nav dat => <= Rx pos wrt navaid, Radial => <= v- Values expected / tested => <= Item - Desc =>
//Typ Lat Lon Alt Freq Rnge TruHdng Iden] onRose atNm atAlt atHdg rSele Op To sigN - Tolr Defl - Tolr DNrm -Tolr xtkTol ]
//
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 117.932, 2.5, 2500, 27, 297.932, 1, 1, 1.0, 0.01, 0.0, 0.01, 0.0, 0.01, 50.0, "1: ISFO On LOC"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 116.932, 6.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, -1.0, 0.10, -0.1, 0.01, 50.0, "2: ISFO -1 Deg"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 118.932, 6.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, 1.0, 0.01, -0.1, 0.01, 50.0, "3: ISFO +1 Deg"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 113.932, 3.0, 600, 27, 297.932, 1, 1, 1.0, 0.01, -3.0, 0.10, -0.1, 0.01, 50.0, "4: ISFO < MinDefl"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 121.932, 3.0, 600, 27, 297.932, 1, 1, 1.0, 0.01, 3.0, 0.01, -0.1, 0.01, 50.0, "5: ISFO > MzxDefl"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 297.932, 4.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, 0.0, 0.01, 0.0, 0.01, 50.0, "6: ISFO BC On LOC"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 296.932, 4.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, 1.0, 0.10, -0.1, 0.01, 50.0, "7: ISFO BC -1 Deg"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 298.932, 4.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, -1.0, 0.01, -0.1, 0.01, 50.0, "8: ISFO BC +1 Deg"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 293.932, 4.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, 3.0, 0.10, -0.1, 0.01, 50.0, "9: ISFO BC > MaxD"}, //
{4, 37.626, -122.394, 8, 109.55, 18, 297.932, "ISFO", 301.932, 4.0, 1500, 27, 297.932, 1, 1, 1.0, 0.01, -3.0, 0.01, -0.1, 0.01, 50.0, "10: ISFO BC < MinD"} //
};
void NavRadioTests::callNavRadioILS()
{
// set flag for newnavradio
fgSetBool("/instrumentation/use-new-navradio", false);
// setup
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<FGNavRadio> 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/nav", 6.0);
//
int tale = sizeof(ILSTestRoll) / sizeof(ILSTestRoll[0]);
for (int i = 0; (i < tale); i++) {
// prep error message
const string& itemDesc = " navRadioILS Item " + ILSTestRoll[i].tDesc + " @ ";
// Txmitting navaid
node->setDoubleValue("frequencies/selected-mhz", ILSTestRoll[i].nvFreq);
node->setDoubleValue("radials/selected-deg", ILSTestRoll[i].rSele);
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::ILS, FGPositioned::LOC}};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent(ILSTestRoll[i].nvIden,
SGGeod::fromDeg(ILSTestRoll[i].nvLon, ILSTestRoll[i].nvLat), &f));
SGGeod posWrtRadial = SGGeodesy::direct(nav->geod(), (ILSTestRoll[i].onRose), (ILSTestRoll[i].atDstNM * SG_NM_TO_METER));
posWrtRadial.setElevationFt(ILSTestRoll[i].atAltFt);
setPositionAndStabilise(r.get(), posWrtRadial);
// heading-deg property below means bearing to txmitter; calc copied from navradio.cxx !!!
double bngToNavaid, az2, s;
SGGeodesy::inverse(posWrtRadial, (nav->geod()), bngToNavaid, az2, s);
double xtkE = sin((ILSTestRoll[i].rSele - ILSTestRoll[i].onRose) * SG_DEGREES_TO_RADIANS) * (ILSTestRoll[i].atDstNM * SG_NM_TO_METER);
//
const double locWidth = nav->localizerWidth();
// Expected Defl / Scaling is hokey because ILS width varies ??
const double deflectionScale = 20.0 / locWidth; // 20 degrees is full VOR swing (-10 to +10 degrees)
double xpecDefl = (ILSTestRoll[i].vHdgDefl * deflectionScale);
xpecDefl = (xpecDefl > 10) ? 10 : xpecDefl;
xpecDefl = (xpecDefl < -10) ? -10 : xpecDefl;
//
// Verify expected: Operational and To flags
string tMesg = itemDesc + "ILS type";
CPPUNIT_ASSERT_MESSAGE(tMesg, nav->type() == FGPositioned::ILS);
tMesg = itemDesc + "Operable";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, ILSTestRoll[i].vOpnl, node->getBoolValue("operable"));
tMesg = itemDesc + "TO Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, ILSTestRoll[i].vToFlag, node->getBoolValue("to-flag"));
tMesg = itemDesc + "FROM Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, ILSTestRoll[i].vToFlag, !node->getBoolValue("from-flag"));
//
tMesg = itemDesc + "heading-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, bngToNavaid, node->getDoubleValue("heading-deg"), 1);
tMesg = itemDesc + "nav-id";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, ILSTestRoll[i].nvIden, string{node->getStringValue("nav-id")});
// Converting nvFreq to string results in trailing zeros
tMesg = itemDesc + "selected-mhz-fmt";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, formatFrequency(ILSTestRoll[i].nvFreq), string{node->getStringValue("frequencies/selected-mhz-fmt")});
// actual-deg means: bearing seen on intstrument's dial: actual == onRose
tMesg = itemDesc + "actual-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, ILSTestRoll[i].onRose, node->getDoubleValue("radials/actual-deg"), ILSTestRoll[i].vDefnTolr);
tMesg = itemDesc + "Sig Norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, ILSTestRoll[i].vSigNorm, node->getDoubleValue("signal-quality-norm"), ILSTestRoll[i].vSigTolr);
tMesg = itemDesc + "needle defl";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecDefl, node->getDoubleValue("heading-needle-deflection"), ILSTestRoll[i].vDeflTolr);
tMesg = itemDesc + "defl norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, (xpecDefl * 0.1), node->getDoubleValue("heading-needle-deflection-norm"), ILSTestRoll[i].vDefnTolr);
tMesg = itemDesc + "xTrack error";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xtkE, node->getDoubleValue("crosstrack-error-m"), ILSTestRoll[i].xtkTolr);
}
}
void NavRadioTests::callNewNavRadioILS()
{
// set flag for newnavradio
fgSetBool("/instrumentation/use-new-navradio", true);
// setup
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<FGNavRadio> 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/nav", 6.0);
//
int tale = sizeof(ILSTestRoll) / sizeof(ILSTestRoll[0]);
for (int i = 0; (i < tale); i++) {
// prep error message
const string & itemDesc = "newNavRadioILS Item " + ILSTestRoll[i].tDesc + " @ ";
// Txmitting navaid
node->setDoubleValue("frequencies/selected-mhz", ILSTestRoll[i].nvFreq);
node->setDoubleValue("radials/selected-deg", ILSTestRoll[i].rSele);
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::ILS, FGPositioned::LOC}};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent(ILSTestRoll[i].nvIden, \
SGGeod::fromDeg( ILSTestRoll[i].nvLon, ILSTestRoll[i].nvLat), &f));
SGGeod posWrtRadial = SGGeodesy::direct(nav->geod(), (ILSTestRoll[i].onRose ), (ILSTestRoll[i].atDstNM * SG_NM_TO_METER));
posWrtRadial.setElevationFt(ILSTestRoll[i].atAltFt);
setPositionAndStabilise(r.get(), posWrtRadial);
// heading-deg property below means bearing to txmitter; calc copied from navradio.cxx !!!
double bngToNavaid, az2, s;
SGGeodesy::inverse(posWrtRadial, (nav->geod()), bngToNavaid, az2, s);
double xtkE = sin( (ILSTestRoll[i].rSele - ILSTestRoll[i].onRose) * SG_DEGREES_TO_RADIANS) \
* ( ILSTestRoll[i].atDstNM * SG_NM_TO_METER ) ;
//
const double locWidth = nav->localizerWidth();
// Expected Defl / Scaling is hokey because ILS width varies ??
const double deflectionScale = 20.0 / locWidth; // 20 degrees is full VOR swing (-10 to +10 degrees)
double xpecDefl = (ILSTestRoll[i].vHdgDefl * deflectionScale);
xpecDefl = ( xpecDefl > 10 ) ? 10 : xpecDefl;
xpecDefl = ( xpecDefl < -10 ) ? -10 : xpecDefl;
//
// Verify expected: Operational and To flags
string tMesg = itemDesc + "ILS type";
CPPUNIT_ASSERT_MESSAGE( tMesg, nav->type() == FGPositioned::ILS);
tMesg = itemDesc + "Operable";
CPPUNIT_ASSERT_EQUAL_MESSAGE( tMesg, ILSTestRoll[i].vOpnl, node->getBoolValue("operable"));
tMesg = itemDesc + "TO Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE( tMesg, ILSTestRoll[i].vToFlag, node->getBoolValue("to-flag"));
tMesg = itemDesc + "FROM Flag";
CPPUNIT_ASSERT_EQUAL_MESSAGE( tMesg, ILSTestRoll[i].vToFlag, !node->getBoolValue("from-flag"));
//
tMesg = itemDesc + "heading-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( tMesg, bngToNavaid, node->getDoubleValue("heading-deg"), 1);
tMesg = itemDesc + "nav-id";
CPPUNIT_ASSERT_EQUAL_MESSAGE( tMesg, ILSTestRoll[i].nvIden, string{node->getStringValue("nav-id")});
// Converting nvFreq to string results in trailing zeros
tMesg = itemDesc + "selected-mhz-fmt";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, formatFrequency(ILSTestRoll[i].nvFreq), string{node->getStringValue("frequencies/selected-mhz-fmt")});
// actual-deg means: bearing seen on intstrument's dial: actual == onRose
tMesg = itemDesc + "actual-deg";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( tMesg, ILSTestRoll[i].onRose, node->getDoubleValue("radials/actual-deg"), ILSTestRoll[i].vDefnTolr);
tMesg = itemDesc + "Sig Norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( tMesg, ILSTestRoll[i].vSigNorm, node->getDoubleValue("signal-quality-norm"), ILSTestRoll[i].vSigTolr);
tMesg = itemDesc + "needle defl";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( tMesg, xpecDefl, node->getDoubleValue("heading-needle-deflection"), ILSTestRoll[i].vDeflTolr);
tMesg = itemDesc + "defl norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( tMesg, (xpecDefl * 0.1 ), node->getDoubleValue("heading-needle-deflection-norm"), ILSTestRoll[i].vDefnTolr);
tMesg = itemDesc + "xTrack error";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( tMesg, xtkE, node->getDoubleValue("crosstrack-error-m"), ILSTestRoll[i].xtkTolr);
}
}
static const struct {
int nvType;
double nvLat, nvLon, nvAlt, nvFreq, nvRnge, nvAzim;
const string& nvIden;
double onRose, atDstNM, atAltFt, plusDeg, atHdg, rTruDeg;
bool vInRnge, vFalse;
double vSigNorm, vSigTolr, vGSDefl, vDeflTolr, vGSDefn, vDefnTolr;
const string& tDesc;
} GSTestRoll[] = {
//
// Test Items: Add test cases here:
// nv<= fields are copied direct from nav dat => <= Rx pos wrt navaid, Radial => <= v- Values expected / tested => <= Item - Desc =>
//Typ Lat Lon Alt Freq Rnge GSAzim Iden] onRose atNm atAlt or Deg atHdg TruDeg Rng Fls SgN - Tolr GSDefl-Tolr GSDefn-Tolr ]
//
{ 4, 52.563, 13.305, 101, 110.10, 10, 3.000, "ITLW", 117.932, 8.0, 0, 0, 80.828, 260.857, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "1: EDDT 26R +0 deg" }, //
{ 4, 52.563, 13.305, 101, 110.10, 10, 3.000, "ITLW", 117.932, 4.0, 0, 0.50, 80.828, 260.857, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "2: EDDT 26R +0.5 d" }, //
{ 4, 52.563, 13.305, 101, 110.10, 10, 3.000, "ITLW", 117.932, 2.0, 0, -1.00, 80.828, 260.857, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "3: EDDT 26R -1 deg" }, //
{ 4, 52.563, 13.305, 101, 110.10, 10, 3.000, "ITLW", 117.932, 5.0, 0, 3.00, 80.828, 260.857, 1, 1, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "4: EDDT 26R +3.0 Fls"}, //
{ 4, 52.563, 13.305, 101, 110.10, 10, 3.000, "ITLW", 117.932, 3.0, 0, +2.65, 80.828, 260.857, 1, 1, 1.0, 0.01,-1.75, 0.1, -0.5, 0.01, "5: EDDT 26R +3.5 Fls"}, //
// { 4, 51.464, -0.439, 50, 109.50, 10, 3.000, "ILL", 89.690, 7.5, 2500, 0, 80.828, 269.690, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "6: EGLL 27L 2K5 7M5"}, // Fail: Rx finds IBB
// { 4, 51.464, -0.439, 50, 109.50, 10, 3.000, "ILL", 89.690, 9.0, 3000, 0, 80.828, 269.690, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "7: EGLL 27L 3K0 9M0"}, //
// { 4, 51.464, -0.439, 50, 109.50, 10, 3.000, "ILL", 89.690,17.5, 4000, 0, 80.828, 269.690, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "8: EGLL 27L 4K 17M5"}, //
// { 4, 51.464, -0.439, 50, 109.50, 10, 3.000, "ILL", 89.690,25.0, 4000, 0, 80.828, 269.690, 1, 0, 1.0, 0.01, 0.0, 0.1, 0.0, 0.01, "9: EGLL 27L 4K 25M0"} //
};
void NavRadioTests::callNavRadioGS() {
//set flag for newnavradio
fgSetBool("/instrumentation/use-new-navradio", false);
// setup
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<FGNavRadio> 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/nav", 6.0);
//
// GS beam depth +-0.7deg; needle deflection -+3.5; gs-direct: Rxvr elevation from GS Txmitter level
//
const double halfBeam = 0.700;
const double deflFact = 3.500;
//
int tale = sizeof(GSTestRoll) / sizeof(GSTestRoll[0]);
for (int i = 0; (i < tale); i++) {
// prep error message
const string& itemDesc = " navRadioGS Item " + GSTestRoll[i].tDesc + " @ ";
// Txmitting navaid
node->setDoubleValue("frequencies/selected-mhz", GSTestRoll[i].nvFreq);
node->setDoubleValue("radials/selected-deg", GSTestRoll[i].rTruDeg);
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::GS, FGPositioned::LOC}};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent(GSTestRoll[i].nvIden,
SGGeod::fromDeg(GSTestRoll[i].nvLon, GSTestRoll[i].nvLat), &f));
// Check for proper nav type befor doing GS things
string tMesg = itemDesc + "GS Type ?";
CPPUNIT_ASSERT_MESSAGE(tMesg, nav->type() == FGPositioned::GS);
/////////////
// derive the GS geometry in cartesian vectors, to match what navradio.cxx does
SGGeod aboveGS = nav->geod();
aboveGS.setElevationM(nav->geod().getElevationM() + 100);
SGVec3d gsVerticalAxis = SGVec3d::fromGeod(aboveGS) - nav->cart();
// intentionally different approach to what navradio uses
gsVerticalAxis *= 0.01; // make it per meter, since we used 100m above
// derive the baseline
SGQuatd baseLineRot = SGQuatd::fromLonLat(nav->geod()) * SGQuatd::fromHeadAttBankDeg(GSTestRoll[i].atHdg, 0, 0);
SGVec3d gsAltAxis = baseLineRot.backTransform(SGVec3d(1.0, 0.0, 0.0));
const SGVec3d gsCart = nav->cart();
//////////////////
// expected deflection is calculated here if atAltFt is non-zero
double xpecAzim, xpecDefl, xpecDefn;
double bngToNavaid, az2, s;
if (GSTestRoll[i].atAltFt == 0) {
// Line item atAlt is zero so use degrees off GlideSlope for Rx position
double gsAngleRad = (nav->glideSlopeAngleDeg() + GSTestRoll[i].plusDeg) * SG_DEGREES_TO_RADIANS;
SGVec3d radioPos = gsCart;
radioPos += (gsVerticalAxis * tan(gsAngleRad) * GSTestRoll[i].atDstNM * SG_NM_TO_METER);
radioPos += (gsAltAxis * GSTestRoll[i].atDstNM * SG_NM_TO_METER);
setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
xpecAzim = (GSTestRoll[i].nvAzim + GSTestRoll[i].plusDeg);
} else {
// Line item atAlt is non zero so use altitude for Rx position
SGGeod p = SGGeodesy::direct(nav->geod(), GSTestRoll[i].atAltFt, GSTestRoll[i].atDstNM * SG_NM_TO_METER);
p.setElevationFt(GSTestRoll[i].atAltFt);
setPositionAndStabilise(r.get(), p);
//tbd calc Rx Azim from Tx for altitude case
SGGeodesy::inverse(p, nav->geod(), bngToNavaid, az2, s);
xpecAzim = SG_RADIANS_TO_DEGREES * (atan((GSTestRoll[i].atAltFt * SG_FEET_TO_METER) / s));
}
//
if (GSTestRoll[i].vFalse) {
// rxFlse indicates false signal, use deflections manually entered in Item
xpecDefl = GSTestRoll[i].vGSDefl;
xpecDefn = GSTestRoll[i].vGSDefn;
} else {
if (GSTestRoll[i].atAltFt == 0) {
// not rxFlse and atAltFt is zero : calculate needle deflections from Rx posn degrees wrt beam
xpecDefn = 0 - GSTestRoll[i].plusDeg / halfBeam; // Plane high: needle below
} else {
// not rxFlse and atAltFt non zero : calculate needle deflections from Rx posn azimuth wrt beam
xpecDefn = (GSTestRoll[i].nvAzim - az2) / halfBeam; // Plane high: needle below;
}
xpecDefn = (xpecDefn > 1) ? 1 : xpecDefn;
xpecDefn = (xpecDefn < -1) ? -1 : xpecDefn;
xpecDefl = xpecDefn * deflFact;
}
//
// Verify expected:
tMesg = itemDesc + "Operable";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, GSTestRoll[i].vInRnge, node->getBoolValue("operable"));
tMesg = itemDesc + "nav-id";
string dddbug = node->getStringValue("nav-id");
CPPUNIT_ASSERT_MESSAGE(tMesg, !strcmp((GSTestRoll[i].nvIden).c_str(), node->getStringValue("nav-id")));
tMesg = itemDesc + "Sig Norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, GSTestRoll[i].vSigNorm, node->getDoubleValue("signal-quality-norm"), GSTestRoll[i].vSigTolr);
tMesg = itemDesc + "gs-direct";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecAzim, node->getDoubleValue("gs-direct-deg"), 1);
tMesg = itemDesc + "needle defl";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecDefl, node->getDoubleValue("gs-needle-deflection"), GSTestRoll[i].vDeflTolr);
tMesg = itemDesc + "defl norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecDefn, node->getDoubleValue("gs-needle-deflection-norm"), GSTestRoll[i].vDefnTolr);
//
}
}
void NavRadioTests::callNewNavRadioGS()
{
//set flag for newnavradio
fgSetBool("/instrumentation/use-new-navradio", true);
// setup
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<FGNavRadio> 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/nav", 6.0);
//
// GS beam depth +-0.7deg; needle deflection -+3.5; gs-direct: Rxvr elevation from GS Txmitter level
//
const double halfBeam = 0.700;
const double deflFact = 3.500;
//
int tale = sizeof(GSTestRoll) / sizeof(GSTestRoll[0]);
for (int i = 0; (i < tale); i++) {
// prep error message
const string& itemDesc = "newnavRadioGS Item " + GSTestRoll[i].tDesc + " @ ";
// Txmitting navaid
node->setDoubleValue("frequencies/selected-mhz", GSTestRoll[i].nvFreq);
node->setDoubleValue("radials/selected-deg", GSTestRoll[i].rTruDeg);
FGPositioned::TypeFilter f{{FGPositioned::VOR, FGPositioned::GS, FGPositioned::LOC}};
FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent(GSTestRoll[i].nvIden,
SGGeod::fromDeg(GSTestRoll[i].nvLon, GSTestRoll[i].nvLat), &f));
// Check for proper nav type befor doing GS things
string tMesg = itemDesc + "GS Type ?";
CPPUNIT_ASSERT_MESSAGE(tMesg, nav->type() == FGPositioned::GS);
/////////////
// derive the GS geometry in cartesian vectors, to match what navradio.cxx does
SGGeod aboveGS = nav->geod();
aboveGS.setElevationM(nav->geod().getElevationM() + 100);
SGVec3d gsVerticalAxis = SGVec3d::fromGeod(aboveGS) - nav->cart();
// intentionally different approach to what navradio uses
gsVerticalAxis *= 0.01; // make it per meter, since we used 100m above
// derive the baseline
SGQuatd baseLineRot = SGQuatd::fromLonLat(nav->geod()) * SGQuatd::fromHeadAttBankDeg(GSTestRoll[i].atHdg, 0, 0);
SGVec3d gsAltAxis = baseLineRot.backTransform(SGVec3d(1.0, 0.0, 0.0));
const SGVec3d gsCart = nav->cart();
//////////////////
// expected deflection is calculated here if atAltFt is non-zero
double xpecAzim, xpecDefl, xpecDefn;
double bngToNavaid, az2, s;
if (GSTestRoll[i].atAltFt == 0) {
// Line item atAlt is zero so use degrees off GlideSlope for Rx position
double gsAngleRad = (nav->glideSlopeAngleDeg() + GSTestRoll[i].plusDeg) * SG_DEGREES_TO_RADIANS;
SGVec3d radioPos = gsCart;
radioPos += (gsVerticalAxis * tan(gsAngleRad) * GSTestRoll[i].atDstNM * SG_NM_TO_METER);
radioPos += (gsAltAxis * GSTestRoll[i].atDstNM * SG_NM_TO_METER);
setPositionAndStabilise(r.get(), SGGeod::fromCart(radioPos));
xpecAzim = (GSTestRoll[i].nvAzim + GSTestRoll[i].plusDeg);
} else {
// Line item atAlt is non zero so use altitude for Rx position
SGGeod p = SGGeodesy::direct(nav->geod(), GSTestRoll[i].atAltFt, GSTestRoll[i].atDstNM * SG_NM_TO_METER);
p.setElevationFt(GSTestRoll[i].atAltFt);
setPositionAndStabilise(r.get(), p);
//tbd calc Rx Azim from Tx for altitude case
SGGeodesy::inverse(p, nav->geod(), bngToNavaid, az2, s);
xpecAzim = SG_RADIANS_TO_DEGREES * (atan((GSTestRoll[i].atAltFt * SG_FEET_TO_METER) / s));
}
//
if (GSTestRoll[i].vFalse) {
// rxFlse indicates false signal, use deflections manually entered in Item
xpecDefl = GSTestRoll[i].vGSDefl;
xpecDefn = GSTestRoll[i].vGSDefn;
} else {
if (GSTestRoll[i].atAltFt == 0) {
// not rxFlse and atAltFt is zero : calculate needle deflections from Rx posn degrees wrt beam
xpecDefn = 0 - GSTestRoll[i].plusDeg / halfBeam; // Plane high: needle below
} else {
// not rxFlse and atAltFt non zero : calculate needle deflections from Rx posn azimuth wrt beam
xpecDefn = (GSTestRoll[i].nvAzim - az2) / halfBeam; // Plane high: needle below;
}
xpecDefn = (xpecDefn > 1) ? 1 : xpecDefn;
xpecDefn = (xpecDefn < -1) ? -1 : xpecDefn;
xpecDefl = xpecDefn * deflFact;
}
//
// Verify expected:
tMesg = itemDesc + "Operable";
CPPUNIT_ASSERT_EQUAL_MESSAGE(tMesg, GSTestRoll[i].vInRnge, node->getBoolValue("operable"));
tMesg = itemDesc + "nav-id";
string dddbug = node->getStringValue("nav-id");
CPPUNIT_ASSERT_MESSAGE(tMesg, !strcmp((GSTestRoll[i].nvIden).c_str(), node->getStringValue("nav-id")));
tMesg = itemDesc + "Sig Norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, GSTestRoll[i].vSigNorm, node->getDoubleValue("signal-quality-norm"), GSTestRoll[i].vSigTolr);
tMesg = itemDesc + "gs-direct";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecAzim, node->getDoubleValue("gs-direct-deg"), 1);
tMesg = itemDesc + "needle defl";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecDefl, node->getDoubleValue("gs-needle-deflection"), GSTestRoll[i].vDeflTolr);
tMesg = itemDesc + "defl norm";
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(tMesg, xpecDefn, node->getDoubleValue("gs-needle-deflection-norm"), GSTestRoll[i].vDefnTolr);
//
}
}
void NavRadioTests::testGS()
{
@ -479,27 +838,3 @@ void NavRadioTests::testGlideslopeLongDistance()
CPPUNIT_ASSERT_EQUAL(false, node->getBoolValue("gs-in-range"));
CPPUNIT_ASSERT_EQUAL(true, node->getBoolValue("in-range"));
}
void NavRadioTests::testNewRadioBasic()
{
SGPropertyNode_ptr configNode(new SGPropertyNode);
configNode->setStringValue("name", "navtest");
configNode->setIntValue("number", 2);
std::unique_ptr<SGSubsystem> r(Instrumentation::NavRadio::createInstance(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/nav", 6.0);
node->setDoubleValue("frequencies/selected-mhz", 113.8);
SGGeod pos = SGGeod::fromDegFt(-3.352780, 55.499199, 20000);
setPositionAndStabilise(r.get(), pos);
CPPUNIT_ASSERT_EQUAL(true, node->getBoolValue("operable"));
CPPUNIT_ASSERT(!strcmp("TLA", node->getStringValue("nav-id")));
CPPUNIT_ASSERT_EQUAL(true, node->getBoolValue("in-range"));
}

View file

@ -27,7 +27,6 @@
class FGNavRadio;
class SGGeod;
class SGSubsystem;
// The flight plan unit tests.
class NavRadioTests : public CppUnit::TestFixture
@ -35,20 +34,23 @@ class NavRadioTests : public CppUnit::TestFixture
// Set up the test suite.
CPPUNIT_TEST_SUITE(NavRadioTests);
CPPUNIT_TEST(testBasic);
CPPUNIT_TEST(testCDIDeflection);
CPPUNIT_TEST(testILSBasic);
CPPUNIT_TEST(callNavRadioCDI);
CPPUNIT_TEST(callNewNavRadioCDI);
CPPUNIT_TEST(callNavRadioILS);
CPPUNIT_TEST(callNewNavRadioILS);
CPPUNIT_TEST(callNavRadioGS);
CPPUNIT_TEST(callNewNavRadioGS);
CPPUNIT_TEST(testGS);
CPPUNIT_TEST(testILSFalseCourse);
CPPUNIT_TEST(testILSPaired);
CPPUNIT_TEST(testILSAdjacentPaired);
CPPUNIT_TEST(testGlideslopeLongDistance);
CPPUNIT_TEST(testNewRadioBasic);
CPPUNIT_TEST_SUITE_END();
void setPositionAndStabilise(SGSubsystem* r, const SGGeod& g);
void setPositionAndStabilise(FGNavRadio* r, const SGGeod& g);
public:
// Set up function for each test.
@ -57,18 +59,24 @@ public:
// Clean up after each test.
void tearDown();
std::string formatFrequency(double f);
// The tests.
void testBasic();
void testCDIDeflection();
void testILSBasic();
void callNavRadioCDI();
void callNewNavRadioCDI();
void callNavRadioILS();
void callNewNavRadioILS();
void callNavRadioGS();
void callNewNavRadioGS();
void testGS();
void testILSFalseCourse();
void testILSPaired();
void testILSAdjacentPaired();
void testGlideslopeLongDistance();
void testNewRadioBasic();
};
#endif // _FG_NAVRADIO_UNIT_TESTS_HXX