FlightGear next
navradio.cxx
Go to the documentation of this file.
1// navradio.cxx -- class to manage a nav radio instance
2//
3// Written by Curtis Olson, started April 2000.
4//
5// Copyright (C) 2000 - 2002 Curtis L. Olson - http://www.flightgear.org/~curt
6//
7// This program is free software; you can redistribute it and/or
8// modify it under the terms of the GNU General Public License as
9// published by the Free Software Foundation; either version 2 of the
10// License, or (at your option) any later version.
11//
12// This program is distributed in the hope that it will be useful, but
13// WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15// General Public License for more details.
16//
17// You should have received a copy of the GNU General Public License
18// along with this program; if not, write to the Free Software
19// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20//
21
22#ifdef HAVE_CONFIG_H
23# include <config.h>
24#endif
25
26#include <sstream>
27#include <cstring>
28#include <cstdio>
29
30#include <simgear/sg_inlines.h>
31#include <simgear/timing/sg_time.hxx>
32#include <simgear/math/sg_random.hxx>
33#include <simgear/misc/sg_path.hxx>
34#include <simgear/math/sg_geodesy.hxx>
35#include <simgear/structure/exception.hxx>
36#include <simgear/math/interpolater.hxx>
37#include <simgear/misc/strutils.hxx>
38#include <simgear/sound/sample_group.hxx>
39
40#include <Navaids/navrecord.hxx>
41#include <Sound/audioident.hxx>
42#include <Airports/runways.hxx>
43#include <Navaids/navlist.hxx>
44#include <Main/util.hxx>
45
46#include "navradio.hxx"
47
48using std::string;
49
50// General-purpose sawtooth function. Graph looks like this:
51// /\ .
52// \/
53// Odd symmetry, inversion symmetry about the origin.
54// Unit slope at the origin.
55// Max 1, min -1, period 4.
56// Two zero-crossings per period, one with + slope, one with - slope.
57// Useful for false localizer courses.
58static double sawtooth(double xx)
59{
60 return 4.0 * fabs(xx/4.0 + 0.25 - floor(xx/4.0 + 0.75)) - 1.0;
61}
62
63// Calculate a Cartesian unit vector in the
64// local horizontal plane, i.e. tangent to the
65// surface of the earth at the local ground zero.
66// The tangent vector passes through the given <midpoint>
67// and points forward along the given <heading>.
68// The <heading> is given in degrees.
69static SGVec3d tangentVector(const SGGeod& midpoint, const double heading)
70{
71// The size of the delta is presumably chosen to give
72// numerical stability. I don't know how the value was chosen.
73// It probably doesn't matter much. It gets divided out.
74 double delta(100.0); // in meters
75 SGGeod head, tail;
76 double az2; // ignored
77 SGGeodesy::direct(midpoint, heading, delta, head, az2);
78 SGGeodesy::direct(midpoint, 180+heading, delta, tail, az2);
79 head.setElevationM(midpoint.getElevationM());
80 tail.setElevationM(midpoint.getElevationM());
81 SGVec3d head_xyz = SGVec3d::fromGeod(head);
82 SGVec3d tail_xyz = SGVec3d::fromGeod(tail);
83// Awkward formula here, needed because vector-by-scalar
84// multiplication is defined, but not vector-by-scalar division.
85 return (head_xyz - tail_xyz) * (0.5/delta);
86}
87
88// Create a "serviceable" node with a default value of "true"
89SGPropertyNode_ptr createServiceableProp(SGPropertyNode* aParent,
90 const char* aName)
91{
92 SGPropertyNode_ptr n =
93 aParent->getChild(aName, 0, true)->getChild("serviceable", 0, true);
94 simgear::props::Type typ = n->getType();
95 if ((typ == simgear::props::NONE) || (typ == simgear::props::UNSPECIFIED)) {
96 n->setBoolValue(true);
97 }
98 return n;
99}
100
101static std::unique_ptr<SGInterpTable> static_terminalRangeInterp,
103
104// Constructor
105FGNavRadio::FGNavRadio(SGPropertyNode *node) :
106 play_count(0),
107 _nav_search(true),
108 _last_freq(0.0),
109 target_radial(0.0),
110 effective_range(0.0),
111 target_gs(0.0),
112 twist(0.0),
113 horiz_vel(0.0),
114 last_x(0.0),
115 last_xtrack_error(0.0),
116 xrate_ms(0.0),
117 _localizerWidth(5.0),
118 _time_before_search_sec(-1.0),
119 _gsCart(SGVec3d::zeros()),
120 _gsAxis(SGVec3d::zeros()),
121 _gsVertical(SGVec3d::zeros()),
122 _toFlag(false),
123 _fromFlag(false),
124 _cdiDeflection(0.0),
125 _cdiCrossTrackErrorM(0.0),
126 _gsNeedleDeflection(0.0),
127 _gsNeedleDeflectionNorm(0.0),
128 _audioIdent(NULL)
129{
130 // bug: for legacy compatability, all nav-radios should use the nav[0] power output
131 // https://sourceforge.net/p/flightgear/codetickets/2811/
132 setDefaultPowerSupplyPath("/systems/electrical/outputs/nav[0]");
133 readConfig(node, "nav");
134
135 if (!static_terminalRangeInterp.get()) {
136 // one-time interpolator init
137 SGPath path( globals->get_fg_root() );
138 SGPath term = path;
139 term.append( "Navaids/range.term" );
140 SGPath low = path;
141 low.append( "Navaids/range.low" );
142 SGPath high = path;
143 high.append( "Navaids/range.high" );
144
145 static_terminalRangeInterp.reset(new SGInterpTable(term));
146 static_lowRangeInterp.reset(new SGInterpTable(low));
147 static_highRangeInterp.reset(new SGInterpTable(high));
148 }
149
150 string branch = nodePath();
151 _radio_node = fgGetNode(branch, true);
152}
153
154
155// Destructor
157{
158 if (gps_course_node) {
159 gps_course_node->removeChangeListener(this);
160 }
161
162 if (nav_slaved_to_gps_node) {
163 nav_slaved_to_gps_node->removeChangeListener(this);
164 }
165
166 delete _audioIdent;
167}
168
169
170void
172{
173 SGPropertyNode* node = _radio_node.get();
174
176
177 // inputs
178 is_valid_node = node->getChild("data-is-valid", 0, true);
179 vol_btn_node = node->getChild("volume", 0, true);
180 ident_btn_node = node->getChild("ident", 0, true);
181 ident_btn_node->setBoolValue( true );
182 audio_btn_node = node->getChild("audio-btn", 0, true);
183 audio_btn_node->setBoolValue( true );
184 backcourse_node = node->getChild("back-course-btn", 0, true);
185 backcourse_node->setBoolValue( false );
186
187 nav_serviceable_node = node->getChild("serviceable", 0, true);
188 cdi_serviceable_node = createServiceableProp(node, "cdi");
189 gs_serviceable_node = createServiceableProp(node, "gs");
190 tofrom_serviceable_node = createServiceableProp(node, "to-from");
191
192 falseCoursesEnabledNode =
193 fgGetNode("/sim/realism/false-radio-courses-enabled");
194 if (!falseCoursesEnabledNode) {
195 falseCoursesEnabledNode =
196 fgGetNode("/sim/realism/false-radio-courses-enabled", true);
197 falseCoursesEnabledNode->setBoolValue(true);
198 }
199
200 // frequencies
201 SGPropertyNode *subnode = node->getChild("frequencies", 0, true);
202 freq_node = subnode->getChild("selected-mhz", 0, true);
203 alt_freq_node = subnode->getChild("standby-mhz", 0, true);
204 freq_node->addChangeListener(this);
205 alt_freq_node->addChangeListener(this);
206
207 fmt_freq_node = subnode->getChild("selected-mhz-fmt", 0, true);
208 fmt_alt_freq_node = subnode->getChild("standby-mhz-fmt", 0, true);
209 is_loc_freq_node = subnode->getChild("is-localizer-frequency", 0, true );
210
211 // radials
212 subnode = node->getChild("radials", 0, true);
213 sel_radial_node = subnode->getChild("selected-deg", 0, true);
214 radial_node = subnode->getChild("actual-deg", 0, true);
215 recip_radial_node = subnode->getChild("reciprocal-radial-deg", 0, true);
216 target_radial_true_node = subnode->getChild("target-radial-deg", 0, true);
217 target_auto_hdg_node = subnode->getChild("target-auto-hdg-deg", 0, true);
218
219 // outputs
220 heading_node = node->getChild("heading-deg", 0, true);
221 time_to_intercept = node->getChild("time-to-intercept-sec", 0, true);
222 to_flag_node = node->getChild("to-flag", 0, true);
223 from_flag_node = node->getChild("from-flag", 0, true);
224 inrange_node = node->getChild("in-range", 0, true);
225 signal_quality_norm_node = node->getChild("signal-quality-norm", 0, true);
226 cdi_deflection_node = node->getChild("heading-needle-deflection", 0, true);
227 cdi_deflection_norm_node = node->getChild("heading-needle-deflection-norm", 0, true);
228 cdi_xtrack_error_node = node->getChild("crosstrack-error-m", 0, true);
229 cdi_xtrack_hdg_err_node
230 = node->getChild("crosstrack-heading-error-deg", 0, true);
231 has_gs_node = node->getChild("has-gs", 0, true);
232 loc_node = node->getChild("nav-loc", 0, true);
233 loc_dist_node = node->getChild("nav-distance", 0, true);
234 gs_deflection_node = node->getChild("gs-needle-deflection", 0, true);
235 gs_deflection_deg_node = node->getChild("gs-needle-deflection-deg", 0, true);
236 gs_deflection_norm_node = node->getChild("gs-needle-deflection-norm", 0, true);
237 gs_direct_node = node->getChild("gs-direct-deg", 0, true);
238 gs_rate_of_climb_node = node->getChild("gs-rate-of-climb", 0, true);
239 gs_rate_of_climb_fpm_node = node->getChild("gs-rate-of-climb-fpm", 0, true);
240 gs_dist_node = node->getChild("gs-distance", 0, true);
241 gs_inrange_node = node->getChild("gs-in-range", 0, true);
242
243 nav_id_node = node->getChild("nav-id", 0, true);
244 id_c1_node = node->getChild("nav-id_asc1", 0, true);
245 id_c2_node = node->getChild("nav-id_asc2", 0, true);
246 id_c3_node = node->getChild("nav-id_asc3", 0, true);
247 id_c4_node = node->getChild("nav-id_asc4", 0, true);
248
249 // gps slaving support
250 nav_slaved_to_gps_node = node->getChild("slaved-to-gps", 0, true);
251 nav_slaved_to_gps_node->addChangeListener(this);
252
253 gps_cdi_deflection_node = fgGetNode("/instrumentation/gps/cdi-deflection", true);
254 gps_to_flag_node = fgGetNode("/instrumentation/gps/to-flag", true);
255 gps_from_flag_node = fgGetNode("/instrumentation/gps/from-flag", true);
256 gps_has_gs_node = fgGetNode("/instrumentation/gps/has-gs", true);
257 gps_course_node = fgGetNode("/instrumentation/gps/desired-course-deg", true);
258 gps_course_node->addChangeListener(this);
259
260 gps_xtrack_error_nm_node = fgGetNode("/instrumentation/gps/wp/wp[1]/course-error-nm", true);
261 _magvarNode = fgGetNode("/environment/magnetic-variation-deg", true);
262
263 std::ostringstream temp;
264 temp << name() << "-ident-" << number();
265 if( NULL == _audioIdent )
266 _audioIdent = new VORAudioIdent( temp.str() );
267 _audioIdent->init();
268
269 // dme-in-range is deprecated,
270 // temporarily create dme-in-range alias for instrumentation/dme[0]/in-range
271 // remove after flightgear 2.6.0
272 node->getNode("dme-in-range", true)->alias(fgGetNode("/instrumentation/dme[0]/in-range", true), false);
273}
274
275void
277{
278 _time_before_search_sec = -1.0;
279}
280
281// model standard VOR/DME/TACAN service volumes as per AIM 1-1-8
282double FGNavRadio::adjustNavRange( double stationElev, double aircraftElev,
283 double nominalRange )
284{
285 if (nominalRange <= 0.0) {
286 nominalRange = FG_NAV_DEFAULT_RANGE;
287 }
288
289 // extend out actual usable range to be 1.3x the published safe range
290 const double usability_factor = 1.3;
291
292 // assumptions we model the standard service volume, plus
293 // ... rather than specifying a cylinder, we model a cone that
294 // contains the cylinder. Then we put an upside down cone on top
295 // to model diminishing returns at too-high altitudes.
296
297 // altitude difference
298 double alt = ( aircraftElev * SG_METER_TO_FEET - stationElev );
299 // cout << "aircraft elev = " << aircraftElev * SG_METER_TO_FEET
300 // << " station elev = " << stationElev << endl;
301
302 if ( nominalRange < 25.0 + SG_EPSILON ) {
303 // Standard Terminal Service Volume
304 return static_terminalRangeInterp->interpolate( alt ) * usability_factor;
305 } else if ( nominalRange < 50.0 + SG_EPSILON ) {
306 // Standard Low Altitude Service Volume
307 // table is based on range of 40, scale to actual range
308 return static_lowRangeInterp->interpolate( alt ) * nominalRange / 40.0
309 * usability_factor;
310 } else {
311 // Standard High Altitude Service Volume
312 // table is based on range of 130, scale to actual range
313 return static_highRangeInterp->interpolate( alt ) * nominalRange / 130.0
314 * usability_factor;
315 }
316}
317
318
319// model standard ILS service volumes as per AIM 1-1-9
320double FGNavRadio::adjustILSRange( double stationElev, double aircraftElev,
321 double offsetDegrees, double distance )
322{
323 // assumptions we model the standard service volume, plus
324
325 // altitude difference
326 // double alt = ( aircraftElev * SG_METER_TO_FEET - stationElev );
327// double offset = fabs( offsetDegrees );
328
329// if ( offset < 10 ) {
330// return FG_ILS_DEFAULT_RANGE;
331// } else if ( offset < 35 ) {
332// return 10 + (35 - offset) * (FG_ILS_DEFAULT_RANGE - 10) / 25;
333// } else if ( offset < 45 ) {
334// return (45 - offset);
335// } else if ( offset > 170 ) {
336// return FG_ILS_DEFAULT_RANGE;
337// } else if ( offset > 145 ) {
338// return 10 + (offset - 145) * (FG_ILS_DEFAULT_RANGE - 10) / 25;
339// } else if ( offset > 135 ) {
340// return (offset - 135);
341// } else {
342// return 0;
343// }
345}
346
347// Frequencies with odd 100kHz numbers in the range from 108.00 - 111.95
348// are LOC/GS (ILS) frequency pairs
349// (108.00, 108.05, 108.20, 108.25.. =VOR)
350// (108.10, 108.15, 108.30, 108.35.. =ILS)
351static inline bool IsLocalizerFrequency( double f )
352{
353 if( f < 108.0 || f >= 112.00 ) return false;
354 return (((SGMiscd::roundToInt(f * 100.0) % 100)/10) % 2) != 0;
355}
356
357
359// Update the various nav values based on position and valid tuned in navs
361void
363{
364 if (dt <= 0.0) {
365 return; // paused
366 }
367
369 {
370 updateReceiver(dt);
371 updateCDI(dt);
372 } else {
373 clearOutputs();
374 }
375
376 updateAudio( dt );
377}
378
379void FGNavRadio::updateFormattedFrequencies()
380{
381 // Create "formatted" versions of the nav frequencies for
382 // instrument displays.
383 char tmp[16];
384 snprintf(tmp, 16, "%.2f", freq_node->getDoubleValue());
385 fmt_freq_node->setStringValue(tmp);
386 snprintf(tmp, 16, "%.2f", alt_freq_node->getDoubleValue());
387 fmt_alt_freq_node->setStringValue(tmp);
388 is_loc_freq_node->setBoolValue( IsLocalizerFrequency( freq_node->getDoubleValue() ));
389}
390
391void FGNavRadio::clearOutputs()
392{
393 inrange_node->setBoolValue( false );
394 signal_quality_norm_node->setDoubleValue( 0.0 );
395 cdi_deflection_node->setDoubleValue( 0.0 );
396 cdi_deflection_norm_node->setDoubleValue( 0.0 );
397 cdi_xtrack_error_node->setDoubleValue( 0.0 );
398 cdi_xtrack_hdg_err_node->setDoubleValue( 0.0 );
399 time_to_intercept->setDoubleValue( 0.0 );
400 heading_node->setDoubleValue(0.0);
401 gs_deflection_node->setDoubleValue( 0.0 );
402 gs_deflection_deg_node->setDoubleValue(0.0);
403 gs_deflection_norm_node->setDoubleValue(0.0);
404 gs_direct_node->setDoubleValue(0.0);
405 gs_inrange_node->setBoolValue( false );
406 loc_node->setBoolValue( false );
407 has_gs_node->setBoolValue(false);
408
409 to_flag_node->setBoolValue( false );
410 from_flag_node->setBoolValue( false );
411 is_valid_node->setBoolValue(false);
412 nav_id_node->setStringValue("");
413
414 _navaid = NULL;
415}
416
417void FGNavRadio::updateReceiver(double dt)
418{
419 SGVec3d aircraft = SGVec3d::fromGeod(globals->get_aircraft_position());
420 double loc_dist = 0;
421
422 // Do a nav station search only once a second to reduce
423 // unnecessary work. (Also, make sure to do this before caching
424 // any values!)
425 _time_before_search_sec -= dt;
426 if ( _time_before_search_sec < 0 ) {
427 search();
428 }
429
430 if (_navaid)
431 {
432 loc_dist = dist(aircraft, _navaid->cart());
433 loc_dist_node->setDoubleValue( loc_dist );
434 }
435
436 if (nav_slaved_to_gps_node->getBoolValue()) {
437 // when slaved to GPS: only allow stuff above: tune NAV station
438 // All other data driven by GPS only.
439 updateGPSSlaved();
440 return;
441 }
442
443 if (!_navaid) {
444 _cdiDeflection = 0.0;
445 _cdiCrossTrackErrorM = 0.0;
446 _toFlag = _fromFlag = false;
447 _gsNeedleDeflection = 0.0;
448 _gsNeedleDeflectionNorm = 0.0;
449 heading_node->setDoubleValue(0.0);
450 inrange_node->setBoolValue(false);
451 signal_quality_norm_node->setDoubleValue(0.0);
452 gs_dist_node->setDoubleValue( 0.0 );
453 gs_inrange_node->setBoolValue(false);
454 return;
455 }
456
457 double nav_elev = _navaid->get_elev_ft();
458
459 bool is_loc = loc_node->getBoolValue();
460 double signal_quality_norm = signal_quality_norm_node->getDoubleValue();
461
462 double az2, s;
464 // compute forward and reverse wgs84 headings to localizer
466 double hdg;
467 SGGeodesy::inverse(globals->get_aircraft_position(), _navaid->geod(), hdg, az2, s);
468 heading_node->setDoubleValue(hdg);
469 double radial = az2 - twist;
470 double recip = radial + 180.0;
471 SG_NORMALIZE_RANGE(recip, 0.0, 360.0);
472 radial_node->setDoubleValue( radial );
473 recip_radial_node->setDoubleValue( recip );
474
476 // compute the target/selected radial in "true" heading
478 if (!is_loc) {
479 target_radial = sel_radial_node->getDoubleValue();
480 }
481
482 // VORs need twist (mag-var) added; ILS/LOCs don't but we set twist to 0.0
483 double trtrue = target_radial + twist;
484 SG_NORMALIZE_RANGE(trtrue, 0.0, 360.0);
485 target_radial_true_node->setDoubleValue( trtrue );
486
488 // adjust reception range for altitude
490 if ( is_loc ) {
491 double offset = radial - target_radial;
492 SG_NORMALIZE_RANGE(offset, -180.0, 180.0);
493 effective_range
494 = adjustILSRange( nav_elev, globals->get_aircraft_position().getElevationM(), offset,
495 loc_dist * SG_METER_TO_NM );
496 } else {
497 effective_range
498 = adjustNavRange( nav_elev, globals->get_aircraft_position().getElevationM(), _navaid->get_range() );
499 }
500
501 double effective_range_m = effective_range * SG_NM_TO_METER;
502
504 // compute signal quality
505 // 100% within effective_range
506 // decreases 1/x^2 further out
508 double last_signal_quality_norm = signal_quality_norm;
509
510 if ( loc_dist < effective_range_m ) {
511 signal_quality_norm = 1.0;
512 } else {
513 double range_exceed_norm = loc_dist/effective_range_m;
514 signal_quality_norm = 1/(range_exceed_norm*range_exceed_norm);
515 }
516
517 if (_apply_lowpass_filter) {
518 signal_quality_norm = fgGetLowPass( last_signal_quality_norm,
519 signal_quality_norm, dt );
520 }
521 _apply_lowpass_filter = true;
522
523 signal_quality_norm_node->setDoubleValue( signal_quality_norm );
524 bool inrange = signal_quality_norm > 0.2;
525 inrange_node->setBoolValue( inrange );
526
528 // compute to/from flag status
530 if (inrange) {
531 if (is_loc) {
532 _toFlag = true;
533 } else {
534 double offset = fabs(radial - target_radial);
535 _toFlag = (offset > 90.0 && offset < 270.0);
536 }
537 _fromFlag = !_toFlag;
538 } else {
539 _toFlag = _fromFlag = false;
540 }
541
542 // CDI deflection
543 double r = target_radial - radial;
544 SG_NORMALIZE_RANGE(r, -180.0, 180.0);
545
546 if ( is_loc ) {
547 if (falseCoursesEnabledNode->getBoolValue()) {
548 // The factor of 30.0 gives a period of 120 which gives us 3 cycles and six
549 // zeros i.e. six courses: one front course, one back course, and four
550 // false courses. Three of the six are reverse sensing.
551 _cdiDeflection = 30.0 * sawtooth(r / 30.0);
552 } else {
553 // no false courses, but we do need to create a back course
554 if (fabs(r) > 90.0) { // front course
555 _cdiDeflection = r - copysign(180.0, r);
556 } else {
557 _cdiDeflection = r; // back course
558 }
559
560 _cdiDeflection = -_cdiDeflection; // reverse for outbound radial
561 } // of false courses disabled
562
563 const double VOR_FULL_ARC = 20.0; // VOR is -10 .. 10 degree swing
564 _cdiDeflection *= VOR_FULL_ARC / _localizerWidth; // increased localizer sensitivity
565
566 if (backcourse_node->getBoolValue()) {
567 _cdiDeflection = -_cdiDeflection;
568 }
569 } else {
570 // handle the TO side of the VOR
571 if (fabs(r) > 90.0) {
572 r = ( r<0.0 ? -r-180.0 : -r+180.0 );
573 }
574 _cdiDeflection = r;
575 } // of non-localizer case
576
577 SG_CLAMP_RANGE(_cdiDeflection, -10.0, 10.0 );
578 _cdiDeflection *= signal_quality_norm;
579
580 // cross-track error (in meters)
581 _cdiCrossTrackErrorM = loc_dist * sin(r * SGD_DEGREES_TO_RADIANS);
582
583 updateGlideSlope(dt, aircraft, signal_quality_norm);
584}
585
586void FGNavRadio::updateGlideSlope(double dt, const SGVec3d& aircraft, double signal_quality_norm)
587{
588 bool gsInRange = (_gs && inrange_node->getBoolValue());
589 double gsDist = 0;
590
591 if (gsInRange)
592 {
593 gsDist = dist(aircraft, _gsCart);
594 gsInRange = (gsDist < (_gs->get_range() * SG_NM_TO_METER));
595 }
596
597 gs_inrange_node->setBoolValue(gsInRange);
598 gs_dist_node->setDoubleValue( gsDist );
599
600 if (!gsInRange)
601 {
602 _gsNeedleDeflection = 0.0;
603 _gsNeedleDeflectionNorm = 0.0;
604 return;
605 }
606
607 SGVec3d pos = aircraft - _gsCart; // relative vector from gs antenna to aircraft
608 // The positive GS axis points along the runway in the landing direction,
609 // toward the far end, not toward the approach area, so we need a - sign here:
610 double comp_h = -dot(pos, _gsAxis); // component in horiz direction
611 double comp_v = dot(pos, _gsVertical); // component in vertical direction
612 //double comp_b = dot(pos, _gsBaseline); // component in baseline direction
613 //if (comp_b) {} // ... (useful for debugging)
614
615// _gsDirect represents the angle of elevation of the aircraft
616// as seen by the GS transmitter.
617 _gsDirect = atan2(comp_v, comp_h) * SGD_RADIANS_TO_DEGREES;
618// At this point, if the aircraft is centered on the glide slope,
619// _gsDirect will be a small positive number, e.g. 3.0 degrees
620
621// Aim the branch cut straight down
622// into the ground below the GS transmitter:
623 if (_gsDirect < -90.0) _gsDirect += 360.0;
624
625 double deflectionAngle = target_gs - _gsDirect;
626
627 if (falseCoursesEnabledNode->getBoolValue()) {
628 // Construct false glideslopes. The scale factor of 1.5
629 // in the sawtooth gives a period of 6 degrees.
630 // There will be zeros at 3, 6r, 9, 12r et cetera
631 // where "r" indicates reverse sensing.
632 // This is is consistent with conventional pilot lore
633 // e.g. http://www.allstar.fiu.edu/aerojava/ILS.htm
634 // but inconsistent with
635 // http://www.freepatentsonline.com/3757338.html
636 //
637 // It may be that some of each exist.
638 if (deflectionAngle < 0) {
639 deflectionAngle = 1.5 * sawtooth(deflectionAngle / 1.5);
640 } else {
641 // no false GS below the true GS
642 }
643 }
644
645// GS is documented to be 1.4 degrees thick,
646// i.e. plus or minus 0.7 degrees from the midline:
647 SG_CLAMP_RANGE(deflectionAngle, -0.7, 0.7);
648
649// Many older instrument xml frontends depend on
650// the un-normalized gs-needle-deflection.
651// Apparently the interface standard is plus or minus 3.5 "volts"
652// for a full-scale deflection:
653 _gsNeedleDeflection = deflectionAngle * 5.0;
654 _gsNeedleDeflection *= signal_quality_norm;
655
656 _gsNeedleDeflectionNorm = (deflectionAngle / 0.7) * signal_quality_norm;
657
659 // Calculate desired rate of climb for intercepting the GS
661 double gs_diff = target_gs - _gsDirect;
662 // convert desired vertical path angle into a climb rate
663 double des_angle = _gsDirect - 10 * gs_diff;
664 /* printf("target_gs=%.1f angle=%.1f gs_diff=%.1f des_angle=%.1f\n",
665 target_gs, _gsDirect, gs_diff, des_angle); */
666
667 // estimate horizontal speed towards ILS in meters per minute
668 double elapsedDistance = last_x - gsDist;
669 last_x = gsDist;
670
671 double new_vel = ( elapsedDistance / dt );
672 horiz_vel = 0.99 * horiz_vel + 0.01 * new_vel;
673 /* printf("vel=%.1f (dist=%.1f dt=%.2f)\n", horiz_vel, elapsedDistance, dt);*/
674
675 gs_rate_of_climb_node
676 ->setDoubleValue( -sin( des_angle * SGD_DEGREES_TO_RADIANS )
677 * horiz_vel * SG_METER_TO_FEET );
678 gs_rate_of_climb_fpm_node
679 ->setDoubleValue( gs_rate_of_climb_node->getDoubleValue() * 60 );
680}
681
682void FGNavRadio::valueChanged (SGPropertyNode* prop)
683{
684 if (prop == gps_course_node) {
685 if (!nav_slaved_to_gps_node->getBoolValue()) {
686 return;
687 }
688
689 // GPS desired course has changed, sync up our selected-course
690 double v = prop->getDoubleValue();
691 if (v != sel_radial_node->getDoubleValue()) {
692 sel_radial_node->setDoubleValue(v);
693 }
694 } else if (prop == nav_slaved_to_gps_node) {
695 if (prop->getBoolValue()) {
696 // slaved-to-GPS activated, clear obsolete NAV outputs and sync up selected course
697 clearOutputs();
698 sel_radial_node->setDoubleValue(gps_course_node->getDoubleValue());
699 }
700 // slave-to-GPS enabled/disabled, resync NAV station (update all outputs)
701 _navaid = NULL;
702 _apply_lowpass_filter = false;
703 _time_before_search_sec = 0;
704 } else if (prop == alt_freq_node) {
705 updateFormattedFrequencies();
706 } else if (prop == freq_node) {
707 updateFormattedFrequencies();
708 _apply_lowpass_filter = false; // signal quality allowed to vary quickly
709 _time_before_search_sec = 0.0; // trigger a new search() ASAP
710 }
711}
712
713void FGNavRadio::updateGPSSlaved()
714{
715 has_gs_node->setBoolValue(gps_has_gs_node->getBoolValue());
716
717 _toFlag = gps_to_flag_node->getBoolValue();
718 _fromFlag = gps_from_flag_node->getBoolValue();
719
720 bool gpsValid = (_toFlag | _fromFlag);
721 inrange_node->setBoolValue(gpsValid);
722 if (!gpsValid) {
723 signal_quality_norm_node->setDoubleValue(0.0);
724 _cdiDeflection = 0.0;
725 _cdiCrossTrackErrorM = 0.0;
726 _gsNeedleDeflection = 0.0;
727 _gsNeedleDeflectionNorm = 0.0;
728 return;
729 }
730
731 // this is unfortunate, but panel instruments use this value to decide
732 // if the navradio output is valid.
733 signal_quality_norm_node->setDoubleValue(1.0);
734
735 _cdiDeflection = gps_cdi_deflection_node->getDoubleValue();
736 // clmap to some range (+/- 10 degrees) as the regular deflection
737 SG_CLAMP_RANGE(_cdiDeflection, -10.0, 10.0 );
738
739 _cdiCrossTrackErrorM = gps_xtrack_error_nm_node->getDoubleValue() * SG_NM_TO_METER;
740 _gsNeedleDeflection = 0.0; // FIXME, supply this
741
742 double trtrue = gps_course_node->getDoubleValue() + _magvarNode->getDoubleValue();
743 SG_NORMALIZE_RANGE(trtrue, 0.0, 360.0);
744 target_radial_true_node->setDoubleValue( trtrue );
745}
746
747void FGNavRadio::updateCDI(double dt)
748{
749 bool cdi_serviceable = cdi_serviceable_node->getBoolValue();
750 bool inrange = inrange_node->getBoolValue();
751
752 if (tofrom_serviceable_node->getBoolValue()) {
753 to_flag_node->setBoolValue(_toFlag);
754 from_flag_node->setBoolValue(_fromFlag);
755 } else {
756 to_flag_node->setBoolValue(false);
757 from_flag_node->setBoolValue(false);
758 }
759
760 if (!cdi_serviceable) {
761 _cdiDeflection = 0.0;
762 _cdiCrossTrackErrorM = 0.0;
763 }
764
765 cdi_deflection_node->setDoubleValue(_cdiDeflection);
766 cdi_deflection_norm_node->setDoubleValue(_cdiDeflection * 0.1);
767 cdi_xtrack_error_node->setDoubleValue(_cdiCrossTrackErrorM);
768
770 // compute an approximate ground track heading error
772 double hdg_error = 0.0;
773 if ( inrange && cdi_serviceable ) {
774 double vn = fgGetDouble( "/velocities/speed-north-fps" );
775 double ve = fgGetDouble( "/velocities/speed-east-fps" );
776 double gnd_trk_true = atan2( ve, vn ) * SGD_RADIANS_TO_DEGREES;
777 if ( gnd_trk_true < 0.0 ) { gnd_trk_true += 360.0; }
778
779 SGPropertyNode *true_hdg
780 = fgGetNode("/orientation/heading-deg", true);
781 hdg_error = gnd_trk_true - true_hdg->getDoubleValue();
782
783 // cout << "ground track = " << gnd_trk_true
784 // << " orientation = " << true_hdg->getDoubleValue() << endl;
785 }
786 cdi_xtrack_hdg_err_node->setDoubleValue( hdg_error );
787
789 // Calculate a suggested target heading to smoothly intercept
790 // a nav/ils radial.
792
793 // Now that we have cross track heading adjustment built in,
794 // we shouldn't need to overdrive the heading angle within 8km
795 // of the station.
796 //
797 // The cdi deflection should be +/-10 for a full range of deflection
798 // so multiplying this by 3 gives us +/- 30 degrees heading
799 // compensation.
800 double adjustment = _cdiDeflection * 3.0;
801 SG_CLAMP_RANGE( adjustment, -30.0, 30.0 );
802
803 // determine the target heading to fly to intercept the
804 // tgt_radial = target radial (true) + cdi offset adjustment -
805 // xtrack heading error adjustment
806 double nta_hdg;
807 double trtrue = target_radial_true_node->getDoubleValue();
808 if ( loc_node->getBoolValue() && backcourse_node->getBoolValue() ) {
809 // tuned to a localizer and backcourse mode activated
810 trtrue += 180.0; // reverse the target localizer heading
811 SG_NORMALIZE_RANGE(trtrue, 0.0, 360.0);
812 nta_hdg = trtrue - adjustment - hdg_error;
813 } else {
814 nta_hdg = trtrue + adjustment - hdg_error;
815 }
816
817 SG_NORMALIZE_RANGE(nta_hdg, 0.0, 360.0);
818 target_auto_hdg_node->setDoubleValue( nta_hdg );
819
821 // compute the time to intercept selected radial (based on
822 // current and last cross track errors and dt)
824 double t = 0.0;
825 if ( inrange && cdi_serviceable ) {
826 double cur_rate = (last_xtrack_error - _cdiCrossTrackErrorM) / dt;
827 xrate_ms = 0.99 * xrate_ms + 0.01 * cur_rate;
828 if ( fabs(xrate_ms) > 0.00001 ) {
829 t = _cdiCrossTrackErrorM / xrate_ms;
830 } else {
831 t = 9999.9;
832 }
833 }
834 time_to_intercept->setDoubleValue( t );
835
836 if (!gs_serviceable_node->getBoolValue() ) {
837 _gsNeedleDeflection = 0.0;
838 _gsNeedleDeflectionNorm = 0.0;
839 }
840 gs_deflection_node->setDoubleValue(_gsNeedleDeflection);
841 gs_deflection_deg_node->setDoubleValue(_gsNeedleDeflectionNorm * 0.7);
842 gs_deflection_norm_node->setDoubleValue(_gsNeedleDeflectionNorm);
843 gs_direct_node->setDoubleValue(_gsDirect);
844
845 last_xtrack_error = _cdiCrossTrackErrorM;
846}
847
848void FGNavRadio::updateAudio( double dt )
849{
850 if (!_navaid || !inrange_node->getBoolValue() || !nav_serviceable_node->getBoolValue()) {
851 _audioIdent->setIdent("", 0.0 );
852 return;
853 }
854
855 // play station ident via audio system if on + ident,
856 // otherwise turn it off
858 || !ident_btn_node->getBoolValue()
859 || !audio_btn_node->getBoolValue() ) {
860 _audioIdent->setIdent("", 0.0 );
861 return;
862 }
863
864 _audioIdent->setIdent( _navaid->get_trans_ident(), vol_btn_node->getFloatValue() );
865
866 _audioIdent->update( dt );
867}
868
869FGNavRecord* FGNavRadio::findPrimaryNavaid(const SGGeod& aPos, double aFreqMHz)
870{
871 return FGNavList::findByFreq(aFreqMHz, aPos, FGNavList::navFilter());
872}
873
874// Update current nav/adf radio stations based on current position
876{
877 // set delay for next search
878 _time_before_search_sec = 1.0;
879
880 double freq = freq_node->getDoubleValue();
881
882 // immediate NAV search when frequency has changed (toggle between nav and g/s search otherwise)
883 _nav_search |= (_last_freq != freq);
884
885 // do we need to search a new NAV station in this iteration?
886 if (_nav_search)
887 {
888 _last_freq = freq;
889 FGNavRecord* nav = findPrimaryNavaid(globals->get_aircraft_position(), freq);
890 if (nav == _navaid) {
891 if (nav && (nav->type() != FGPositioned::VOR))
892 _nav_search = false; // search glideslope on next iteration
893 return; // nav hasn't changed, we're done
894 }
895 // remember new navaid station
896 _navaid = nav;
897 }
898
899 // search glideslope station
900 if ((_navaid.valid()) && (_navaid->type() != FGPositioned::VOR))
901 {
903 FGNavRecord* gs = FGNavList::findByFreq(freq, globals->get_aircraft_position(),
904 &gsFilter);
905 if ((!_nav_search) && (gs == _gs))
906 {
907 _nav_search = true; // search NAV on next iteration
908 return; // g/s hasn't changed, neither has nav - we're done
909 }
910 // remember new glideslope station
911 _gs = gs;
912 }
913
914 _nav_search = true; // search NAV on next iteration
915
916 // nav or gs station has changed
917 updateNav();
918}
919
920// Update current nav/adf/glideslope outputs when station has changed
922{
923 // update necessary, nav and/or gs has changed
924 FGNavRecord* nav = _navaid;
925 string identBuffer(4, ' ');
926 if (nav) {
927 nav_id_node->setStringValue(nav->get_ident());
928 identBuffer = simgear::strutils::rpad( nav->ident(), 4, ' ' );
929
930 effective_range = adjustNavRange(nav->get_elev_ft(), globals->get_aircraft_position().getElevationM(), nav->get_range());
931 loc_node->setBoolValue(nav->type() != FGPositioned::VOR);
932 twist = nav->get_multiuse();
933
934 if (nav->type() == FGPositioned::VOR) {
935 target_radial = sel_radial_node->getDoubleValue();
936 _gs = NULL;
937 } else { // ILS or LOC
938 _localizerWidth = nav->localizerWidth();
939 twist = 0.0;
940 effective_range = nav->get_range();
941
942 target_radial = nav->get_multiuse();
943 SG_NORMALIZE_RANGE(target_radial, 0.0, 360.0);
944
945 if (_gs) {
946 target_gs = _gs->glideSlopeAngleDeg();
947
948 double gs_radial = fmod(_gs->get_multiuse(), 1000.0);
949 SG_NORMALIZE_RANGE(gs_radial, 0.0, 360.0);
950 _gsCart = _gs->cart();
951
952 // GS axis unit tangent vector
953 // (along the runway):
954 _gsAxis = tangentVector(_gs->geod(), gs_radial);
955
956 // GS baseline unit tangent vector
957 // (transverse to the runway along the ground)
958 _gsBaseline = tangentVector(_gs->geod(), gs_radial + 90.0);
959 _gsVertical = cross(_gsBaseline, _gsAxis);
960 } // of have glideslope
961 } // of found LOC or ILS
962
963 } else { // found nothing
964 _gs = NULL;
965 nav_id_node->setStringValue("");
966 loc_node->setBoolValue(false);
967 _audioIdent->setIdent("", 0.0 );
968 }
969
970 has_gs_node->setBoolValue(_gs != NULL);
971 is_valid_node->setBoolValue(nav != NULL);
972 id_c1_node->setIntValue( (int)identBuffer[0] );
973 id_c2_node->setIntValue( (int)identBuffer[1] );
974 id_c3_node->setIntValue( (int)identBuffer[2] );
975 id_c4_node->setIntValue( (int)identBuffer[3] );
976}
977
978
979// Register the subsystem.
980#if 0
981SGSubsystemMgr::InstancedRegistrant<FGNavRadio> registrantFGNavRadio(
982 SGSubsystemMgr::FDM,
983 {{"instrumentation", SGSubsystemMgr::Dependency::HARD}});
984#endif
void initServicePowerProperties(SGPropertyNode *node)
std::string name() const
void readConfig(SGPropertyNode *config, std::string defaultName)
std::string nodePath() const
void setDefaultPowerSupplyPath(const std::string &p)
specify the default path to use to power the instrument, if it's non- standard.
bool isServiceableAndPowered() const
SGGeod get_aircraft_position() const
Definition globals.cxx:611
static FGNavRecordRef findByFreq(double freq, const SGGeod &position, TypeFilter *filter=nullptr)
Query the database for the specified station.
Definition navlist.cxx:187
static TypeFilter * navFilter()
filter matching VOR & ILS/LOC transmitters
Definition navlist.cxx:169
void updateNav()
Definition navradio.cxx:921
void reinit() override
Definition navradio.cxx:276
void init() override
Definition navradio.cxx:171
FGNavRadio(SGPropertyNode *node)
Definition navradio.cxx:105
void update(double dt) override
Definition navradio.cxx:362
void search()
Definition navradio.cxx:875
int get_range() const
Definition navrecord.hxx:77
const char * get_ident() const
Definition navrecord.hxx:80
double get_elev_ft() const
Definition navrecord.hxx:74
double get_multiuse() const
Definition navrecord.hxx:78
double localizerWidth() const
return the localizer width, in degrees computation is based up ICAO stdandard width at the runway thr...
Definition navrecord.cxx:63
Type type() const
const std::string & ident() const
FGGlobals * globals
Definition globals.cxx:142
static std::unique_ptr< SGInterpTable > static_lowRangeInterp
Definition navradio.cxx:102
static std::unique_ptr< SGInterpTable > static_terminalRangeInterp
Definition navradio.cxx:101
static double sawtooth(double xx)
Definition navradio.cxx:58
static std::unique_ptr< SGInterpTable > static_highRangeInterp
Definition navradio.cxx:102
static bool IsLocalizerFrequency(double f)
Definition navradio.cxx:351
SGPropertyNode_ptr createServiceableProp(SGPropertyNode *aParent, const char *aName)
Definition navradio.cxx:89
static SGVec3d tangentVector(const SGGeod &midpoint, const double heading)
Definition navradio.cxx:69
const double FG_NAV_DEFAULT_RANGE
Definition navrecord.hxx:36
const double FG_LOC_DEFAULT_RANGE
Definition navrecord.hxx:37
double fgGetDouble(const char *name, double defaultValue)
Get a double value for a property.
Definition proptest.cpp:30
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27
double fgGetLowPass(double current, double target, double timeratio)
Move a value towards a target.
Definition util.cxx:46