c7423cbcea
- Fixed triggering of debug assertion "altitude_callout_voice != NULL" when altitude callouts were delayed by higher priority warnings. - Fixed performance bug: MK_VIII::IOHandler::TerrainClearanceFilter::update sucked CPU time (top #12 in profiler), due to complex "moving average" calculation on a queue of hundreds/thousands of altitude samples. Also, pausing the sim caused this queue to grow excessively (by one sample per update loop) => resuming the sim after pausing it for a long time resulted in the TerrainClearanceFilter sucking even more CPU time... => Number of altitude samples is now limited to 5 samples/sim-time-second (still more than enough to calculate a stable average).
4971 lines
140 KiB
C++
Executable file
4971 lines
140 KiB
C++
Executable file
// mk_viii.cxx -- Honeywell MK VIII EGPWS emulation
|
|
//
|
|
// Written by Jean-Yves Lefort, started September 2005.
|
|
//
|
|
// Copyright (C) 2005, 2006 Jean-Yves Lefort - jylefort@FreeBSD.org
|
|
//
|
|
// 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, write to the Free Software
|
|
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// References:
|
|
//
|
|
// [PILOT] Honeywell International Inc., "MK VI and MK VIII,
|
|
// Enhanced Ground Proximity Warning System (EGPWS),
|
|
// Pilot's Guide", May 2004
|
|
//
|
|
// http://www.egpws.com/engineering_support/documents/pilot_guides/060-4314-000.pdf
|
|
//
|
|
// [SPEC] Honeywell International Inc., "Product Specification
|
|
// for the MK VI and MK VIII Enhanced Ground Proximity
|
|
// Warning System (EGPWS)", June 2004
|
|
//
|
|
// http://www.egpws.com/engineering_support/documents/specs/965-1180-601.pdf
|
|
//
|
|
// [INSTALL] Honeywell Inc., "MK VI, MK VIII, Enhanced Ground
|
|
// Proximity Warning System (Class A TAWS), Installation
|
|
// Design Guide", July 2003
|
|
//
|
|
// http://www.egpws.com/engineering_support/documents/install/060-4314-150.pdf
|
|
//
|
|
// Notes:
|
|
//
|
|
// [1] [SPEC] does not specify the "must be airborne"
|
|
// condition; we use it to make sure that the alert
|
|
// will not trigger when on the ground, since it would
|
|
// make no sense.
|
|
|
|
#ifdef _MSC_VER
|
|
# pragma warning( disable: 4355 )
|
|
#endif
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <math.h>
|
|
|
|
#include <string>
|
|
#include <sstream>
|
|
|
|
#include <simgear/constants.h>
|
|
#include <simgear/sg_inlines.h>
|
|
#include <simgear/debug/logstream.hxx>
|
|
#include <simgear/math/sg_geodesy.hxx>
|
|
#include <simgear/math/sg_random.h>
|
|
#include <simgear/misc/sg_path.hxx>
|
|
#include <simgear/sound/soundmgr_openal.hxx>
|
|
#include <simgear/structure/exception.hxx>
|
|
|
|
using std::string;
|
|
|
|
#include <Airports/runways.hxx>
|
|
#include <Airports/simple.hxx>
|
|
#ifndef _MSC_VER
|
|
# include <Include/version.h>
|
|
#endif
|
|
#include <Main/fg_props.hxx>
|
|
#include <Main/globals.hxx>
|
|
#include "instrument_mgr.hxx"
|
|
#include "mk_viii.hxx"
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// constants //////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
#define GLIDESLOPE_DOTS_TO_DDM 0.0875 // [INSTALL] 6.2.30
|
|
#define GLIDESLOPE_DDM_TO_DOTS (1 / GLIDESLOPE_DOTS_TO_DDM)
|
|
|
|
#define LOCALIZER_DOTS_TO_DDM 0.0775 // [INSTALL] 6.2.33
|
|
#define LOCALIZER_DDM_TO_DOTS (1 / LOCALIZER_DOTS_TO_DDM)
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// helpers ////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
#define assert_not_reached() assert(false)
|
|
#define n_elements(_array) (sizeof(_array) / sizeof((_array)[0]))
|
|
#define test_bits(_bits, _test) (((_bits) & (_test)) != 0)
|
|
|
|
#define mk_node(_name) (mk->properties_handler.external_properties._name)
|
|
|
|
#define mk_dinput_feed(_name) (mk->io_handler.input_feeders.discretes._name)
|
|
#define mk_dinput(_name) (mk->io_handler.inputs.discretes._name)
|
|
#define mk_ainput_feed(_name) (mk->io_handler.input_feeders.arinc429._name)
|
|
#define mk_ainput(_name) (mk->io_handler.inputs.arinc429._name)
|
|
#define mk_doutput(_name) (mk->io_handler.outputs.discretes._name)
|
|
#define mk_aoutput(_name) (mk->io_handler.outputs.arinc429._name)
|
|
#define mk_data(_name) (mk->io_handler.data._name)
|
|
|
|
#define mk_voice(_name) (mk->voice_player.voices._name)
|
|
#define mk_altitude_voice(_i) (mk->voice_player.voices.altitude_callouts[(_i)])
|
|
|
|
#define mk_alert(_name) (AlertHandler::ALERT_ ## _name)
|
|
#define mk_alert_flag(_name) (AlertHandler::ALERT_FLAG_ ## _name)
|
|
#define mk_set_alerts (mk->alert_handler.set_alerts)
|
|
#define mk_unset_alerts (mk->alert_handler.unset_alerts)
|
|
#define mk_repeat_alert (mk->alert_handler.repeat_alert)
|
|
#define mk_test_alert(_name) (mk_test_alerts(mk_alert(_name)))
|
|
#define mk_test_alerts(_test) (test_bits(mk->alert_handler.alerts, (_test)))
|
|
|
|
const double MK_VIII::TCFHandler::k = 0.25;
|
|
|
|
static double
|
|
modify_amplitude (double amplitude, double dB)
|
|
{
|
|
return amplitude * pow(10.0, dB / 20.0);
|
|
}
|
|
|
|
static double
|
|
heading_add (double h1, double h2)
|
|
{
|
|
double result = h1 + h2;
|
|
if (result >= 360)
|
|
result -= 360;
|
|
return result;
|
|
}
|
|
|
|
static double
|
|
heading_substract (double h1, double h2)
|
|
{
|
|
double result = h1 - h2;
|
|
if (result < 0)
|
|
result += 360;
|
|
return result;
|
|
}
|
|
|
|
static double
|
|
get_heading_difference (double h1, double h2)
|
|
{
|
|
double diff = h1 - h2;
|
|
|
|
if (diff < -180)
|
|
diff += 360;
|
|
else if (diff > 180)
|
|
diff -= 360;
|
|
|
|
return fabs(diff);
|
|
}
|
|
|
|
static double
|
|
get_reciprocal_heading (double h)
|
|
{
|
|
return heading_add(h, 180);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// PropertiesHandler //////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
MK_VIII::PropertiesHandler::init ()
|
|
{
|
|
mk_node(ai_caged) = fgGetNode("/instrumentation/attitude-indicator/caged-flag", true);
|
|
mk_node(ai_roll) = fgGetNode("/instrumentation/attitude-indicator/indicated-roll-deg", true);
|
|
mk_node(ai_serviceable) = fgGetNode("/instrumentation/attitude-indicator/serviceable", true);
|
|
mk_node(altimeter_altitude) = fgGetNode("/instrumentation/altimeter/indicated-altitude-ft", true);
|
|
mk_node(altimeter_serviceable) = fgGetNode("/instrumentation/altimeter/serviceable", true);
|
|
mk_node(altitude) = fgGetNode("/position/altitude-ft", true);
|
|
mk_node(altitude_agl) = fgGetNode("/position/altitude-agl-ft", true);
|
|
mk_node(altitude_gear_agl) = fgGetNode("/position/gear-agl-ft", true);
|
|
mk_node(orientation_roll) = fgGetNode("/orientation/roll-deg", true);
|
|
mk_node(asi_serviceable) = fgGetNode("/instrumentation/airspeed-indicator/serviceable", true);
|
|
mk_node(asi_speed) = fgGetNode("/instrumentation/airspeed-indicator/indicated-speed-kt", true);
|
|
mk_node(autopilot_heading_lock) = fgGetNode("/autopilot/locks/heading", true);
|
|
mk_node(flaps) = fgGetNode("/controls/flight/flaps", true);
|
|
mk_node(gear_down) = fgGetNode("/controls/gear/gear-down", true);
|
|
mk_node(latitude) = fgGetNode("/position/latitude-deg", true);
|
|
mk_node(longitude) = fgGetNode("/position/longitude-deg", true);
|
|
mk_node(nav0_cdi_serviceable) = fgGetNode("/instrumentation/nav/cdi/serviceable", true);
|
|
mk_node(nav0_gs_distance) = fgGetNode("/instrumentation/nav/gs-distance", true);
|
|
mk_node(nav0_gs_needle_deflection) = fgGetNode("/instrumentation/nav/gs-needle-deflection", true);
|
|
mk_node(nav0_gs_serviceable) = fgGetNode("/instrumentation/nav/gs/serviceable", true);
|
|
mk_node(nav0_has_gs) = fgGetNode("/instrumentation/nav/has-gs", true);
|
|
mk_node(nav0_heading_needle_deflection) = fgGetNode("/instrumentation/nav/heading-needle-deflection", true);
|
|
mk_node(nav0_in_range) = fgGetNode("/instrumentation/nav/in-range", true);
|
|
mk_node(nav0_nav_loc) = fgGetNode("/instrumentation/nav/nav-loc", true);
|
|
mk_node(nav0_serviceable) = fgGetNode("/instrumentation/nav/serviceable", true);
|
|
mk_node(power) = fgGetNode(("/systems/electrical/outputs/" + mk->name).c_str(), mk->num, true);
|
|
mk_node(replay_state) = fgGetNode("/sim/freeze/replay-state", true);
|
|
mk_node(vs) = fgGetNode("/velocities/vertical-speed-fps", true);
|
|
}
|
|
|
|
void
|
|
MK_VIII::PropertiesHandler::unbind ()
|
|
{
|
|
vector<SGPropertyNode_ptr>::iterator iter;
|
|
|
|
for (iter = tied_properties.begin(); iter != tied_properties.end(); iter++)
|
|
(*iter)->untie();
|
|
|
|
tied_properties.clear();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// PowerHandler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
MK_VIII::PowerHandler::bind (SGPropertyNode *node)
|
|
{
|
|
mk->properties_handler.tie(node, "serviceable", SGRawValuePointer<bool>(&serviceable));
|
|
}
|
|
|
|
bool
|
|
MK_VIII::PowerHandler::handle_abnormal_voltage (bool abnormal,
|
|
Timer *timer,
|
|
double max_duration)
|
|
{
|
|
if (abnormal)
|
|
{
|
|
if (timer->start_or_elapsed() >= max_duration)
|
|
return true; // power loss
|
|
}
|
|
else
|
|
timer->stop();
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::PowerHandler::update ()
|
|
{
|
|
double voltage = mk_node(power)->getDoubleValue();
|
|
bool now_powered = true;
|
|
|
|
// [SPEC] 3.2.1
|
|
|
|
if (! serviceable)
|
|
now_powered = false;
|
|
if (voltage < 15)
|
|
now_powered = false;
|
|
if (handle_abnormal_voltage(voltage < 20.5, &low_surge_timer, 0.03))
|
|
now_powered = false;
|
|
if (handle_abnormal_voltage(voltage < 22.0 || voltage > 30.3, &abnormal_timer, 300))
|
|
now_powered = false;
|
|
if (handle_abnormal_voltage(voltage > 32.2, &high_surge_timer, 1))
|
|
now_powered = false;
|
|
if (handle_abnormal_voltage(voltage > 37.8, &very_high_surge_timer, 0.1))
|
|
now_powered = false;
|
|
if (voltage > 46.3)
|
|
now_powered = false;
|
|
|
|
if (powered)
|
|
{
|
|
// [SPEC] 3.5.2
|
|
|
|
if (now_powered)
|
|
power_loss_timer.stop();
|
|
else
|
|
{
|
|
if (power_loss_timer.start_or_elapsed() >= 0.2)
|
|
power_off();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (now_powered)
|
|
power_on();
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::PowerHandler::power_on ()
|
|
{
|
|
powered = true;
|
|
mk->system_handler.power_on();
|
|
}
|
|
|
|
void
|
|
MK_VIII::PowerHandler::power_off ()
|
|
{
|
|
powered = false;
|
|
mk->system_handler.power_off();
|
|
mk->voice_player.stop(VoicePlayer::STOP_NOW);
|
|
mk->self_test_handler.power_off(); // run before IOHandler::power_off()
|
|
mk->io_handler.power_off();
|
|
mk->mode2_handler.power_off();
|
|
mk->mode6_handler.power_off();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// SystemHandler //////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
MK_VIII::SystemHandler::power_on ()
|
|
{
|
|
state = STATE_BOOTING;
|
|
|
|
// [SPEC] 3.5.2 mentions a 20 seconds maximum boot delay. We use a
|
|
// random delay between 3 and 5 seconds.
|
|
|
|
boot_delay = 3 + sg_random() * 2;
|
|
boot_timer.start();
|
|
}
|
|
|
|
void
|
|
MK_VIII::SystemHandler::power_off ()
|
|
{
|
|
state = STATE_OFF;
|
|
|
|
boot_timer.stop();
|
|
}
|
|
|
|
void
|
|
MK_VIII::SystemHandler::update ()
|
|
{
|
|
if (state == STATE_BOOTING)
|
|
{
|
|
if (boot_timer.elapsed() >= boot_delay)
|
|
{
|
|
last_replay_state = mk_node(replay_state)->getIntValue();
|
|
|
|
mk->configuration_module.boot();
|
|
|
|
mk->io_handler.boot();
|
|
mk->fault_handler.boot();
|
|
mk->alert_handler.boot();
|
|
|
|
// inputs are needed by the following boot() routines
|
|
mk->io_handler.update_inputs();
|
|
|
|
mk->mode2_handler.boot();
|
|
mk->mode6_handler.boot();
|
|
|
|
state = STATE_ON;
|
|
|
|
mk->io_handler.post_boot();
|
|
}
|
|
}
|
|
else if (state != STATE_OFF && mk->configuration_module.state == ConfigurationModule::STATE_OK)
|
|
{
|
|
// If the replay state changes, switch to reposition mode for 3
|
|
// seconds ([SPEC] 6.0.5) to avoid spurious alerts.
|
|
|
|
int replay_state = mk_node(replay_state)->getIntValue();
|
|
if (replay_state != last_replay_state)
|
|
{
|
|
mk->alert_handler.reposition();
|
|
|
|
last_replay_state = replay_state;
|
|
state = STATE_REPOSITION;
|
|
reposition_timer.start();
|
|
}
|
|
|
|
if (state == STATE_REPOSITION && reposition_timer.elapsed() >= 3)
|
|
{
|
|
// inputs are needed by StateHandler::post_reposition()
|
|
mk->io_handler.update_inputs();
|
|
|
|
mk->state_handler.post_reposition();
|
|
|
|
state = STATE_ON;
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// ConfigurationModule ////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
MK_VIII::ConfigurationModule::ConfigurationModule (MK_VIII *device)
|
|
: mk(device)
|
|
{
|
|
// arbitrary defaults
|
|
categories[CATEGORY_AIRCRAFT_MODE_TYPE_SELECT] = 0;
|
|
categories[CATEGORY_AIR_DATA_INPUT_SELECT] = 1;
|
|
categories[CATEGORY_POSITION_INPUT_SELECT] = 2;
|
|
categories[CATEGORY_ALTITUDE_CALLOUTS] = 13;
|
|
categories[CATEGORY_AUDIO_MENU_SELECT] = 0;
|
|
categories[CATEGORY_TERRAIN_DISPLAY_SELECT] = 1;
|
|
categories[CATEGORY_OPTIONS_SELECT_GROUP_1] = 124;
|
|
categories[CATEGORY_RADIO_ALTITUDE_INPUT_SELECT] = 2;
|
|
categories[CATEGORY_NAVIGATION_INPUT_SELECT] = 3;
|
|
categories[CATEGORY_ATTITUDE_INPUT_SELECT] = 6;
|
|
categories[CATEGORY_HEADING_INPUT_SELECT] = 2;
|
|
categories[CATEGORY_WINDSHEAR_INPUT_SELECT] = 0;
|
|
categories[CATEGORY_INPUT_OUTPUT_DISCRETE_TYPE_SELECT] = 7;
|
|
categories[CATEGORY_AUDIO_OUTPUT_LEVEL] = 0;
|
|
categories[CATEGORY_UNDEFINED_INPUT_SELECT_1] = 0;
|
|
categories[CATEGORY_UNDEFINED_INPUT_SELECT_2] = 0;
|
|
categories[CATEGORY_UNDEFINED_INPUT_SELECT_3] = 0;
|
|
}
|
|
|
|
static double m1_t1_min_agl1 (double vs) { return -1620 - 1.1133 * vs; }
|
|
static double m1_t1_min_agl2 (double vs) { return -400 - 0.4 * vs; }
|
|
static double m1_t4_min_agl1 (double vs) { return -1625.47 - 1.1167 * vs; }
|
|
static double m1_t4_min_agl2 (double vs) { return -0.1958 * vs; }
|
|
|
|
static int m3_t1_max_agl (bool flap_override) { return 1500; }
|
|
static double m3_t1_max_alt_loss (bool flap_override, double agl) { return 5.4 + 0.092 * agl; }
|
|
static int m3_t2_max_agl (bool flap_override) { return flap_override ? 815 : 925; }
|
|
static double m3_t2_max_alt_loss (bool flap_override, double agl)
|
|
{
|
|
int bias = agl > 700 ? 5 : 0;
|
|
|
|
if (flap_override)
|
|
return (9.0 + 0.184 * agl) + bias;
|
|
else
|
|
return (5.4 + 0.092 * agl) + bias;
|
|
}
|
|
|
|
static double m4_t1_min_agl2 (double airspeed) { return -1083 + 8.333 * airspeed; }
|
|
static double m4_t568_a_min_agl2 (double airspeed) { return -1523 + 11.36 * airspeed; }
|
|
static double m4_t79_a_min_agl2 (double airspeed) { return -1182 + 11.36 * airspeed; }
|
|
static double m4_t568_b_min_agl2 (double airspeed) { return -1570 + 11.6 * airspeed; }
|
|
static double m4_t79_b_min_agl2 (double airspeed) { return -1222 + 11.6 * airspeed; }
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::m6_t2_is_bank_angle (Parameter<double> *agl,
|
|
double abs_roll_deg,
|
|
bool ap_engaged)
|
|
{
|
|
if (ap_engaged)
|
|
{
|
|
if (agl->ncd || agl->get() > 122)
|
|
return abs_roll_deg > 33;
|
|
}
|
|
else
|
|
{
|
|
if (agl->ncd || agl->get() > 2450)
|
|
return abs_roll_deg > 55;
|
|
else if (agl->get() > 150)
|
|
return agl->get() < 153.33333 * abs_roll_deg - 5983.3333;
|
|
}
|
|
|
|
if (agl->get() > 30)
|
|
return agl->get() < 4 * abs_roll_deg - 10;
|
|
else if (agl->get() > 5)
|
|
return abs_roll_deg > 10;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::m6_t4_is_bank_angle (Parameter<double> *agl,
|
|
double abs_roll_deg,
|
|
bool ap_engaged)
|
|
{
|
|
if (ap_engaged)
|
|
{
|
|
if (agl->ncd || agl->get() > 156)
|
|
return abs_roll_deg > 33;
|
|
}
|
|
else
|
|
{
|
|
if (agl->ncd || agl->get() > 210)
|
|
return abs_roll_deg > 50;
|
|
}
|
|
|
|
if (agl->get() > 10)
|
|
return agl->get() < 5.7142857 * abs_roll_deg - 75.714286;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_aircraft_mode_type_select (int value)
|
|
{
|
|
static const Mode1Handler::EnvelopesConfiguration m1_t1 =
|
|
{ false, 10, m1_t1_min_agl1, 284, m1_t1_min_agl2, 2450 };
|
|
static const Mode1Handler::EnvelopesConfiguration m1_t4 =
|
|
{ true, 50, m1_t4_min_agl1, 346, m1_t4_min_agl2, 1958 };
|
|
|
|
static const Mode2Handler::Configuration m2_t1 = { 190, 280 };
|
|
static const Mode2Handler::Configuration m2_t4 = { 220, 310 };
|
|
static const Mode2Handler::Configuration m2_t5 = { 220, 310 };
|
|
|
|
static const Mode3Handler::Configuration m3_t1 =
|
|
{ 30, m3_t1_max_agl, m3_t1_max_alt_loss };
|
|
static const Mode3Handler::Configuration m3_t2 =
|
|
{ 50, m3_t2_max_agl, m3_t2_max_alt_loss };
|
|
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t1_ac =
|
|
{ 190, 250, 500, m4_t1_min_agl2, 1000 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t5_ac =
|
|
{ 178, 200, 500, m4_t568_a_min_agl2, 1000 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t68_ac =
|
|
{ 178, 200, 500, m4_t568_a_min_agl2, 750 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t79_ac =
|
|
{ 148, 170, 500, m4_t79_a_min_agl2, 750 };
|
|
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t1_b =
|
|
{ 159, 250, 245, m4_t1_min_agl2, 1000 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t5_b =
|
|
{ 148, 200, 200, m4_t568_b_min_agl2, 1000 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t6_b =
|
|
{ 150, 200, 170, m4_t568_b_min_agl2, 750 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t7_b =
|
|
{ 120, 170, 170, m4_t79_b_min_agl2, 750 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t8_b =
|
|
{ 148, 200, 150, m4_t568_b_min_agl2, 750 };
|
|
static const Mode4Handler::EnvelopesConfiguration m4_t9_b =
|
|
{ 118, 170, 150, m4_t79_b_min_agl2, 750 };
|
|
|
|
static const Mode4Handler::ModesConfiguration m4_t1 = { &m4_t1_ac, &m4_t1_b };
|
|
static const Mode4Handler::ModesConfiguration m4_t5 = { &m4_t5_ac, &m4_t5_b };
|
|
static const Mode4Handler::ModesConfiguration m4_t6 = { &m4_t68_ac, &m4_t6_b };
|
|
static const Mode4Handler::ModesConfiguration m4_t7 = { &m4_t79_ac, &m4_t7_b };
|
|
static const Mode4Handler::ModesConfiguration m4_t8 = { &m4_t68_ac, &m4_t8_b };
|
|
static const Mode4Handler::ModesConfiguration m4_t9 = { &m4_t79_ac, &m4_t9_b };
|
|
|
|
static Mode6Handler::BankAnglePredicate m6_t2 = m6_t2_is_bank_angle;
|
|
static Mode6Handler::BankAnglePredicate m6_t4 = m6_t4_is_bank_angle;
|
|
|
|
static const IOHandler::FaultsConfiguration f_slow = { 180, 200 };
|
|
static const IOHandler::FaultsConfiguration f_fast = { 250, 290 };
|
|
|
|
static const struct
|
|
{
|
|
int type;
|
|
const Mode1Handler::EnvelopesConfiguration *m1;
|
|
const Mode2Handler::Configuration *m2;
|
|
const Mode3Handler::Configuration *m3;
|
|
const Mode4Handler::ModesConfiguration *m4;
|
|
Mode6Handler::BankAnglePredicate m6;
|
|
const IOHandler::FaultsConfiguration *f;
|
|
int runway_database;
|
|
} aircraft_types[] = {
|
|
{ 0, &m1_t4, &m2_t4, &m3_t2, &m4_t6, m6_t4, &f_fast, 2000 },
|
|
{ 1, &m1_t4, &m2_t4, &m3_t2, &m4_t8, m6_t4, &f_fast, 2000 },
|
|
{ 2, &m1_t4, &m2_t4, &m3_t2, &m4_t5, m6_t4, &f_fast, 2000 },
|
|
{ 3, &m1_t4, &m2_t5, &m3_t2, &m4_t7, m6_t4, &f_slow, 2000 },
|
|
{ 4, &m1_t4, &m2_t5, &m3_t2, &m4_t9, m6_t4, &f_slow, 2000 },
|
|
{ 254, &m1_t1, &m2_t1, &m3_t1, &m4_t1, m6_t2, &f_fast, 3500 },
|
|
{ 255, &m1_t1, &m2_t1, &m3_t1, &m4_t1, m6_t2, &f_fast, 2000 }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(aircraft_types); i++)
|
|
if (aircraft_types[i].type == value)
|
|
{
|
|
mk->mode1_handler.conf.envelopes = aircraft_types[i].m1;
|
|
mk->mode2_handler.conf = aircraft_types[i].m2;
|
|
mk->mode3_handler.conf = aircraft_types[i].m3;
|
|
mk->mode4_handler.conf.modes = aircraft_types[i].m4;
|
|
mk->mode6_handler.conf.is_bank_angle = aircraft_types[i].m6;
|
|
mk->io_handler.conf.faults = aircraft_types[i].f;
|
|
mk->conf.runway_database = aircraft_types[i].runway_database;
|
|
return true;
|
|
}
|
|
|
|
state = STATE_INVALID_AIRCRAFT_TYPE;
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_air_data_input_select (int value)
|
|
{
|
|
// unimplemented
|
|
return (value >= 0 && value <= 6) || (value >= 10 && value <= 13) || value == 255;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_position_input_select (int value)
|
|
{
|
|
if (value == 2)
|
|
mk->io_handler.conf.use_internal_gps = true;
|
|
else if ((value >= 0 && value <= 5)
|
|
|| (value >= 10 && value <= 13)
|
|
|| (value == 253)
|
|
|| (value == 255))
|
|
mk->io_handler.conf.use_internal_gps = false;
|
|
else
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_altitude_callouts (int value)
|
|
{
|
|
enum
|
|
{
|
|
MINIMUMS = -1,
|
|
SMART_500 = -2,
|
|
FIELD_500 = -3,
|
|
FIELD_500_ABOVE = -4
|
|
};
|
|
|
|
static const struct
|
|
{
|
|
int id;
|
|
int callouts[13];
|
|
} values[] = {
|
|
{ 0, { MINIMUMS, SMART_500, 200, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 1, { MINIMUMS, SMART_500, 200, 0 } },
|
|
{ 2, { MINIMUMS, SMART_500, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 3, { MINIMUMS, SMART_500, 0 } },
|
|
{ 4, { MINIMUMS, 200, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 5, { MINIMUMS, 200, 0 } },
|
|
{ 6, { MINIMUMS, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 7, { 0 } },
|
|
{ 8, { MINIMUMS, 0 } },
|
|
{ 9, { MINIMUMS, 500, 200, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 10, { MINIMUMS, 500, 200, 0 } },
|
|
{ 11, { MINIMUMS, 500, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 12, { MINIMUMS, 500, 0 } },
|
|
{ 13, { MINIMUMS, 1000, 500, 400, 300, 200, 100, 50, 40, 30, 20, 10, 0 } },
|
|
{ 14, { MINIMUMS, 100, 0 } },
|
|
{ 15, { MINIMUMS, 200, 100, 0 } },
|
|
{ 100, { FIELD_500, 0 } },
|
|
{ 101, { FIELD_500_ABOVE, 0 } }
|
|
};
|
|
|
|
unsigned i;
|
|
|
|
mk->mode6_handler.conf.minimums_enabled = false;
|
|
mk->mode6_handler.conf.smart_500_enabled = false;
|
|
mk->mode6_handler.conf.above_field_voice = NULL;
|
|
for (i = 0; i < n_altitude_callouts; i++)
|
|
mk->mode6_handler.conf.altitude_callouts_enabled[i] = false;
|
|
|
|
for (i = 0; i < n_elements(values); i++)
|
|
if (values[i].id == value)
|
|
{
|
|
for (int j = 0; values[i].callouts[j] != 0; j++)
|
|
switch (values[i].callouts[j])
|
|
{
|
|
case MINIMUMS:
|
|
mk->mode6_handler.conf.minimums_enabled = true;
|
|
break;
|
|
|
|
case SMART_500:
|
|
mk->mode6_handler.conf.smart_500_enabled = true;
|
|
break;
|
|
|
|
case FIELD_500:
|
|
mk->mode6_handler.conf.above_field_voice = mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_500);
|
|
break;
|
|
|
|
case FIELD_500_ABOVE:
|
|
mk->mode6_handler.conf.above_field_voice = mk_voice(five_hundred_above);
|
|
break;
|
|
|
|
default:
|
|
for (unsigned k = 0; k < n_altitude_callouts; k++)
|
|
if (mk->mode6_handler.altitude_callout_definitions[k] == values[i].callouts[j])
|
|
mk->mode6_handler.conf.altitude_callouts_enabled[k] = true;
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_audio_menu_select (int value)
|
|
{
|
|
if (value == 0 || value == 1)
|
|
mk->mode4_handler.conf.voice_too_low_gear = mk_voice(too_low_gear);
|
|
else if (value == 2 || value == 3)
|
|
mk->mode4_handler.conf.voice_too_low_gear = mk_voice(too_low_flaps);
|
|
else
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_terrain_display_select (int value)
|
|
{
|
|
if (value == 2)
|
|
mk->tcf_handler.conf.enabled = false;
|
|
else if (value == 0 || value == 1 || (value >= 3 && value <= 15)
|
|
|| (value >= 18 && value <= 20) || (value >= 235 && value <= 255))
|
|
mk->tcf_handler.conf.enabled = true;
|
|
else
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_options_select_group_1 (int value)
|
|
{
|
|
if (value >= 0 && value < 128)
|
|
{
|
|
mk->io_handler.conf.flap_reversal = test_bits(value, 1 << 1);
|
|
mk->mode6_handler.conf.bank_angle_enabled = test_bits(value, 1 << 2);
|
|
mk->io_handler.conf.steep_approach_enabled = test_bits(value, 1 << 6);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_radio_altitude_input_select (int value)
|
|
{
|
|
if (value >= 0 && value <= 2)
|
|
mk->io_handler.conf.use_gear_altitude = true;
|
|
else
|
|
mk->io_handler.conf.use_gear_altitude = false;
|
|
return (value >= 0 && value <= 4) || (value >= 251 && value <= 255);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_navigation_input_select (int value)
|
|
{
|
|
if (value >= 0 && value <= 2)
|
|
mk->io_handler.conf.localizer_enabled = false;
|
|
else if ((value >= 3 && value <= 5) || (value >= 250 && value <= 255))
|
|
mk->io_handler.conf.localizer_enabled = true;
|
|
else
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_attitude_input_select (int value)
|
|
{
|
|
if (value == 2)
|
|
mk->io_handler.conf.use_attitude_indicator=true;
|
|
else
|
|
mk->io_handler.conf.use_attitude_indicator=false;
|
|
return (value >= 0 && value <= 6) || value == 253 || value == 255;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_heading_input_select (int value)
|
|
{
|
|
// unimplemented
|
|
return (value >= 0 && value <= 3) || value == 254 || value == 255;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_windshear_input_select (int value)
|
|
{
|
|
// unimplemented
|
|
return value == 0 || (value >= 253 && value <= 255);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_input_output_discrete_type_select (int value)
|
|
{
|
|
static const struct
|
|
{
|
|
int type;
|
|
IOHandler::LampConfiguration lamp_conf;
|
|
bool gpws_inhibit_enabled;
|
|
bool momentary_flap_override_enabled;
|
|
bool alternate_steep_approach;
|
|
} io_types[] = {
|
|
{ 0, { false, false }, false, true, true },
|
|
{ 1, { true, false }, false, true, true },
|
|
{ 2, { false, false }, true, true, true },
|
|
{ 3, { true, false }, true, true, true },
|
|
{ 4, { false, true }, true, true, true },
|
|
{ 5, { true, true }, true, true, true },
|
|
{ 6, { false, false }, true, true, false },
|
|
{ 7, { true, false }, true, true, false },
|
|
{ 254, { false, false }, true, false, true },
|
|
{ 255, { false, false }, true, false, true }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(io_types); i++)
|
|
if (io_types[i].type == value)
|
|
{
|
|
mk->io_handler.conf.lamp = &io_types[i].lamp_conf;
|
|
mk->io_handler.conf.gpws_inhibit_enabled = io_types[i].gpws_inhibit_enabled;
|
|
mk->io_handler.conf.momentary_flap_override_enabled = io_types[i].momentary_flap_override_enabled;
|
|
mk->io_handler.conf.alternate_steep_approach = io_types[i].alternate_steep_approach;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_audio_output_level (int value)
|
|
{
|
|
static const struct
|
|
{
|
|
int id;
|
|
int relative_dB;
|
|
} values[] = {
|
|
{ 0, 0 },
|
|
{ 1, -6 },
|
|
{ 2, -12 },
|
|
{ 3, -18 },
|
|
{ 4, -24 }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(values); i++)
|
|
if (values[i].id == value)
|
|
{
|
|
mk->voice_player.set_volume(mk->voice_player.conf.volume = modify_amplitude(1.0, values[i].relative_dB));
|
|
return true;
|
|
}
|
|
|
|
// The self test needs the voice player even when the configuration
|
|
// is invalid, so set a default value.
|
|
mk->voice_player.set_volume(mk->voice_player.conf.volume = 1.0);
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::ConfigurationModule::read_undefined_input_select (int value)
|
|
{
|
|
// unimplemented
|
|
return value == 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::ConfigurationModule::boot ()
|
|
{
|
|
bool (MK_VIII::ConfigurationModule::*readers[N_CATEGORIES]) (int) = {
|
|
&MK_VIII::ConfigurationModule::read_aircraft_mode_type_select,
|
|
&MK_VIII::ConfigurationModule::read_air_data_input_select,
|
|
&MK_VIII::ConfigurationModule::read_position_input_select,
|
|
&MK_VIII::ConfigurationModule::read_altitude_callouts,
|
|
&MK_VIII::ConfigurationModule::read_audio_menu_select,
|
|
&MK_VIII::ConfigurationModule::read_terrain_display_select,
|
|
&MK_VIII::ConfigurationModule::read_options_select_group_1,
|
|
&MK_VIII::ConfigurationModule::read_radio_altitude_input_select,
|
|
&MK_VIII::ConfigurationModule::read_navigation_input_select,
|
|
&MK_VIII::ConfigurationModule::read_attitude_input_select,
|
|
&MK_VIII::ConfigurationModule::read_heading_input_select,
|
|
&MK_VIII::ConfigurationModule::read_windshear_input_select,
|
|
&MK_VIII::ConfigurationModule::read_input_output_discrete_type_select,
|
|
&MK_VIII::ConfigurationModule::read_audio_output_level,
|
|
&MK_VIII::ConfigurationModule::read_undefined_input_select,
|
|
&MK_VIII::ConfigurationModule::read_undefined_input_select,
|
|
&MK_VIII::ConfigurationModule::read_undefined_input_select
|
|
};
|
|
|
|
memcpy(effective_categories, categories, sizeof(categories));
|
|
state = STATE_OK;
|
|
|
|
for (int i = 0; i < N_CATEGORIES; i++)
|
|
if (! (this->*readers[i])(effective_categories[i]))
|
|
{
|
|
SG_LOG(SG_INSTR, SG_ALERT, "MK VIII EGPWS configuration category " << i + 1 << ": invalid value " << effective_categories[i]);
|
|
|
|
if (state == STATE_OK)
|
|
state = STATE_INVALID_DATABASE;
|
|
|
|
mk_doutput(gpws_inop) = true;
|
|
mk_doutput(tad_inop) = true;
|
|
|
|
return;
|
|
}
|
|
|
|
// handle conflicts
|
|
|
|
if (mk->mode6_handler.conf.above_field_voice && ! mk->tcf_handler.conf.enabled)
|
|
{
|
|
// [INSTALL] 3.6
|
|
SG_LOG(SG_INSTR, SG_ALERT, "MK VIII EGPWS configuration module: when category 4 is set to 100 or 101, category 6 must not be set to 2");
|
|
state = STATE_INVALID_DATABASE;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::ConfigurationModule::bind (SGPropertyNode *node)
|
|
{
|
|
for (int i = 0; i < N_CATEGORIES; i++)
|
|
{
|
|
std::ostringstream name;
|
|
name << "configuration-module/category-" << i + 1;
|
|
mk->properties_handler.tie(node, name.str().c_str(), SGRawValuePointer<int>(&categories[i]));
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// FaultHandler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// [INSTALL] only specifies that the faults cause a GPWS INOP
|
|
// indication. We arbitrarily set a TAD INOP when it makes sense.
|
|
const unsigned int MK_VIII::FaultHandler::fault_inops[] = {
|
|
INOP_GPWS | INOP_TAD, // [INSTALL] 3.15.1.3
|
|
INOP_GPWS, // [INSTALL] appendix E 6.6.2
|
|
INOP_GPWS, // [INSTALL] appendix E 6.6.4
|
|
INOP_GPWS, // [INSTALL] appendix E 6.6.6
|
|
INOP_GPWS | INOP_TAD, // [INSTALL] appendix E 6.6.7
|
|
INOP_GPWS, // [INSTALL] appendix E 6.6.13
|
|
INOP_GPWS, // [INSTALL] appendix E 6.6.25
|
|
INOP_GPWS, // [INSTALL] appendix E 6.6.27
|
|
INOP_TAD, // unspecified
|
|
INOP_GPWS, // unspecified
|
|
INOP_GPWS, // unspecified
|
|
// [INSTALL] 2.3.10.1 specifies that GPWS INOP is "activated during
|
|
// any detected partial or total failure of the GPWS modes 1-5", so
|
|
// do not set any INOP for mode 6 and bank angle.
|
|
0, // unspecified
|
|
0, // unspecified
|
|
INOP_TAD // unspecified
|
|
};
|
|
|
|
bool
|
|
MK_VIII::FaultHandler::has_faults () const
|
|
{
|
|
for (int i = 0; i < N_FAULTS; i++)
|
|
if (faults[i])
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::FaultHandler::has_faults (unsigned int inop)
|
|
{
|
|
for (int i = 0; i < N_FAULTS; i++)
|
|
if (faults[i] && test_bits(fault_inops[i], inop))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::FaultHandler::boot ()
|
|
{
|
|
memset(faults, 0, sizeof(faults));
|
|
}
|
|
|
|
void
|
|
MK_VIII::FaultHandler::set_fault (Fault fault)
|
|
{
|
|
if (! faults[fault])
|
|
{
|
|
faults[fault] = true;
|
|
|
|
mk->self_test_handler.set_inop();
|
|
|
|
if (test_bits(fault_inops[fault], INOP_GPWS))
|
|
{
|
|
mk_unset_alerts(~mk_alert(TCF_TOO_LOW_TERRAIN));
|
|
mk_doutput(gpws_inop) = true;
|
|
}
|
|
if (test_bits(fault_inops[fault], INOP_TAD))
|
|
{
|
|
mk_unset_alerts(mk_alert(TCF_TOO_LOW_TERRAIN));
|
|
mk_doutput(tad_inop) = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::FaultHandler::unset_fault (Fault fault)
|
|
{
|
|
if (faults[fault])
|
|
{
|
|
faults[fault] = false;
|
|
if (! has_faults(INOP_GPWS))
|
|
mk_doutput(gpws_inop) = false;
|
|
if (! has_faults(INOP_TAD))
|
|
mk_doutput(tad_inop) = false;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// IOHandler //////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
double
|
|
MK_VIII::IOHandler::TerrainClearanceFilter::update (double agl)
|
|
{
|
|
// [PILOT] page 20 specifies that the terrain clearance is equal to
|
|
// 75% of the radio altitude, averaged over the previous 15 seconds.
|
|
|
|
// no updates when simulation is paused (dt=0.0), and add 5 samples/second only
|
|
if (globals->get_sim_time_sec() - last_update < 0.2)
|
|
return value;
|
|
last_update = globals->get_sim_time_sec();
|
|
|
|
samples_type::iterator iter;
|
|
|
|
// remove samples older than 15 seconds
|
|
for (iter = samples.begin(); iter != samples.end() && globals->get_sim_time_sec() - (*iter).timestamp >= 15; iter = samples.begin())
|
|
samples.erase(iter);
|
|
|
|
// append new sample
|
|
samples.push_back(Sample<double>(agl));
|
|
|
|
// calculate average
|
|
double new_value = 0;
|
|
if (samples.size() > 0)
|
|
{
|
|
// time consuming loop => queue limited to 75 samples
|
|
// (= 15seconds * 5samples/second)
|
|
for (iter = samples.begin(); iter != samples.end(); iter++)
|
|
new_value += (*iter).value;
|
|
new_value /= samples.size();
|
|
}
|
|
new_value *= 0.75;
|
|
|
|
if (new_value > value)
|
|
value = new_value;
|
|
|
|
return value;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::TerrainClearanceFilter::reset ()
|
|
{
|
|
samples.clear();
|
|
value = 0;
|
|
last_update = -1.0;
|
|
}
|
|
|
|
MK_VIII::IOHandler::IOHandler (MK_VIII *device)
|
|
: mk(device), _lamp(LAMP_NONE)
|
|
{
|
|
memset(&input_feeders, 0, sizeof(input_feeders));
|
|
memset(&inputs.discretes, 0, sizeof(inputs.discretes));
|
|
memset(&outputs, 0, sizeof(outputs));
|
|
memset(&power_saved, 0, sizeof(power_saved));
|
|
|
|
mk_dinput_feed(landing_gear) = true;
|
|
mk_dinput_feed(landing_flaps) = true;
|
|
mk_dinput_feed(glideslope_inhibit) = true;
|
|
mk_dinput_feed(decision_height) = true;
|
|
mk_dinput_feed(autopilot_engaged) = true;
|
|
mk_ainput_feed(uncorrected_barometric_altitude) = true;
|
|
mk_ainput_feed(barometric_altitude_rate) = true;
|
|
mk_ainput_feed(radio_altitude) = true;
|
|
mk_ainput_feed(glideslope_deviation) = true;
|
|
mk_ainput_feed(roll_angle) = true;
|
|
mk_ainput_feed(localizer_deviation) = true;
|
|
mk_ainput_feed(computed_airspeed) = true;
|
|
|
|
// will be unset on power on
|
|
mk_doutput(gpws_inop) = true;
|
|
mk_doutput(tad_inop) = true;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::boot ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
mk_doutput(gpws_inop) = false;
|
|
mk_doutput(tad_inop) = false;
|
|
|
|
mk_doutput(glideslope_cancel) = power_saved.glideslope_cancel;
|
|
|
|
altitude_samples.clear();
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::post_boot ()
|
|
{
|
|
if (momentary_steep_approach_enabled())
|
|
{
|
|
last_landing_gear = mk_dinput(landing_gear);
|
|
last_real_flaps_down = real_flaps_down();
|
|
}
|
|
|
|
// read externally-latching input discretes
|
|
update_alternate_discrete_input(&mk_dinput(mode6_low_volume));
|
|
update_alternate_discrete_input(&mk_dinput(audio_inhibit));
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::power_off ()
|
|
{
|
|
power_saved.glideslope_cancel = mk_doutput(glideslope_cancel); // [SPEC] 6.2.5
|
|
|
|
memset(&outputs, 0, sizeof(outputs));
|
|
|
|
audio_inhibit_fault_timer.stop();
|
|
landing_gear_fault_timer.stop();
|
|
flaps_down_fault_timer.stop();
|
|
momentary_flap_override_fault_timer.stop();
|
|
self_test_fault_timer.stop();
|
|
glideslope_cancel_fault_timer.stop();
|
|
steep_approach_fault_timer.stop();
|
|
gpws_inhibit_fault_timer.stop();
|
|
ta_tcf_inhibit_fault_timer.stop();
|
|
|
|
// [SPEC] 6.9.3.5
|
|
mk_doutput(gpws_inop) = true;
|
|
mk_doutput(tad_inop) = true;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::enter_ground ()
|
|
{
|
|
reset_terrain_clearance();
|
|
|
|
if (conf.momentary_flap_override_enabled)
|
|
mk_doutput(flap_override) = false; // [INSTALL] 3.15.1.6
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::enter_takeoff ()
|
|
{
|
|
reset_terrain_clearance();
|
|
|
|
if (momentary_steep_approach_enabled())
|
|
// landing or go-around, disable steep approach as per [SPEC] 6.2.1
|
|
mk_doutput(steep_approach) = false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_inputs ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
// 1. process input feeders
|
|
|
|
if (mk_dinput_feed(landing_gear))
|
|
mk_dinput(landing_gear) = mk_node(gear_down)->getBoolValue();
|
|
if (mk_dinput_feed(landing_flaps))
|
|
mk_dinput(landing_flaps) = mk_node(flaps)->getDoubleValue() < 1;
|
|
if (mk_dinput_feed(glideslope_inhibit))
|
|
mk_dinput(glideslope_inhibit) = mk_node(nav0_gs_distance)->getDoubleValue() < 0;
|
|
if (mk_dinput_feed(autopilot_engaged))
|
|
{
|
|
const char *mode;
|
|
|
|
mode = mk_node(autopilot_heading_lock)->getStringValue();
|
|
mk_dinput(autopilot_engaged) = mode && *mode;
|
|
}
|
|
|
|
if (mk_ainput_feed(uncorrected_barometric_altitude))
|
|
{
|
|
if (mk_node(altimeter_serviceable)->getBoolValue())
|
|
mk_ainput(uncorrected_barometric_altitude).set(mk_node(altimeter_altitude)->getDoubleValue());
|
|
else
|
|
mk_ainput(uncorrected_barometric_altitude).unset();
|
|
}
|
|
if (mk_ainput_feed(barometric_altitude_rate))
|
|
// Do not use the vsi, because it is pressure-based only, and
|
|
// therefore too laggy for GPWS alerting purposes. I guess that
|
|
// a real ADC combines pressure-based and inerta-based altitude
|
|
// rates to provide a non-laggy rate. That non-laggy rate is
|
|
// best emulated by /velocities/vertical-speed-fps * 60.
|
|
mk_ainput(barometric_altitude_rate).set(mk_node(vs)->getDoubleValue() * 60);
|
|
if (mk_ainput_feed(radio_altitude))
|
|
{
|
|
double agl;
|
|
if (conf.use_gear_altitude)
|
|
agl = mk_node(altitude_gear_agl)->getDoubleValue();
|
|
else
|
|
agl = mk_node(altitude_agl)->getDoubleValue();
|
|
// Some flight models may return negative values when on the
|
|
// ground or after a crash; do not allow them.
|
|
mk_ainput(radio_altitude).set(SG_MAX2(0.0, agl));
|
|
}
|
|
if (mk_ainput_feed(glideslope_deviation))
|
|
{
|
|
if (mk_node(nav0_serviceable)->getBoolValue()
|
|
&& mk_node(nav0_gs_serviceable)->getBoolValue()
|
|
&& mk_node(nav0_in_range)->getBoolValue()
|
|
&& mk_node(nav0_has_gs)->getBoolValue())
|
|
// gs-needle-deflection is expressed in degrees, and 1 dot =
|
|
// 0.32 degrees (according to
|
|
// http://www.ntsb.gov/Recs/letters/2000/A00_41_45.pdf)
|
|
mk_ainput(glideslope_deviation).set(mk_node(nav0_gs_needle_deflection)->getDoubleValue() / 0.32 * GLIDESLOPE_DOTS_TO_DDM);
|
|
else
|
|
mk_ainput(glideslope_deviation).unset();
|
|
}
|
|
if (mk_ainput_feed(roll_angle))
|
|
{
|
|
if (conf.use_attitude_indicator)
|
|
{
|
|
// read data from attitude indicator instrument (requires vacuum system to work)
|
|
if (mk_node(ai_serviceable)->getBoolValue() && ! mk_node(ai_caged)->getBoolValue())
|
|
mk_ainput(roll_angle).set(mk_node(ai_roll)->getDoubleValue());
|
|
else
|
|
mk_ainput(roll_angle).unset();
|
|
}
|
|
else
|
|
{
|
|
// use simulator source
|
|
mk_ainput(roll_angle).set(mk_node(orientation_roll)->getDoubleValue());
|
|
}
|
|
}
|
|
if (mk_ainput_feed(localizer_deviation))
|
|
{
|
|
if (mk_node(nav0_serviceable)->getBoolValue()
|
|
&& mk_node(nav0_cdi_serviceable)->getBoolValue()
|
|
&& mk_node(nav0_in_range)->getBoolValue()
|
|
&& mk_node(nav0_nav_loc)->getBoolValue())
|
|
// heading-needle-deflection is expressed in degrees, and 1
|
|
// dot = 2 degrees (0.5 degrees for ILS, but navradio.cxx
|
|
// performs the conversion)
|
|
mk_ainput(localizer_deviation).set(mk_node(nav0_heading_needle_deflection)->getDoubleValue() / 2 * LOCALIZER_DOTS_TO_DDM);
|
|
else
|
|
mk_ainput(localizer_deviation).unset();
|
|
}
|
|
if (mk_ainput_feed(computed_airspeed))
|
|
{
|
|
if (mk_node(asi_serviceable)->getBoolValue())
|
|
mk_ainput(computed_airspeed).set(mk_node(asi_speed)->getDoubleValue());
|
|
else
|
|
mk_ainput(computed_airspeed).unset();
|
|
}
|
|
|
|
// 2. compute data
|
|
|
|
mk_data(decision_height).set(&mk_ainput(decision_height));
|
|
mk_data(radio_altitude).set(&mk_ainput(radio_altitude));
|
|
mk_data(roll_angle).set(&mk_ainput(roll_angle));
|
|
|
|
// update barometric_altitude_rate as per [SPEC] 6.2.1: "During
|
|
// normal conditions, the system will base Mode 1 computations upon
|
|
// barometric rate from the ADC. If this computed data is not valid
|
|
// or available then the system will use internally computed
|
|
// barometric altitude rate."
|
|
|
|
if (! mk_ainput(barometric_altitude_rate).ncd)
|
|
// the altitude rate input is valid, use it
|
|
mk_data(barometric_altitude_rate).set(mk_ainput(barometric_altitude_rate).get());
|
|
else
|
|
{
|
|
bool has = false;
|
|
|
|
// The altitude rate input is invalid. We can compute an
|
|
// altitude rate if all the following conditions are true:
|
|
//
|
|
// - the altitude input is valid
|
|
// - there is an altitude sample with age >= 1 second
|
|
// - that altitude sample is valid
|
|
|
|
if (! mk_ainput(uncorrected_barometric_altitude).ncd)
|
|
{
|
|
if (! altitude_samples.empty() && ! altitude_samples.front().value.ncd)
|
|
{
|
|
double elapsed = globals->get_sim_time_sec() - altitude_samples.front().timestamp;
|
|
if (elapsed >= 1)
|
|
{
|
|
has = true;
|
|
mk_data(barometric_altitude_rate).set((mk_ainput(uncorrected_barometric_altitude).get() - altitude_samples.front().value.get()) / elapsed * 60);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! has)
|
|
mk_data(barometric_altitude_rate).unset();
|
|
}
|
|
|
|
altitude_samples.push_back(Sample< Parameter<double> >(mk_ainput(uncorrected_barometric_altitude)));
|
|
|
|
// Erase everything from the beginning of the list up to the sample
|
|
// preceding the most recent sample whose age is >= 1 second.
|
|
|
|
altitude_samples_type::iterator erase_last = altitude_samples.begin();
|
|
altitude_samples_type::iterator iter;
|
|
|
|
for (iter = altitude_samples.begin(); iter != altitude_samples.end(); iter++)
|
|
if (globals->get_sim_time_sec() - (*iter).timestamp >= 1)
|
|
erase_last = iter;
|
|
else
|
|
break;
|
|
|
|
if (erase_last != altitude_samples.begin())
|
|
altitude_samples.erase(altitude_samples.begin(), erase_last);
|
|
|
|
// update GPS data
|
|
|
|
if (conf.use_internal_gps)
|
|
{
|
|
mk_data(gps_altitude).set(mk_node(altitude)->getDoubleValue());
|
|
mk_data(gps_latitude).set(mk_node(latitude)->getDoubleValue());
|
|
mk_data(gps_longitude).set(mk_node(longitude)->getDoubleValue());
|
|
mk_data(gps_vertical_figure_of_merit).set(0.0);
|
|
}
|
|
else
|
|
{
|
|
mk_data(gps_altitude).set(&mk_ainput(gps_altitude));
|
|
mk_data(gps_latitude).set(&mk_ainput(gps_latitude));
|
|
mk_data(gps_longitude).set(&mk_ainput(gps_longitude));
|
|
mk_data(gps_vertical_figure_of_merit).set(&mk_ainput(gps_vertical_figure_of_merit));
|
|
}
|
|
|
|
// update glideslope and localizer
|
|
|
|
mk_data(glideslope_deviation_dots).set(&mk_ainput(glideslope_deviation), GLIDESLOPE_DDM_TO_DOTS);
|
|
mk_data(localizer_deviation_dots).set(&mk_ainput(localizer_deviation), LOCALIZER_DDM_TO_DOTS);
|
|
|
|
// Update geometric altitude; [SPEC] 6.7.8 provides an overview of a
|
|
// complex algorithm which combines several input sources to
|
|
// calculate the geometric altitude. Since the exact algorithm is
|
|
// not given, we fallback to a much simpler method.
|
|
|
|
if (! mk_data(gps_altitude).ncd)
|
|
mk_data(geometric_altitude).set(mk_data(gps_altitude).get());
|
|
else if (! mk_ainput(uncorrected_barometric_altitude).ncd)
|
|
mk_data(geometric_altitude).set(mk_ainput(uncorrected_barometric_altitude).get());
|
|
else
|
|
mk_data(geometric_altitude).unset();
|
|
|
|
// update terrain clearance
|
|
|
|
update_terrain_clearance();
|
|
|
|
// 3. perform sanity checks
|
|
|
|
if (! mk_data(radio_altitude).ncd && mk_data(radio_altitude).get() < 0)
|
|
mk_data(radio_altitude).unset();
|
|
|
|
if (! mk_data(decision_height).ncd && mk_data(decision_height).get() < 0)
|
|
mk_data(decision_height).unset();
|
|
|
|
if (! mk_data(gps_latitude).ncd
|
|
&& (mk_data(gps_latitude).get() < -90
|
|
|| mk_data(gps_latitude).get() > 90))
|
|
mk_data(gps_latitude).unset();
|
|
|
|
if (! mk_data(gps_longitude).ncd
|
|
&& (mk_data(gps_longitude).get() < -180
|
|
|| mk_data(gps_longitude).get() > 180))
|
|
mk_data(gps_longitude).unset();
|
|
|
|
if (! mk_data(roll_angle).ncd
|
|
&& ((mk_data(roll_angle).get() < -180)
|
|
|| (mk_data(roll_angle).get() > 180)))
|
|
mk_data(roll_angle).unset();
|
|
|
|
// 4. process input feeders requiring data computed above
|
|
|
|
if (mk_dinput_feed(decision_height))
|
|
mk_dinput(decision_height) = ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(decision_height).ncd
|
|
&& mk_data(radio_altitude).get() <= mk_data(decision_height).get();
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_terrain_clearance ()
|
|
{
|
|
if (! mk_data(radio_altitude).ncd)
|
|
mk_data(terrain_clearance).set(terrain_clearance_filter.update(mk_data(radio_altitude).get()));
|
|
else
|
|
mk_data(terrain_clearance).unset();
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::reset_terrain_clearance ()
|
|
{
|
|
terrain_clearance_filter.reset();
|
|
update_terrain_clearance();
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::handle_input_fault (bool test, FaultHandler::Fault fault)
|
|
{
|
|
if (test)
|
|
{
|
|
if (! mk->fault_handler.faults[fault])
|
|
mk->fault_handler.set_fault(fault);
|
|
}
|
|
else
|
|
mk->fault_handler.unset_fault(fault);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::handle_input_fault (bool test,
|
|
Timer *timer,
|
|
double max_duration,
|
|
FaultHandler::Fault fault)
|
|
{
|
|
if (test)
|
|
{
|
|
if (! mk->fault_handler.faults[fault])
|
|
{
|
|
if (timer->start_or_elapsed() >= max_duration)
|
|
mk->fault_handler.set_fault(fault);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mk->fault_handler.unset_fault(fault);
|
|
timer->stop();
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_input_faults ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
// [INSTALL] 3.15.1.3
|
|
handle_input_fault(mk_dinput(audio_inhibit),
|
|
&audio_inhibit_fault_timer,
|
|
60,
|
|
FaultHandler::FAULT_ALL_MODES_INHIBIT);
|
|
|
|
// [INSTALL] appendix E 6.6.2
|
|
handle_input_fault(mk_dinput(landing_gear)
|
|
&& ! mk_ainput(computed_airspeed).ncd
|
|
&& mk_ainput(computed_airspeed).get() > conf.faults->max_gear_down_airspeed,
|
|
&landing_gear_fault_timer,
|
|
60,
|
|
FaultHandler::FAULT_GEAR_SWITCH);
|
|
|
|
// [INSTALL] appendix E 6.6.4
|
|
handle_input_fault(flaps_down()
|
|
&& ! mk_ainput(computed_airspeed).ncd
|
|
&& mk_ainput(computed_airspeed).get() > conf.faults->max_flaps_down_airspeed,
|
|
&flaps_down_fault_timer,
|
|
60,
|
|
FaultHandler::FAULT_FLAPS_SWITCH);
|
|
|
|
// [INSTALL] appendix E 6.6.6
|
|
if (conf.momentary_flap_override_enabled)
|
|
handle_input_fault(mk_dinput(momentary_flap_override),
|
|
&momentary_flap_override_fault_timer,
|
|
15,
|
|
FaultHandler::FAULT_MOMENTARY_FLAP_OVERRIDE_INVALID);
|
|
|
|
// [INSTALL] appendix E 6.6.7
|
|
handle_input_fault(mk_dinput(self_test),
|
|
&self_test_fault_timer,
|
|
60,
|
|
FaultHandler::FAULT_SELF_TEST_INVALID);
|
|
|
|
// [INSTALL] appendix E 6.6.13
|
|
handle_input_fault(mk_dinput(glideslope_cancel),
|
|
&glideslope_cancel_fault_timer,
|
|
15,
|
|
FaultHandler::FAULT_GLIDESLOPE_CANCEL_INVALID);
|
|
|
|
// [INSTALL] appendix E 6.6.25
|
|
if (momentary_steep_approach_enabled())
|
|
handle_input_fault(mk_dinput(steep_approach),
|
|
&steep_approach_fault_timer,
|
|
15,
|
|
FaultHandler::FAULT_STEEP_APPROACH_INVALID);
|
|
|
|
// [INSTALL] appendix E 6.6.27
|
|
if (conf.gpws_inhibit_enabled)
|
|
handle_input_fault(mk_dinput(gpws_inhibit),
|
|
&gpws_inhibit_fault_timer,
|
|
5,
|
|
FaultHandler::FAULT_GPWS_INHIBIT);
|
|
|
|
// [INSTALL] does not specify a fault for this one, but it makes
|
|
// sense to have it behave like GPWS inhibit
|
|
handle_input_fault(mk_dinput(ta_tcf_inhibit),
|
|
&ta_tcf_inhibit_fault_timer,
|
|
5,
|
|
FaultHandler::FAULT_TA_TCF_INHIBIT);
|
|
|
|
// [PILOT] page 48: "In the event that required data for a
|
|
// particular function is not available, then that function is
|
|
// automatically inhibited and annunciated"
|
|
|
|
handle_input_fault(mk_data(radio_altitude).ncd
|
|
|| mk_ainput(uncorrected_barometric_altitude).ncd
|
|
|| mk_data(barometric_altitude_rate).ncd
|
|
|| mk_ainput(computed_airspeed).ncd
|
|
|| mk_data(terrain_clearance).ncd,
|
|
FaultHandler::FAULT_MODES14_INPUTS_INVALID);
|
|
|
|
if (! mk_dinput(glideslope_inhibit))
|
|
handle_input_fault(mk_data(radio_altitude).ncd,
|
|
FaultHandler::FAULT_MODE5_INPUTS_INVALID);
|
|
|
|
if (mk->mode6_handler.altitude_callouts_enabled())
|
|
handle_input_fault(mk->mode6_handler.conf.above_field_voice
|
|
? (mk_data(gps_latitude).ncd
|
|
|| mk_data(gps_longitude).ncd
|
|
|| mk_data(geometric_altitude).ncd)
|
|
: mk_data(radio_altitude).ncd,
|
|
FaultHandler::FAULT_MODE6_INPUTS_INVALID);
|
|
|
|
if (mk->mode6_handler.conf.bank_angle_enabled)
|
|
handle_input_fault(mk_data(roll_angle).ncd,
|
|
FaultHandler::FAULT_BANK_ANGLE_INPUTS_INVALID);
|
|
|
|
if (mk->tcf_handler.conf.enabled)
|
|
handle_input_fault(mk_data(radio_altitude).ncd
|
|
|| mk_data(geometric_altitude).ncd
|
|
|| mk_data(gps_latitude).ncd
|
|
|| mk_data(gps_longitude).ncd
|
|
|| mk_data(gps_vertical_figure_of_merit).ncd,
|
|
FaultHandler::FAULT_TCF_INPUTS_INVALID);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_alternate_discrete_input (bool *ptr)
|
|
{
|
|
assert(mk->system_handler.state == SystemHandler::STATE_ON);
|
|
|
|
if (ptr == &mk_dinput(mode6_low_volume))
|
|
{
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_OK
|
|
&& mk->self_test_handler.state == SelfTestHandler::STATE_NONE)
|
|
mk->mode6_handler.set_volume(*ptr ? modify_amplitude(1.0, -6) : 1.0);
|
|
}
|
|
else if (ptr == &mk_dinput(audio_inhibit))
|
|
{
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_OK
|
|
&& mk->self_test_handler.state == SelfTestHandler::STATE_NONE)
|
|
mk->voice_player.set_volume(*ptr ? 0.0 : mk->voice_player.conf.volume);
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_internal_latches ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
// [SPEC] 6.2.1
|
|
if (conf.momentary_flap_override_enabled
|
|
&& mk_doutput(flap_override)
|
|
&& ! mk->state_handler.takeoff
|
|
&& (mk_data(radio_altitude).ncd || mk_data(radio_altitude).get() <= 50))
|
|
mk_doutput(flap_override) = false;
|
|
|
|
// [SPEC] 6.2.1
|
|
if (momentary_steep_approach_enabled())
|
|
{
|
|
if (mk_doutput(steep_approach)
|
|
&& ! mk->state_handler.takeoff
|
|
&& ((last_landing_gear && ! mk_dinput(landing_gear))
|
|
|| (last_real_flaps_down && ! real_flaps_down())))
|
|
// gear or flaps raised during approach: it's a go-around
|
|
mk_doutput(steep_approach) = false;
|
|
|
|
last_landing_gear = mk_dinput(landing_gear);
|
|
last_real_flaps_down = real_flaps_down();
|
|
}
|
|
|
|
// [SPEC] 6.2.5
|
|
if (mk_doutput(glideslope_cancel)
|
|
&& (mk_data(glideslope_deviation_dots).ncd
|
|
|| mk_data(radio_altitude).ncd
|
|
|| mk_data(radio_altitude).get() > 2000
|
|
|| mk_data(radio_altitude).get() < 30))
|
|
mk_doutput(glideslope_cancel) = false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_egpws_alert_discrete_1 ()
|
|
{
|
|
if (mk->voice_player.voice)
|
|
{
|
|
const struct
|
|
{
|
|
int bit;
|
|
VoicePlayer::Voice *voice;
|
|
} voices[] = {
|
|
{ 11, mk_voice(sink_rate_pause_sink_rate) },
|
|
{ 12, mk_voice(pull_up) },
|
|
{ 13, mk_voice(terrain) },
|
|
{ 13, mk_voice(terrain_pause_terrain) },
|
|
{ 14, mk_voice(dont_sink_pause_dont_sink) },
|
|
{ 15, mk_voice(too_low_gear) },
|
|
{ 16, mk_voice(too_low_flaps) },
|
|
{ 17, mk_voice(too_low_terrain) },
|
|
{ 18, mk_voice(soft_glideslope) },
|
|
{ 18, mk_voice(hard_glideslope) },
|
|
{ 19, mk_voice(minimums_minimums) }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(voices); i++)
|
|
if (voices[i].voice == mk->voice_player.voice)
|
|
{
|
|
mk_aoutput(egpws_alert_discrete_1) = 1 << voices[i].bit;
|
|
return;
|
|
}
|
|
}
|
|
|
|
mk_aoutput(egpws_alert_discrete_1) = 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_egpwc_logic_discretes ()
|
|
{
|
|
mk_aoutput(egpwc_logic_discretes) = 0;
|
|
|
|
if (mk->state_handler.takeoff)
|
|
mk_aoutput(egpwc_logic_discretes) |= 1 << 18;
|
|
|
|
static const struct
|
|
{
|
|
int bit;
|
|
unsigned int alerts;
|
|
} logic[] = {
|
|
{ 13, mk_alert(TCF_TOO_LOW_TERRAIN) },
|
|
{ 19, mk_alert(MODE1_SINK_RATE) },
|
|
{ 20, mk_alert(MODE1_PULL_UP) },
|
|
{ 21, mk_alert(MODE2A_PREFACE) | mk_alert(MODE2B_PREFACE) | mk_alert(MODE2B_LANDING_MODE) | mk_alert(MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING) },
|
|
{ 22, mk_alert(MODE2A) | mk_alert(MODE2B) },
|
|
{ 23, mk_alert(MODE3) },
|
|
{ 24, mk_alert(MODE4_TOO_LOW_FLAPS) | mk_alert(MODE4_TOO_LOW_GEAR) | mk_alert(MODE4AB_TOO_LOW_TERRAIN) | mk_alert(MODE4C_TOO_LOW_TERRAIN) },
|
|
{ 25, mk_alert(MODE5_SOFT) | mk_alert(MODE5_HARD) }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(logic); i++)
|
|
if (mk_test_alerts(logic[i].alerts))
|
|
mk_aoutput(egpwc_logic_discretes) |= 1 << logic[i].bit;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_mode6_callouts_discrete_1 ()
|
|
{
|
|
if (mk->voice_player.voice)
|
|
{
|
|
const struct
|
|
{
|
|
int bit;
|
|
VoicePlayer::Voice *voice;
|
|
} voices[] = {
|
|
{ 11, mk_voice(minimums_minimums) },
|
|
{ 16, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_10) },
|
|
{ 17, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_20) },
|
|
{ 18, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_30) },
|
|
{ 19, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_40) },
|
|
{ 20, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_50) },
|
|
{ 23, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_100) },
|
|
{ 24, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_200) },
|
|
{ 25, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_300) }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(voices); i++)
|
|
if (voices[i].voice == mk->voice_player.voice)
|
|
{
|
|
mk_aoutput(mode6_callouts_discrete_1) = 1 << voices[i].bit;
|
|
return;
|
|
}
|
|
}
|
|
|
|
mk_aoutput(mode6_callouts_discrete_1) = 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_mode6_callouts_discrete_2 ()
|
|
{
|
|
if (mk->voice_player.voice)
|
|
{
|
|
const struct
|
|
{
|
|
int bit;
|
|
VoicePlayer::Voice *voice;
|
|
} voices[] = {
|
|
{ 11, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_400) },
|
|
{ 12, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_500) },
|
|
{ 13, mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_1000) },
|
|
{ 18, mk_voice(bank_angle_pause_bank_angle) },
|
|
{ 18, mk_voice(bank_angle_pause_bank_angle_3) },
|
|
{ 23, mk_voice(five_hundred_above) }
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(voices); i++)
|
|
if (voices[i].voice == mk->voice_player.voice)
|
|
{
|
|
mk_aoutput(mode6_callouts_discrete_2) = 1 << voices[i].bit;
|
|
return;
|
|
}
|
|
}
|
|
|
|
mk_aoutput(mode6_callouts_discrete_2) = 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_egpws_alert_discrete_2 ()
|
|
{
|
|
mk_aoutput(egpws_alert_discrete_2) = 1 << 27; // Terrain NA
|
|
|
|
if (mk_doutput(glideslope_cancel))
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 11;
|
|
if (mk_doutput(gpws_alert))
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 12;
|
|
if (mk_doutput(gpws_warning))
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 13;
|
|
if (mk_doutput(gpws_inop))
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 14;
|
|
if (mk_doutput(audio_on))
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 16;
|
|
if (mk_doutput(tad_inop))
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 24;
|
|
if (mk->fault_handler.has_faults())
|
|
mk_aoutput(egpws_alert_discrete_2) |= 1 << 25;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_egpwc_alert_discrete_3 ()
|
|
{
|
|
mk_aoutput(egpwc_alert_discrete_3) = 1 << 17 | 1 << 18;
|
|
|
|
if (mk->fault_handler.faults[FaultHandler::FAULT_MODES14_INPUTS_INVALID])
|
|
mk_aoutput(egpwc_alert_discrete_3) |= 1 << 11;
|
|
if (mk->fault_handler.faults[FaultHandler::FAULT_MODE5_INPUTS_INVALID])
|
|
mk_aoutput(egpwc_alert_discrete_3) |= 1 << 12;
|
|
if (mk->fault_handler.faults[FaultHandler::FAULT_MODE6_INPUTS_INVALID])
|
|
mk_aoutput(egpwc_alert_discrete_3) |= 1 << 13;
|
|
if (mk->fault_handler.faults[FaultHandler::FAULT_BANK_ANGLE_INPUTS_INVALID])
|
|
mk_aoutput(egpwc_alert_discrete_3) |= 1 << 14;
|
|
if (mk_doutput(tad_inop))
|
|
mk_aoutput(egpwc_alert_discrete_3) |= 1 << 16;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_outputs ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
mk_doutput(audio_on) = ! mk_dinput(audio_inhibit)
|
|
&& mk->voice_player.voice
|
|
&& ! mk->voice_player.voice->element->silence;
|
|
|
|
update_egpws_alert_discrete_1();
|
|
update_egpwc_logic_discretes();
|
|
update_mode6_callouts_discrete_1();
|
|
update_mode6_callouts_discrete_2();
|
|
update_egpws_alert_discrete_2();
|
|
update_egpwc_alert_discrete_3();
|
|
}
|
|
|
|
bool *
|
|
MK_VIII::IOHandler::get_lamp_output (Lamp lamp)
|
|
{
|
|
switch (lamp)
|
|
{
|
|
case LAMP_GLIDESLOPE:
|
|
return &mk_doutput(gpws_alert);
|
|
|
|
case LAMP_CAUTION:
|
|
return conf.lamp->format2 ? &mk_doutput(gpws_alert) : &mk_doutput(gpws_warning);
|
|
|
|
case LAMP_WARNING:
|
|
return &mk_doutput(gpws_warning);
|
|
|
|
default:
|
|
assert_not_reached();
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::update_lamps ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
if (_lamp != LAMP_NONE && conf.lamp->flashing)
|
|
{
|
|
// [SPEC] 6.9.3: 70 cycles per minute
|
|
if (lamp_timer.elapsed() >= 60.0 / 70.0 / 2.0)
|
|
{
|
|
bool *output = get_lamp_output(_lamp);
|
|
*output = ! *output;
|
|
lamp_timer.start();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::set_lamp (Lamp lamp)
|
|
{
|
|
if (lamp == _lamp)
|
|
return;
|
|
|
|
_lamp = lamp;
|
|
|
|
mk_doutput(gpws_warning) = false;
|
|
mk_doutput(gpws_alert) = false;
|
|
|
|
if (lamp != LAMP_NONE)
|
|
{
|
|
*get_lamp_output(lamp) = true;
|
|
lamp_timer.start();
|
|
}
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::gpws_inhibit () const
|
|
{
|
|
return conf.gpws_inhibit_enabled && mk_dinput(gpws_inhibit);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::real_flaps_down () const
|
|
{
|
|
return conf.flap_reversal ? mk_dinput(landing_flaps) : ! mk_dinput(landing_flaps);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::flaps_down () const
|
|
{
|
|
return flap_override() ? true : real_flaps_down();
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::flap_override () const
|
|
{
|
|
return conf.momentary_flap_override_enabled ? mk_doutput(flap_override) : false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::steep_approach () const
|
|
{
|
|
if (conf.steep_approach_enabled)
|
|
// If alternate action was configured in category 13, we return
|
|
// the value of the input discrete (which should be connected to a
|
|
// latching steep approach cockpit switch). Otherwise, we return
|
|
// the value of the output discrete.
|
|
return conf.alternate_steep_approach ? mk_dinput(steep_approach) : mk_doutput(steep_approach);
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::momentary_steep_approach_enabled () const
|
|
{
|
|
return conf.steep_approach_enabled && ! conf.alternate_steep_approach;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::tie_input (SGPropertyNode *node,
|
|
const char *name,
|
|
bool *input,
|
|
bool *feed)
|
|
{
|
|
mk->properties_handler.tie(node, (string("inputs/discretes/") + name).c_str(), RawValueMethodsData<MK_VIII::IOHandler, bool, bool *>(*this, input, &MK_VIII::IOHandler::get_discrete_input, &MK_VIII::IOHandler::set_discrete_input));
|
|
if (feed)
|
|
mk->properties_handler.tie(node, (string("input-feeders/discretes/") + name).c_str(), SGRawValuePointer<bool>(feed));
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::tie_input (SGPropertyNode *node,
|
|
const char *name,
|
|
Parameter<double> *input,
|
|
bool *feed)
|
|
{
|
|
mk->properties_handler.tie(node, (string("inputs/arinc429/") + name).c_str(), SGRawValuePointer<double>(input->get_pointer()));
|
|
mk->properties_handler.tie(node, (string("inputs/arinc429/") + name + "-ncd").c_str(), SGRawValuePointer<bool>(&input->ncd));
|
|
if (feed)
|
|
mk->properties_handler.tie(node, (string("input-feeders/arinc429/") + name).c_str(), SGRawValuePointer<bool>(feed));
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::tie_output (SGPropertyNode *node,
|
|
const char *name,
|
|
bool *output)
|
|
{
|
|
SGPropertyNode *child = node->getNode((string("outputs/discretes/") + name).c_str(), true);
|
|
|
|
mk->properties_handler.tie(child, SGRawValuePointer<bool>(output));
|
|
child->setAttribute(SGPropertyNode::WRITE, false);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::tie_output (SGPropertyNode *node,
|
|
const char *name,
|
|
int *output)
|
|
{
|
|
SGPropertyNode *child = node->getNode((string("outputs/arinc429/") + name).c_str(), true);
|
|
|
|
mk->properties_handler.tie(child, SGRawValuePointer<int>(output));
|
|
child->setAttribute(SGPropertyNode::WRITE, false);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::bind (SGPropertyNode *node)
|
|
{
|
|
mk->properties_handler.tie(node, "inputs/rs-232/present-status", SGRawValueMethods<MK_VIII::IOHandler, bool>(*this, &MK_VIII::IOHandler::get_present_status, &MK_VIII::IOHandler::set_present_status));
|
|
|
|
tie_input(node, "landing-gear", &mk_dinput(landing_gear), &mk_dinput_feed(landing_gear));
|
|
tie_input(node, "landing-flaps", &mk_dinput(landing_flaps), &mk_dinput_feed(landing_flaps));
|
|
tie_input(node, "momentary-flap-override", &mk_dinput(momentary_flap_override));
|
|
tie_input(node, "self-test", &mk_dinput(self_test));
|
|
tie_input(node, "glideslope-inhibit", &mk_dinput(glideslope_inhibit), &mk_dinput_feed(glideslope_inhibit));
|
|
tie_input(node, "glideslope-cancel", &mk_dinput(glideslope_cancel));
|
|
tie_input(node, "decision-height", &mk_dinput(decision_height), &mk_dinput_feed(decision_height));
|
|
tie_input(node, "mode6-low-volume", &mk_dinput(mode6_low_volume));
|
|
tie_input(node, "audio-inhibit", &mk_dinput(audio_inhibit));
|
|
tie_input(node, "ta-tcf-inhibit", &mk_dinput(ta_tcf_inhibit));
|
|
tie_input(node, "autopilot-engaged", &mk_dinput(autopilot_engaged), &mk_dinput_feed(autopilot_engaged));
|
|
tie_input(node, "steep-approach", &mk_dinput(steep_approach));
|
|
tie_input(node, "gpws-inhibit", &mk_dinput(gpws_inhibit));
|
|
|
|
tie_input(node, "uncorrected-barometric-altitude", &mk_ainput(uncorrected_barometric_altitude), &mk_ainput_feed(uncorrected_barometric_altitude));
|
|
tie_input(node, "barometric-altitude-rate", &mk_ainput(barometric_altitude_rate), &mk_ainput_feed(barometric_altitude_rate));
|
|
tie_input(node, "gps-altitude", &mk_ainput(gps_altitude));
|
|
tie_input(node, "gps-latitude", &mk_ainput(gps_latitude));
|
|
tie_input(node, "gps-longitude", &mk_ainput(gps_longitude));
|
|
tie_input(node, "gps-vertical-figure-of-merit", &mk_ainput(gps_vertical_figure_of_merit));
|
|
tie_input(node, "radio-altitude", &mk_ainput(radio_altitude), &mk_ainput_feed(radio_altitude));
|
|
tie_input(node, "glideslope-deviation", &mk_ainput(glideslope_deviation), &mk_ainput_feed(glideslope_deviation));
|
|
tie_input(node, "roll-angle", &mk_ainput(roll_angle), &mk_ainput_feed(roll_angle));
|
|
tie_input(node, "localizer-deviation", &mk_ainput(localizer_deviation), &mk_ainput_feed(localizer_deviation));
|
|
tie_input(node, "computed-airspeed", &mk_ainput(computed_airspeed), &mk_ainput_feed(computed_airspeed));
|
|
tie_input(node, "decision-height", &mk_ainput(decision_height), &mk_ainput_feed(decision_height));
|
|
|
|
tie_output(node, "gpws-warning", &mk_doutput(gpws_warning));
|
|
tie_output(node, "gpws-alert", &mk_doutput(gpws_alert));
|
|
tie_output(node, "audio-on", &mk_doutput(audio_on));
|
|
tie_output(node, "gpws-inop", &mk_doutput(gpws_inop));
|
|
tie_output(node, "tad-inop", &mk_doutput(tad_inop));
|
|
tie_output(node, "flap-override", &mk_doutput(flap_override));
|
|
tie_output(node, "glideslope-cancel", &mk_doutput(glideslope_cancel));
|
|
tie_output(node, "steep-approach", &mk_doutput(steep_approach));
|
|
|
|
tie_output(node, "egpws-alert-discrete-1", &mk_aoutput(egpws_alert_discrete_1));
|
|
tie_output(node, "egpwc-logic-discretes", &mk_aoutput(egpwc_logic_discretes));
|
|
tie_output(node, "mode6-callouts-discrete-1", &mk_aoutput(mode6_callouts_discrete_1));
|
|
tie_output(node, "mode6-callouts-discrete-2", &mk_aoutput(mode6_callouts_discrete_2));
|
|
tie_output(node, "egpws-alert-discrete-2", &mk_aoutput(egpws_alert_discrete_2));
|
|
tie_output(node, "egpwc-alert-discrete-3", &mk_aoutput(egpwc_alert_discrete_3));
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::get_discrete_input (bool *ptr) const
|
|
{
|
|
return *ptr;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::set_discrete_input (bool *ptr, bool value)
|
|
{
|
|
if (value == *ptr)
|
|
return;
|
|
|
|
if (mk->system_handler.state == SystemHandler::STATE_ON)
|
|
{
|
|
if (ptr == &mk_dinput(momentary_flap_override))
|
|
{
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_OK
|
|
&& mk->self_test_handler.state == SelfTestHandler::STATE_NONE
|
|
&& conf.momentary_flap_override_enabled
|
|
&& value)
|
|
mk_doutput(flap_override) = ! mk_doutput(flap_override);
|
|
}
|
|
else if (ptr == &mk_dinput(self_test))
|
|
mk->self_test_handler.handle_button_event(value);
|
|
else if (ptr == &mk_dinput(glideslope_cancel))
|
|
{
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_OK
|
|
&& mk->self_test_handler.state == SelfTestHandler::STATE_NONE
|
|
&& value)
|
|
{
|
|
if (! mk_doutput(glideslope_cancel))
|
|
{
|
|
// [SPEC] 6.2.5
|
|
|
|
// Although we are not called from update(), we are
|
|
// sure that the inputs we use here are defined,
|
|
// since state is STATE_ON.
|
|
|
|
if (! mk_data(glideslope_deviation_dots).ncd
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& mk_data(radio_altitude).get() <= 2000
|
|
&& mk_data(radio_altitude).get() >= 30)
|
|
mk_doutput(glideslope_cancel) = true;
|
|
}
|
|
// else do nothing ([PILOT] page 22: "Glideslope Cancel
|
|
// can not be deselected (reset) by again pressing the
|
|
// Glideslope Cancel switch.")
|
|
}
|
|
}
|
|
else if (ptr == &mk_dinput(steep_approach))
|
|
{
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_OK
|
|
&& mk->self_test_handler.state == SelfTestHandler::STATE_NONE
|
|
&& momentary_steep_approach_enabled()
|
|
&& value)
|
|
mk_doutput(steep_approach) = ! mk_doutput(steep_approach);
|
|
}
|
|
}
|
|
|
|
*ptr = value;
|
|
|
|
if (mk->system_handler.state == SystemHandler::STATE_ON)
|
|
update_alternate_discrete_input(ptr);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::present_status_section (const char *name)
|
|
{
|
|
printf("%s\n", name);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::present_status_item (const char *name, const char *value)
|
|
{
|
|
if (value)
|
|
printf("\t%-32s %s\n", name, value);
|
|
else
|
|
printf("\t%s\n", name);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::present_status_subitem (const char *name)
|
|
{
|
|
printf("\t\t%s\n", name);
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::present_status ()
|
|
{
|
|
// [SPEC] 6.10.13
|
|
|
|
if (mk->system_handler.state != SystemHandler::STATE_ON)
|
|
return;
|
|
|
|
present_status_section("EGPWC CONFIGURATION");
|
|
present_status_item("PART NUMBER:", "965-1220-000"); // [SPEC] 1.1
|
|
present_status_item("MOD STATUS:", "N/A");
|
|
present_status_item("SERIAL NUMBER:", "N/A");
|
|
printf("\n");
|
|
present_status_item("APPLICATION S/W VERSION:", FLIGHTGEAR_VERSION);
|
|
present_status_item("TERRAIN DATABASE VERSION:", FLIGHTGEAR_VERSION);
|
|
present_status_item("ENVELOPE MOD DATABASE VERSION:", "N/A");
|
|
present_status_item("BOOT CODE VERSION:", FLIGHTGEAR_VERSION);
|
|
printf("\n");
|
|
|
|
present_status_section("CURRENT FAULTS");
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_OK && ! mk->fault_handler.has_faults())
|
|
present_status_item("NO FAULTS");
|
|
else
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
{
|
|
present_status_item("GPWS COMPUTER FAULTS:");
|
|
switch (mk->configuration_module.state)
|
|
{
|
|
case ConfigurationModule::STATE_INVALID_DATABASE:
|
|
present_status_subitem("APPLICATION DATABASE FAILED");
|
|
break;
|
|
|
|
case ConfigurationModule::STATE_INVALID_AIRCRAFT_TYPE:
|
|
present_status_subitem("CONFIGURATION TYPE INVALID");
|
|
break;
|
|
|
|
default:
|
|
assert_not_reached();
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
present_status_item("GPWS COMPUTER OK");
|
|
present_status_item("GPWS EXTERNAL FAULTS:");
|
|
|
|
static const char *fault_names[] = {
|
|
"ALL MODES INHIBIT",
|
|
"GEAR SWITCH",
|
|
"FLAPS SWITCH",
|
|
"MOMENTARY FLAP OVERRIDE INVALID",
|
|
"SELF TEST INVALID",
|
|
"GLIDESLOPE CANCEL INVALID",
|
|
"STEEP APPROACH INVALID",
|
|
"GPWS INHIBIT",
|
|
"TA & TCF INHIBIT",
|
|
"MODES 1-4 INPUTS INVALID",
|
|
"MODE 5 INPUTS INVALID",
|
|
"MODE 6 INPUTS INVALID",
|
|
"BANK ANGLE INPUTS INVALID",
|
|
"TCF INPUTS INVALID"
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(fault_names); i++)
|
|
if (mk->fault_handler.faults[i])
|
|
present_status_subitem(fault_names[i]);
|
|
}
|
|
}
|
|
printf("\n");
|
|
|
|
present_status_section("CONFIGURATION:");
|
|
|
|
static const char *category_names[] = {
|
|
"AIRCRAFT TYPE",
|
|
"AIR DATA TYPE",
|
|
"POSITION INPUT TYPE",
|
|
"CALLOUTS OPTION TYPE",
|
|
"AUDIO MENU TYPE",
|
|
"TERRAIN DISPLAY TYPE",
|
|
"OPTIONS 1 TYPE",
|
|
"RADIO ALTITUDE TYPE",
|
|
"NAVIGATION INPUT TYPE",
|
|
"ATTITUDE TYPE",
|
|
"MAGNETIC HEADING TYPE",
|
|
"WINDSHEAR INPUT TYPE",
|
|
"IO DISCRETE TYPE",
|
|
"VOLUME SELECT"
|
|
};
|
|
|
|
for (size_t i = 0; i < n_elements(category_names); i++)
|
|
{
|
|
std::ostringstream value;
|
|
value << "= " << mk->configuration_module.effective_categories[i];
|
|
present_status_item(category_names[i], value.str().c_str());
|
|
}
|
|
}
|
|
|
|
bool
|
|
MK_VIII::IOHandler::get_present_status () const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::IOHandler::set_present_status (bool value)
|
|
{
|
|
if (value) present_status();
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// VoicePlayer ////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Speaker::bind (SGPropertyNode *node)
|
|
{
|
|
// uses xmlsound property names
|
|
tie(node, "volume", &volume);
|
|
tie(node, "pitch", &pitch);
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Speaker::update_configuration ()
|
|
{
|
|
map< string, SGSharedPtr<SGSoundSample> >::iterator iter;
|
|
for (iter = player->samples.begin(); iter != player->samples.end(); iter++)
|
|
{
|
|
SGSoundSample *sample = (*iter).second;
|
|
|
|
sample->set_pitch(pitch);
|
|
}
|
|
|
|
if (player->voice)
|
|
player->voice->volume_changed();
|
|
}
|
|
|
|
MK_VIII::VoicePlayer::Voice::~Voice ()
|
|
{
|
|
for (iter = elements.begin(); iter != elements.end(); iter++)
|
|
delete *iter; // we owned the element
|
|
elements.clear();
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Voice::play ()
|
|
{
|
|
iter = elements.begin();
|
|
element = *iter;
|
|
|
|
element->play(get_volume());
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Voice::stop (bool now)
|
|
{
|
|
if (element)
|
|
{
|
|
if (now || element->silence)
|
|
{
|
|
element->stop();
|
|
element = NULL;
|
|
}
|
|
else
|
|
iter = elements.end() - 1; // stop after the current element finishes
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Voice::set_volume (float _volume)
|
|
{
|
|
volume = _volume;
|
|
volume_changed();
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Voice::volume_changed ()
|
|
{
|
|
if (element)
|
|
element->set_volume(get_volume());
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::Voice::update ()
|
|
{
|
|
if (element)
|
|
{
|
|
if (! element->is_playing())
|
|
{
|
|
if (++iter == elements.end())
|
|
element = NULL;
|
|
else
|
|
{
|
|
element = *iter;
|
|
element->play(get_volume());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MK_VIII::VoicePlayer::~VoicePlayer ()
|
|
{
|
|
vector<Voice *>::iterator iter1;
|
|
for (iter1 = _voices.begin(); iter1 != _voices.end(); iter1++)
|
|
delete *iter1;
|
|
_voices.clear();
|
|
samples.clear();
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::init ()
|
|
{
|
|
#define STDPAUSE 0.75 // [SPEC] 6.4.4: "the standard 0.75 second delay"
|
|
|
|
SGSoundMgr *smgr = globals->get_soundmgr();
|
|
_sgr = smgr->find("avionics", true);
|
|
_sgr->tie_to_listener();
|
|
|
|
make_voice(&voices.application_data_base_failed, "application-data-base-failed");
|
|
make_voice(&voices.bank_angle, "bank-angle");
|
|
make_voice(&voices.bank_angle_bank_angle, "bank-angle", "bank-angle");
|
|
make_voice(&voices.bank_angle_bank_angle_3, "bank-angle", "bank-angle", 3.0);
|
|
make_voice(&voices.bank_angle_inop, "bank-angle-inop");
|
|
make_voice(&voices.bank_angle_pause_bank_angle, "bank-angle", STDPAUSE, "bank-angle");
|
|
make_voice(&voices.bank_angle_pause_bank_angle_3, "bank-angle", STDPAUSE, "bank-angle", 3.0);
|
|
make_voice(&voices.callouts_inop, "callouts-inop");
|
|
make_voice(&voices.configuration_type_invalid, "configuration-type-invalid");
|
|
make_voice(&voices.dont_sink, "dont-sink");
|
|
make_voice(&voices.dont_sink_pause_dont_sink, "dont-sink", STDPAUSE, "dont-sink");
|
|
make_voice(&voices.five_hundred_above, "500-above");
|
|
make_voice(&voices.glideslope, "glideslope");
|
|
make_voice(&voices.glideslope_inop, "glideslope-inop");
|
|
make_voice(&voices.gpws_inop, "gpws-inop");
|
|
make_voice(&voices.hard_glideslope, "glideslope", "glideslope", 3.0);
|
|
make_voice(&voices.minimums, "minimums");
|
|
make_voice(&voices.minimums_minimums, "minimums", "minimums");
|
|
make_voice(&voices.pull_up, "pull-up");
|
|
make_voice(&voices.sink_rate, "sink-rate");
|
|
make_voice(&voices.sink_rate_pause_sink_rate, "sink-rate", STDPAUSE, "sink-rate");
|
|
make_voice(&voices.soft_glideslope, new Voice::SampleElement(get_sample("glideslope"), modify_amplitude(1.0, -6)));
|
|
make_voice(&voices.terrain, "terrain");
|
|
make_voice(&voices.terrain_pause_terrain, "terrain", STDPAUSE, "terrain");
|
|
make_voice(&voices.too_low_flaps, "too-low-flaps");
|
|
make_voice(&voices.too_low_gear, "too-low-gear");
|
|
make_voice(&voices.too_low_terrain, "too-low-terrain");
|
|
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
{
|
|
std::ostringstream name;
|
|
name << "altitude-" << mk->mode6_handler.altitude_callout_definitions[i];
|
|
make_voice(&voices.altitude_callouts[i], name.str().c_str());
|
|
}
|
|
|
|
speaker.update_configuration();
|
|
}
|
|
|
|
SGSoundSample *
|
|
MK_VIII::VoicePlayer::get_sample (const char *name)
|
|
{
|
|
std::ostringstream refname;
|
|
refname << mk->name << "[" << mk->num << "]" << "/" << name;
|
|
|
|
SGSoundSample *sample = _sgr->find(refname.str());
|
|
if (! sample)
|
|
{
|
|
string filename = "Sounds/mk-viii/" + string(name) + ".wav";
|
|
try
|
|
{
|
|
sample = new SGSoundSample(filename.c_str(), SGPath());
|
|
}
|
|
catch (const sg_exception &e)
|
|
{
|
|
SG_LOG(SG_INSTR, SG_ALERT, "Error loading MK VIII sound sample \"" + filename + "\": " + e.getFormattedMessage());
|
|
exit(1);
|
|
}
|
|
|
|
_sgr->add(sample, refname.str());
|
|
samples[refname.str()] = sample;
|
|
}
|
|
|
|
return sample;
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::play (Voice *_voice, unsigned int flags)
|
|
{
|
|
if (test_bits(flags, PLAY_NOW) || ! voice || voice->element->silence)
|
|
{
|
|
if (voice)
|
|
voice->stop(true);
|
|
|
|
voice = _voice;
|
|
looped = test_bits(flags, PLAY_LOOPED);
|
|
|
|
next_voice = NULL;
|
|
next_looped = false;
|
|
|
|
voice->play();
|
|
}
|
|
else
|
|
{
|
|
next_voice = _voice;
|
|
next_looped = test_bits(flags, PLAY_LOOPED);
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::stop (unsigned int flags)
|
|
{
|
|
if (voice)
|
|
{
|
|
voice->stop(test_bits(flags, STOP_NOW));
|
|
if (voice->element)
|
|
looped = false;
|
|
else
|
|
voice = NULL;
|
|
next_voice = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::set_volume (float _volume)
|
|
{
|
|
volume = _volume;
|
|
if (voice)
|
|
voice->volume_changed();
|
|
}
|
|
|
|
void
|
|
MK_VIII::VoicePlayer::update ()
|
|
{
|
|
if (voice)
|
|
{
|
|
voice->update();
|
|
|
|
if (next_voice)
|
|
{
|
|
if (! voice->element || voice->element->silence)
|
|
{
|
|
voice = next_voice;
|
|
looped = next_looped;
|
|
|
|
next_voice = NULL;
|
|
next_looped = false;
|
|
|
|
voice->play();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (! voice->element)
|
|
{
|
|
if (looped)
|
|
voice->play();
|
|
else
|
|
voice = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// SelfTestHandler ////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool
|
|
MK_VIII::SelfTestHandler::_was_here (int position)
|
|
{
|
|
if (position > current)
|
|
{
|
|
current = position;
|
|
return false;
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::sleep (double duration)
|
|
{
|
|
Action _action = { ACTION_SLEEP, duration, NULL };
|
|
return _action;
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::play (VoicePlayer::Voice *voice)
|
|
{
|
|
mk->voice_player.play(voice);
|
|
Action _action = { ACTION_VOICE, 0, NULL };
|
|
return _action;
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::discrete_on (bool *discrete, double duration)
|
|
{
|
|
*discrete = true;
|
|
return sleep(duration);
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::discrete_on_off (bool *discrete, double duration)
|
|
{
|
|
*discrete = true;
|
|
Action _action = { ACTION_SLEEP | ACTION_DISCRETE_ON_OFF, duration, discrete };
|
|
return _action;
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::discrete_on_off (bool *discrete, VoicePlayer::Voice *voice)
|
|
{
|
|
*discrete = true;
|
|
mk->voice_player.play(voice);
|
|
Action _action = { ACTION_VOICE | ACTION_DISCRETE_ON_OFF, 0, discrete };
|
|
return _action;
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::done ()
|
|
{
|
|
Action _action = { ACTION_DONE, 0, NULL };
|
|
return _action;
|
|
}
|
|
|
|
MK_VIII::SelfTestHandler::Action
|
|
MK_VIII::SelfTestHandler::run ()
|
|
{
|
|
// Note that "Terrain INOP" and "Terrain NA" are or'ed to the same
|
|
// output discrete (see [SPEC] 6.9.3.5).
|
|
|
|
#define was_here() was_here_offset(0)
|
|
#define was_here_offset(offset) _was_here(__LINE__ * 20 + (offset))
|
|
|
|
if (! was_here())
|
|
{
|
|
if (mk->configuration_module.state == ConfigurationModule::STATE_INVALID_DATABASE)
|
|
return play(mk_voice(application_data_base_failed));
|
|
else if (mk->configuration_module.state == ConfigurationModule::STATE_INVALID_AIRCRAFT_TYPE)
|
|
return play(mk_voice(configuration_type_invalid));
|
|
}
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return done();
|
|
|
|
if (! was_here())
|
|
return discrete_on(&mk_doutput(gpws_inop), 0.7);
|
|
if (! was_here())
|
|
return sleep(0.7); // W/S INOP
|
|
if (! was_here())
|
|
return discrete_on(&mk_doutput(tad_inop), 0.7);
|
|
if (! was_here())
|
|
{
|
|
mk_doutput(gpws_inop) = false;
|
|
// do not disable tad_inop since we must enable Terrain NA
|
|
// do not disable W/S INOP since we do not emulate it
|
|
return sleep(0.7); // Terrain NA
|
|
}
|
|
if (! was_here())
|
|
{
|
|
mk_doutput(tad_inop) = false; // disable Terrain NA
|
|
if (mk->io_handler.conf.momentary_flap_override_enabled)
|
|
return discrete_on_off(&mk_doutput(flap_override), 1.0);
|
|
}
|
|
if (! was_here())
|
|
return discrete_on_off(&mk_doutput(audio_on), 1.0);
|
|
if (! was_here())
|
|
{
|
|
if (mk->io_handler.momentary_steep_approach_enabled())
|
|
return discrete_on_off(&mk_doutput(steep_approach), 1.0);
|
|
}
|
|
|
|
if (! was_here())
|
|
{
|
|
if (mk_dinput(glideslope_inhibit))
|
|
goto glideslope_end;
|
|
else
|
|
{
|
|
if (mk->fault_handler.faults[FaultHandler::FAULT_MODE5_INPUTS_INVALID])
|
|
goto glideslope_inop;
|
|
}
|
|
}
|
|
if (! was_here())
|
|
return discrete_on_off(&mk_doutput(gpws_alert), mk_voice(glideslope));
|
|
if (! was_here())
|
|
return discrete_on_off(&mk_doutput(glideslope_cancel), 0.7);
|
|
goto glideslope_end;
|
|
glideslope_inop:
|
|
if (! was_here())
|
|
return play(mk_voice(glideslope_inop));
|
|
glideslope_end:
|
|
|
|
if (! was_here())
|
|
{
|
|
if (mk->fault_handler.faults[FaultHandler::FAULT_MODES14_INPUTS_INVALID])
|
|
goto gpws_inop;
|
|
}
|
|
if (! was_here())
|
|
return discrete_on_off(&mk_doutput(gpws_warning), mk_voice(pull_up));
|
|
goto gpws_end;
|
|
gpws_inop:
|
|
if (! was_here())
|
|
return play(mk_voice(gpws_inop));
|
|
gpws_end:
|
|
|
|
if (! was_here())
|
|
{
|
|
if (mk_dinput(self_test)) // proceed to long self test
|
|
goto long_test;
|
|
}
|
|
if (! was_here())
|
|
{
|
|
if (mk->mode6_handler.conf.bank_angle_enabled
|
|
&& mk->fault_handler.faults[FaultHandler::FAULT_BANK_ANGLE_INPUTS_INVALID])
|
|
return play(mk_voice(bank_angle_inop));
|
|
}
|
|
if (! was_here())
|
|
{
|
|
if (mk->mode6_handler.altitude_callouts_enabled()
|
|
&& mk->fault_handler.faults[FaultHandler::FAULT_MODE6_INPUTS_INVALID])
|
|
return play(mk_voice(callouts_inop));
|
|
}
|
|
if (! was_here())
|
|
return done();
|
|
|
|
long_test:
|
|
if (! was_here())
|
|
{
|
|
mk_doutput(gpws_inop) = true;
|
|
// do not enable W/S INOP, since we do not emulate it
|
|
mk_doutput(tad_inop) = true; // Terrain INOP, Terrain NA
|
|
|
|
return play(mk_voice(sink_rate));
|
|
}
|
|
if (! was_here())
|
|
return play(mk_voice(pull_up));
|
|
if (! was_here())
|
|
return play(mk_voice(terrain));
|
|
if (! was_here())
|
|
return play(mk_voice(pull_up));
|
|
if (! was_here())
|
|
return play(mk_voice(dont_sink));
|
|
if (! was_here())
|
|
return play(mk_voice(too_low_terrain));
|
|
if (! was_here())
|
|
return play(mk->mode4_handler.conf.voice_too_low_gear);
|
|
if (! was_here())
|
|
return play(mk_voice(too_low_flaps));
|
|
if (! was_here())
|
|
return play(mk_voice(too_low_terrain));
|
|
if (! was_here())
|
|
return play(mk_voice(glideslope));
|
|
if (! was_here())
|
|
{
|
|
if (mk->mode6_handler.conf.bank_angle_enabled)
|
|
return play(mk_voice(bank_angle));
|
|
}
|
|
|
|
if (! was_here())
|
|
{
|
|
if (! mk->mode6_handler.altitude_callouts_enabled())
|
|
goto callouts_disabled;
|
|
}
|
|
if (! was_here())
|
|
{
|
|
if (mk->mode6_handler.conf.minimums_enabled)
|
|
return play(mk_voice(minimums));
|
|
}
|
|
if (! was_here())
|
|
{
|
|
if (mk->mode6_handler.conf.above_field_voice)
|
|
return play(mk->mode6_handler.conf.above_field_voice);
|
|
}
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
if (! was_here_offset(i))
|
|
{
|
|
if (mk->mode6_handler.conf.altitude_callouts_enabled[i])
|
|
return play(mk_altitude_voice(i));
|
|
}
|
|
if (! was_here())
|
|
{
|
|
if (mk->mode6_handler.conf.smart_500_enabled)
|
|
return play(mk_altitude_voice(Mode6Handler::ALTITUDE_CALLOUT_500));
|
|
}
|
|
goto callouts_end;
|
|
callouts_disabled:
|
|
if (! was_here())
|
|
return play(mk_voice(minimums_minimums));
|
|
callouts_end:
|
|
|
|
if (! was_here())
|
|
{
|
|
if (mk->tcf_handler.conf.enabled)
|
|
return play(mk_voice(too_low_terrain));
|
|
}
|
|
|
|
return done();
|
|
}
|
|
|
|
void
|
|
MK_VIII::SelfTestHandler::start ()
|
|
{
|
|
assert(state == STATE_START);
|
|
|
|
memcpy(&saved_outputs, &mk->io_handler.outputs, sizeof(mk->io_handler.outputs));
|
|
memset(&mk->io_handler.outputs, 0, sizeof(mk->io_handler.outputs));
|
|
|
|
// [SPEC] 6.10.6: "The self-test results are annunciated, at 6db
|
|
// lower than the normal audio level selected for the aircraft"
|
|
mk->voice_player.set_volume(modify_amplitude(mk->voice_player.conf.volume, -6));
|
|
|
|
if (mk_dinput(mode6_low_volume))
|
|
mk->mode6_handler.set_volume(1.0);
|
|
|
|
state = STATE_RUNNING;
|
|
cancel = CANCEL_NONE;
|
|
memset(&action, 0, sizeof(action));
|
|
current = 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::SelfTestHandler::stop ()
|
|
{
|
|
if (state != STATE_NONE)
|
|
{
|
|
if (state != STATE_START)
|
|
{
|
|
mk->voice_player.stop(VoicePlayer::STOP_NOW);
|
|
mk->voice_player.set_volume(mk_dinput(audio_inhibit) ? 0.0 : mk->voice_player.conf.volume);
|
|
|
|
if (mk_dinput(mode6_low_volume))
|
|
mk->mode6_handler.set_volume(modify_amplitude(1.0, -6));
|
|
|
|
memcpy(&mk->io_handler.outputs, &saved_outputs, sizeof(mk->io_handler.outputs));
|
|
}
|
|
|
|
button_pressed = false;
|
|
state = STATE_NONE;
|
|
// reset self-test handler position
|
|
current=0;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::SelfTestHandler::handle_button_event (bool value)
|
|
{
|
|
if (state == STATE_NONE)
|
|
{
|
|
if (value)
|
|
state = STATE_START;
|
|
}
|
|
else if (state == STATE_START)
|
|
{
|
|
if (value)
|
|
state = STATE_NONE; // cancel the not-yet-started test
|
|
}
|
|
else if (cancel == CANCEL_NONE)
|
|
{
|
|
if (value)
|
|
{
|
|
assert(! button_pressed);
|
|
button_pressed = true;
|
|
button_press_timestamp = globals->get_sim_time_sec();
|
|
}
|
|
else
|
|
{
|
|
if (button_pressed)
|
|
{
|
|
if (globals->get_sim_time_sec() - button_press_timestamp < 2)
|
|
cancel = CANCEL_SHORT;
|
|
else
|
|
cancel = CANCEL_LONG;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
MK_VIII::SelfTestHandler::update ()
|
|
{
|
|
if (state == STATE_NONE)
|
|
return false;
|
|
else if (state == STATE_START)
|
|
{
|
|
if (mk->state_handler.ground && ! mk->io_handler.steep_approach())
|
|
start();
|
|
else
|
|
{
|
|
state = STATE_NONE;
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (button_pressed && cancel == CANCEL_NONE)
|
|
{
|
|
if (globals->get_sim_time_sec() - button_press_timestamp >= 2)
|
|
cancel = CANCEL_LONG;
|
|
}
|
|
}
|
|
|
|
if (! mk->state_handler.ground || cancel != CANCEL_NONE)
|
|
{
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
if (test_bits(action.flags, ACTION_SLEEP))
|
|
{
|
|
if (globals->get_sim_time_sec() - sleep_start < action.sleep_duration)
|
|
return true;
|
|
}
|
|
if (test_bits(action.flags, ACTION_VOICE))
|
|
{
|
|
if (mk->voice_player.voice)
|
|
return true;
|
|
}
|
|
if (test_bits(action.flags, ACTION_DISCRETE_ON_OFF))
|
|
*action.discrete = false;
|
|
|
|
action = run();
|
|
|
|
if (test_bits(action.flags, ACTION_SLEEP))
|
|
sleep_start = globals->get_sim_time_sec();
|
|
if (test_bits(action.flags, ACTION_DONE))
|
|
{
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// AlertHandler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool
|
|
MK_VIII::AlertHandler::select_voice_alerts (unsigned int test)
|
|
{
|
|
if (has_alerts(test))
|
|
{
|
|
voice_alerts = alerts & test;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
voice_alerts &= ~test;
|
|
if (voice_alerts == 0)
|
|
mk->voice_player.stop();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::AlertHandler::boot ()
|
|
{
|
|
reset();
|
|
}
|
|
|
|
void
|
|
MK_VIII::AlertHandler::reposition ()
|
|
{
|
|
reset();
|
|
|
|
mk->io_handler.set_lamp(IOHandler::LAMP_NONE);
|
|
mk->voice_player.stop(VoicePlayer::STOP_NOW);
|
|
}
|
|
|
|
void
|
|
MK_VIII::AlertHandler::reset ()
|
|
{
|
|
alerts = 0;
|
|
old_alerts = 0;
|
|
voice_alerts = 0;
|
|
repeated_alerts = 0;
|
|
altitude_callout_voice = NULL;
|
|
}
|
|
|
|
void
|
|
MK_VIII::AlertHandler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
// Lamps and voices are prioritized according to [SPEC] 6.9.2.
|
|
//
|
|
// Voices which can interrupt other voices (VoicePlayer::PLAY_NOW)
|
|
// are specified by [INSTALL] appendix E 5.3.5.
|
|
//
|
|
// When a voice is overriden by a higher priority voice and the
|
|
// overriding voice stops, we restore the previous voice if it was
|
|
// looped (this is not specified by [SPEC] but seems to make sense).
|
|
|
|
// update lamp
|
|
|
|
if (has_alerts(ALERT_MODE1_PULL_UP | ALERT_MODE2A | ALERT_MODE2B))
|
|
mk->io_handler.set_lamp(IOHandler::LAMP_WARNING);
|
|
else if (has_alerts(ALERT_MODE1_SINK_RATE
|
|
| ALERT_MODE2A_PREFACE
|
|
| ALERT_MODE2B_PREFACE
|
|
| ALERT_MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING
|
|
| ALERT_MODE2A_ALTITUDE_GAIN
|
|
| ALERT_MODE2B_LANDING_MODE
|
|
| ALERT_MODE3
|
|
| ALERT_MODE4_TOO_LOW_FLAPS
|
|
| ALERT_MODE4_TOO_LOW_GEAR
|
|
| ALERT_MODE4AB_TOO_LOW_TERRAIN
|
|
| ALERT_MODE4C_TOO_LOW_TERRAIN
|
|
| ALERT_TCF_TOO_LOW_TERRAIN))
|
|
mk->io_handler.set_lamp(IOHandler::LAMP_CAUTION);
|
|
else if (has_alerts(ALERT_MODE5_SOFT | ALERT_MODE5_HARD))
|
|
mk->io_handler.set_lamp(IOHandler::LAMP_GLIDESLOPE);
|
|
else
|
|
mk->io_handler.set_lamp(IOHandler::LAMP_NONE);
|
|
|
|
// update voice
|
|
|
|
if (select_voice_alerts(ALERT_MODE1_PULL_UP))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE1_PULL_UP))
|
|
{
|
|
if (mk->voice_player.voice != mk_voice(sink_rate_pause_sink_rate))
|
|
mk->voice_player.play(mk_voice(sink_rate), VoicePlayer::PLAY_NOW);
|
|
mk->voice_player.play(mk_voice(pull_up), VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE2A_PREFACE | ALERT_MODE2B_PREFACE))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE2A_PREFACE | ALERT_MODE2B_PREFACE))
|
|
mk->voice_player.play(mk_voice(terrain_pause_terrain), VoicePlayer::PLAY_NOW);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE2A | ALERT_MODE2B))
|
|
{
|
|
if (mk->voice_player.voice != mk_voice(pull_up))
|
|
mk->voice_player.play(mk_voice(pull_up), VoicePlayer::PLAY_NOW | VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING | ALERT_MODE2B_LANDING_MODE))
|
|
{
|
|
if (mk->voice_player.voice != mk_voice(terrain))
|
|
mk->voice_player.play(mk_voice(terrain), VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_MINIMUMS))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE6_MINIMUMS))
|
|
mk->voice_player.play(mk_voice(minimums_minimums));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE4AB_TOO_LOW_TERRAIN | ALERT_MODE4C_TOO_LOW_TERRAIN))
|
|
{
|
|
if (must_play_voice(ALERT_MODE4AB_TOO_LOW_TERRAIN | ALERT_MODE4C_TOO_LOW_TERRAIN))
|
|
mk->voice_player.play(mk_voice(too_low_terrain));
|
|
}
|
|
else if (select_voice_alerts(ALERT_TCF_TOO_LOW_TERRAIN))
|
|
{
|
|
if (must_play_voice(ALERT_TCF_TOO_LOW_TERRAIN))
|
|
mk->voice_player.play(mk_voice(too_low_terrain));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_ALTITUDE_CALLOUT))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE6_ALTITUDE_CALLOUT))
|
|
{
|
|
assert(altitude_callout_voice != NULL);
|
|
mk->voice_player.play(altitude_callout_voice);
|
|
altitude_callout_voice = NULL;
|
|
}
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE4_TOO_LOW_GEAR))
|
|
{
|
|
if (must_play_voice(ALERT_MODE4_TOO_LOW_GEAR))
|
|
mk->voice_player.play(mk->mode4_handler.conf.voice_too_low_gear);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE4_TOO_LOW_FLAPS))
|
|
{
|
|
if (must_play_voice(ALERT_MODE4_TOO_LOW_FLAPS))
|
|
mk->voice_player.play(mk_voice(too_low_flaps));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE1_SINK_RATE))
|
|
{
|
|
if (must_play_voice(ALERT_MODE1_SINK_RATE))
|
|
mk->voice_player.play(mk_voice(sink_rate_pause_sink_rate));
|
|
// [SPEC] 6.2.1: "During the time that the voice message for the
|
|
// outer envelope is inhibited and the caution/warning lamp is
|
|
// on, the Mode 5 alert message is allowed to annunciate for
|
|
// excessive glideslope deviation conditions."
|
|
else if (mk->voice_player.voice != mk_voice(sink_rate_pause_sink_rate)
|
|
&& mk->voice_player.next_voice != mk_voice(sink_rate_pause_sink_rate))
|
|
{
|
|
if (has_alerts(ALERT_MODE5_HARD))
|
|
{
|
|
voice_alerts |= ALERT_MODE5_HARD;
|
|
if (mk->voice_player.voice != mk_voice(hard_glideslope))
|
|
mk->voice_player.play(mk_voice(hard_glideslope), VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
else if (has_alerts(ALERT_MODE5_SOFT))
|
|
{
|
|
voice_alerts |= ALERT_MODE5_SOFT;
|
|
if (must_play_voice(ALERT_MODE5_SOFT))
|
|
mk->voice_player.play(mk_voice(soft_glideslope));
|
|
}
|
|
}
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE3))
|
|
{
|
|
if (must_play_voice(ALERT_MODE3))
|
|
mk->voice_player.play(mk_voice(dont_sink_pause_dont_sink));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE5_HARD))
|
|
{
|
|
if (mk->voice_player.voice != mk_voice(hard_glideslope))
|
|
mk->voice_player.play(mk_voice(hard_glideslope), VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE5_SOFT))
|
|
{
|
|
if (must_play_voice(ALERT_MODE5_SOFT))
|
|
mk->voice_player.play(mk_voice(soft_glideslope));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_LOW_BANK_ANGLE_3))
|
|
{
|
|
if (mk->voice_player.voice != mk_voice(bank_angle_bank_angle_3))
|
|
mk->voice_player.play(mk_voice(bank_angle_bank_angle_3), VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_HIGH_BANK_ANGLE_3))
|
|
{
|
|
if (mk->voice_player.voice != mk_voice(bank_angle_pause_bank_angle_3))
|
|
mk->voice_player.play(mk_voice(bank_angle_pause_bank_angle_3), VoicePlayer::PLAY_LOOPED);
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_LOW_BANK_ANGLE_2))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE6_LOW_BANK_ANGLE_2 | ALERT_MODE6_HIGH_BANK_ANGLE_2))
|
|
mk->voice_player.play(mk_voice(bank_angle_bank_angle));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_HIGH_BANK_ANGLE_2))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE6_LOW_BANK_ANGLE_2 | ALERT_MODE6_HIGH_BANK_ANGLE_2))
|
|
mk->voice_player.play(mk_voice(bank_angle_pause_bank_angle));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_LOW_BANK_ANGLE_1))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE6_LOW_BANK_ANGLE_1 | ALERT_MODE6_HIGH_BANK_ANGLE_1))
|
|
mk->voice_player.play(mk_voice(bank_angle_bank_angle));
|
|
}
|
|
else if (select_voice_alerts(ALERT_MODE6_HIGH_BANK_ANGLE_1))
|
|
{
|
|
if (! has_old_alerts(ALERT_MODE6_LOW_BANK_ANGLE_1 | ALERT_MODE6_HIGH_BANK_ANGLE_1))
|
|
mk->voice_player.play(mk_voice(bank_angle_pause_bank_angle));
|
|
}
|
|
|
|
// set new state
|
|
|
|
old_alerts = voice_alerts;
|
|
repeated_alerts = 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::AlertHandler::set_alerts (unsigned int _alerts,
|
|
unsigned int flags,
|
|
VoicePlayer::Voice *_altitude_callout_voice)
|
|
{
|
|
alerts |= _alerts;
|
|
if (test_bits(flags, ALERT_FLAG_REPEAT))
|
|
repeated_alerts |= _alerts;
|
|
if (_altitude_callout_voice)
|
|
altitude_callout_voice = _altitude_callout_voice;
|
|
}
|
|
|
|
void
|
|
MK_VIII::AlertHandler::unset_alerts (unsigned int _alerts)
|
|
{
|
|
alerts &= ~_alerts;
|
|
repeated_alerts &= ~_alerts;
|
|
if (_alerts & ALERT_MODE6_ALTITUDE_CALLOUT)
|
|
altitude_callout_voice = NULL;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// StateHandler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
MK_VIII::StateHandler::update_ground ()
|
|
{
|
|
if (ground)
|
|
{
|
|
if (! mk_ainput(computed_airspeed).ncd && mk_ainput(computed_airspeed).get() > 60
|
|
&& ! mk_data(radio_altitude).ncd && mk_data(radio_altitude).get() > 30)
|
|
{
|
|
if (potentially_airborne_timer.start_or_elapsed() > 1)
|
|
leave_ground();
|
|
}
|
|
else
|
|
potentially_airborne_timer.stop();
|
|
}
|
|
else
|
|
{
|
|
if (! mk_ainput(computed_airspeed).ncd && mk_ainput(computed_airspeed).get() < 40
|
|
&& ! mk_data(radio_altitude).ncd && mk_data(radio_altitude).get() < 30)
|
|
enter_ground();
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::enter_ground ()
|
|
{
|
|
ground = true;
|
|
mk->io_handler.enter_ground();
|
|
|
|
// [SPEC] 6.0.1 does not specify this, but it seems to be an
|
|
// omission; at this point, we must make sure that we are in takeoff
|
|
// mode (otherwise the next takeoff might happen in approach mode).
|
|
if (! takeoff)
|
|
enter_takeoff();
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::leave_ground ()
|
|
{
|
|
ground = false;
|
|
mk->mode2_handler.leave_ground();
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::update_takeoff ()
|
|
{
|
|
if (takeoff)
|
|
{
|
|
// [SPEC] 6.0.2 is somewhat confusing: it mentions hardcoded
|
|
// terrain clearance (500, 750) and airspeed (178, 200) values,
|
|
// but it also mentions the mode 4A boundary, which varies as a
|
|
// function of aircraft type. We consider that the mentioned
|
|
// values are examples, and that we should use the mode 4A upper
|
|
// boundary.
|
|
|
|
if (! mk_data(terrain_clearance).ncd
|
|
&& ! mk_ainput(computed_airspeed).ncd
|
|
&& mk_data(terrain_clearance).get() > mk->mode4_handler.get_upper_agl(mk->mode4_handler.conf.modes->ac))
|
|
leave_takeoff();
|
|
}
|
|
else
|
|
{
|
|
if (! mk_data(radio_altitude).ncd
|
|
&& mk_data(radio_altitude).get() < mk->mode4_handler.conf.modes->b->min_agl1
|
|
&& mk->io_handler.flaps_down()
|
|
&& mk_dinput(landing_gear))
|
|
enter_takeoff();
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::enter_takeoff ()
|
|
{
|
|
takeoff = true;
|
|
mk->io_handler.enter_takeoff();
|
|
mk->mode2_handler.enter_takeoff();
|
|
mk->mode3_handler.enter_takeoff();
|
|
mk->mode6_handler.enter_takeoff();
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::leave_takeoff ()
|
|
{
|
|
takeoff = false;
|
|
mk->mode6_handler.leave_takeoff();
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::post_reposition ()
|
|
{
|
|
// Synchronize the state with the current situation.
|
|
|
|
bool _ground = !
|
|
(! mk_ainput(computed_airspeed).ncd && mk_ainput(computed_airspeed).get() > 60
|
|
&& ! mk_data(radio_altitude).ncd && mk_data(radio_altitude).get() > 30);
|
|
|
|
bool _takeoff = _ground;
|
|
|
|
if (ground && ! _ground)
|
|
leave_ground();
|
|
else if (! ground && _ground)
|
|
enter_ground();
|
|
|
|
if (takeoff && ! _takeoff)
|
|
leave_takeoff();
|
|
else if (! takeoff && _takeoff)
|
|
enter_takeoff();
|
|
}
|
|
|
|
void
|
|
MK_VIII::StateHandler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
update_ground();
|
|
update_takeoff();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Mode1Handler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
double
|
|
MK_VIII::Mode1Handler::get_pull_up_bias ()
|
|
{
|
|
// [PILOT] page 54: "Steep Approach has priority over Flap Override
|
|
// if selected simultaneously."
|
|
|
|
if (mk->io_handler.steep_approach())
|
|
return 200;
|
|
else if (conf.envelopes->flap_override_bias && mk->io_handler.flap_override())
|
|
return 300;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode1Handler::is_pull_up ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(barometric_altitude_rate).ncd
|
|
&& mk_data(radio_altitude).get() > conf.envelopes->min_agl)
|
|
{
|
|
if (mk_data(radio_altitude).get() < conf.envelopes->pull_up_max_agl1)
|
|
{
|
|
if (mk_data(radio_altitude).get() < conf.envelopes->pull_up_min_agl1(mk_data(barometric_altitude_rate).get() + get_pull_up_bias()))
|
|
return true;
|
|
}
|
|
else if (mk_data(radio_altitude).get() < conf.envelopes->pull_up_max_agl2)
|
|
{
|
|
if (mk_data(radio_altitude).get() < conf.envelopes->pull_up_min_agl2(mk_data(barometric_altitude_rate).get() + get_pull_up_bias()))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode1Handler::update_pull_up ()
|
|
{
|
|
if (is_pull_up())
|
|
{
|
|
if (! mk_test_alert(MODE1_PULL_UP))
|
|
{
|
|
// [SPEC] 6.2.1: at least one sink rate must be issued
|
|
// before switching to pull up; if no sink rate has
|
|
// occurred, a 0.2 second delay is used.
|
|
if (mk->voice_player.voice == mk_voice(sink_rate_pause_sink_rate)
|
|
|| pull_up_timer.start_or_elapsed() >= 0.2)
|
|
mk_set_alerts(mk_alert(MODE1_PULL_UP));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pull_up_timer.stop();
|
|
mk_unset_alerts(mk_alert(MODE1_PULL_UP));
|
|
}
|
|
}
|
|
|
|
double
|
|
MK_VIII::Mode1Handler::get_sink_rate_bias ()
|
|
{
|
|
// [PILOT] page 54: "Steep Approach has priority over Flap Override
|
|
// if selected simultaneously."
|
|
|
|
if (mk->io_handler.steep_approach())
|
|
return 500;
|
|
else if (conf.envelopes->flap_override_bias && mk->io_handler.flap_override())
|
|
return 300;
|
|
else if (! mk_data(glideslope_deviation_dots).ncd)
|
|
{
|
|
double bias = 0;
|
|
|
|
if (mk_data(glideslope_deviation_dots).get() <= -2)
|
|
bias = 300;
|
|
else if (mk_data(glideslope_deviation_dots).get() < 0)
|
|
bias = -150 * mk_data(glideslope_deviation_dots).get();
|
|
|
|
if (mk_data(radio_altitude).get() < 100)
|
|
bias *= 0.01 * mk_data(radio_altitude).get();
|
|
|
|
return bias;
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode1Handler::is_sink_rate ()
|
|
{
|
|
return ! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(barometric_altitude_rate).ncd
|
|
&& mk_data(radio_altitude).get() > conf.envelopes->min_agl
|
|
&& mk_data(radio_altitude).get() < 2450
|
|
&& mk_data(radio_altitude).get() < -572 - 0.6035 * (mk_data(barometric_altitude_rate).get() + get_sink_rate_bias());
|
|
}
|
|
|
|
double
|
|
MK_VIII::Mode1Handler::get_sink_rate_tti ()
|
|
{
|
|
return mk_data(radio_altitude).get() / fabs(mk_data(barometric_altitude_rate).get());
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode1Handler::update_sink_rate ()
|
|
{
|
|
if (is_sink_rate())
|
|
{
|
|
if (mk_test_alert(MODE1_SINK_RATE))
|
|
{
|
|
double tti = get_sink_rate_tti();
|
|
if (tti <= sink_rate_tti * 0.8)
|
|
{
|
|
// ~20% degradation, warn again and store the new reference tti
|
|
sink_rate_tti = tti;
|
|
mk_repeat_alert(mk_alert(MODE1_SINK_RATE));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (sink_rate_timer.start_or_elapsed() >= 0.8)
|
|
{
|
|
mk_set_alerts(mk_alert(MODE1_SINK_RATE));
|
|
sink_rate_tti = get_sink_rate_tti();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sink_rate_timer.stop();
|
|
mk_unset_alerts(mk_alert(MODE1_SINK_RATE));
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode1Handler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
update_pull_up();
|
|
update_sink_rate();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Mode2Handler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
double
|
|
MK_VIII::Mode2Handler::ClosureRateFilter::limit_radio_altitude_rate (double r)
|
|
{
|
|
// Limit radio altitude rate according to aircraft configuration,
|
|
// allowing maximum sensitivity during cruise while providing
|
|
// progressively less sensitivity during the landing phases of
|
|
// flight.
|
|
|
|
if (! mk_data(glideslope_deviation_dots).ncd && fabs(mk_data(glideslope_deviation_dots).get()) <= 2)
|
|
{ // gs deviation <= +- 2 dots
|
|
if (mk_dinput(landing_gear) && mk->io_handler.flaps_down())
|
|
SG_CLAMP_RANGE(r, -1000.0, 3000.0);
|
|
else if (mk_dinput(landing_gear) || mk->io_handler.flaps_down())
|
|
SG_CLAMP_RANGE(r, 0.0, 4000.0);
|
|
else
|
|
SG_CLAMP_RANGE(r, 1000.0, 5000.0);
|
|
}
|
|
else
|
|
{ // no ILS, or gs deviation > +- 2 dots
|
|
if (mk_dinput(landing_gear) && mk->io_handler.flaps_down())
|
|
SG_CLAMP_RANGE(r, 0.0, 4000.0);
|
|
else if (mk_dinput(landing_gear) || mk->io_handler.flaps_down())
|
|
SG_CLAMP_RANGE(r, 1000.0, 5000.0);
|
|
// else no clamp
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::ClosureRateFilter::init ()
|
|
{
|
|
timer.stop();
|
|
last_ra.set(&mk_data(radio_altitude));
|
|
last_ba.set(&mk_ainput(uncorrected_barometric_altitude));
|
|
ra_filter.reset();
|
|
ba_filter.reset();
|
|
output.unset();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::ClosureRateFilter::update ()
|
|
{
|
|
double elapsed = timer.start_or_elapsed();
|
|
if (elapsed < 1)
|
|
return;
|
|
|
|
if (! mk_data(radio_altitude).ncd && ! mk_ainput(uncorrected_barometric_altitude).ncd)
|
|
{
|
|
if (! last_ra.ncd && ! last_ba.ncd)
|
|
{
|
|
// compute radio and barometric altitude rates (positive value = descent)
|
|
double ra_rate = -(mk_data(radio_altitude).get() - last_ra.get()) / elapsed * 60;
|
|
double ba_rate = -(mk_ainput(uncorrected_barometric_altitude).get() - last_ba.get()) / elapsed * 60;
|
|
|
|
// limit radio altitude rate according to aircraft configuration
|
|
ra_rate = limit_radio_altitude_rate(ra_rate);
|
|
|
|
// apply a low-pass filter to the radio altitude rate
|
|
ra_rate = ra_filter.filter(ra_rate);
|
|
// apply a high-pass filter to the barometric altitude rate
|
|
ba_rate = ba_filter.filter(ba_rate);
|
|
|
|
// combine both rates to obtain a closure rate
|
|
output.set(ra_rate + ba_rate);
|
|
}
|
|
else
|
|
output.unset();
|
|
}
|
|
else
|
|
{
|
|
ra_filter.reset();
|
|
ba_filter.reset();
|
|
output.unset();
|
|
}
|
|
|
|
timer.start();
|
|
last_ra.set(&mk_data(radio_altitude));
|
|
last_ba.set(&mk_ainput(uncorrected_barometric_altitude));
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode2Handler::b_conditions ()
|
|
{
|
|
return mk->io_handler.flaps_down()
|
|
|| (! mk_data(glideslope_deviation_dots).ncd && fabs(mk_data(glideslope_deviation_dots).get()) < 2)
|
|
|| takeoff_timer.running;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode2Handler::is_a ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_ainput(computed_airspeed).ncd
|
|
&& ! closure_rate_filter.output.ncd
|
|
&& ! b_conditions())
|
|
{
|
|
if (mk_data(radio_altitude).get() < 1220)
|
|
{
|
|
if (mk_data(radio_altitude).get() < -1579 + 0.7895 * closure_rate_filter.output.get())
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
double upper_limit;
|
|
|
|
if (mk_ainput(computed_airspeed).get() <= conf->airspeed1)
|
|
upper_limit = 1650;
|
|
else if (mk_ainput(computed_airspeed).get() >= conf->airspeed2)
|
|
upper_limit = 2450;
|
|
else
|
|
upper_limit = 1650 + 8.9 * (mk_ainput(computed_airspeed).get() - conf->airspeed1);
|
|
|
|
if (mk_data(radio_altitude).get() < upper_limit)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 522 + 0.1968 * closure_rate_filter.output.get())
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode2Handler::is_b ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(barometric_altitude_rate).ncd
|
|
&& ! closure_rate_filter.output.ncd
|
|
&& b_conditions()
|
|
&& mk_data(radio_altitude).get() < 789)
|
|
{
|
|
double lower_limit;
|
|
|
|
if (mk->io_handler.flaps_down())
|
|
{
|
|
if (mk_data(barometric_altitude_rate).get() > -400)
|
|
lower_limit = 200;
|
|
else if (mk_data(barometric_altitude_rate).get() < -1000)
|
|
lower_limit = 600;
|
|
else
|
|
lower_limit = -66.777 - 0.667 * mk_data(barometric_altitude_rate).get();
|
|
}
|
|
else
|
|
lower_limit = 30;
|
|
|
|
if (mk_data(radio_altitude).get() > lower_limit)
|
|
{
|
|
if (mk_data(radio_altitude).get() < -1579 + 0.7895 * closure_rate_filter.output.get())
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::check_pull_up (unsigned int preface_alert,
|
|
unsigned int alert)
|
|
{
|
|
if (pull_up_timer.running)
|
|
{
|
|
if (pull_up_timer.elapsed() >= 1)
|
|
{
|
|
mk_unset_alerts(preface_alert);
|
|
mk_set_alerts(alert);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (! mk->voice_player.voice)
|
|
pull_up_timer.start();
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::update_a ()
|
|
{
|
|
if (is_a())
|
|
{
|
|
if (mk_test_alert(MODE2A_PREFACE))
|
|
check_pull_up(mk_alert(MODE2A_PREFACE), mk_alert(MODE2A));
|
|
else if (! mk_test_alert(MODE2A))
|
|
{
|
|
mk_unset_alerts(mk_alert(MODE2A_ALTITUDE_GAIN) | mk_alert(MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING));
|
|
mk_set_alerts(mk_alert(MODE2A_PREFACE));
|
|
a_start_time = globals->get_sim_time_sec();
|
|
pull_up_timer.stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (mk_test_alert(MODE2A_ALTITUDE_GAIN))
|
|
{
|
|
if (mk->io_handler.gpws_inhibit()
|
|
|| mk->state_handler.ground // [1]
|
|
|| a_altitude_gain_timer.elapsed() >= 45
|
|
|| mk_data(radio_altitude).ncd
|
|
|| mk_ainput(uncorrected_barometric_altitude).ncd
|
|
|| mk_ainput(uncorrected_barometric_altitude).get() - a_altitude_gain_alt >= 300
|
|
// [PILOT] page 12: "the visual alert will remain on
|
|
// until [...] landing flaps or the flap override switch
|
|
// is activated"
|
|
|| mk->io_handler.flaps_down())
|
|
{
|
|
// exit altitude gain mode
|
|
a_altitude_gain_timer.stop();
|
|
mk_unset_alerts(mk_alert(MODE2A_ALTITUDE_GAIN) | mk_alert(MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING));
|
|
}
|
|
else
|
|
{
|
|
// [SPEC] 6.2.2.2: "If the terrain starts to fall away
|
|
// during this altitude gain time, the voice will shut
|
|
// off"
|
|
if (closure_rate_filter.output.get() < 0)
|
|
mk_unset_alerts(mk_alert(MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING));
|
|
}
|
|
}
|
|
else if (mk_test_alerts(mk_alert(MODE2A_PREFACE) | mk_alert(MODE2A)))
|
|
{
|
|
mk_unset_alerts(mk_alert(MODE2A_PREFACE) | mk_alert(MODE2A));
|
|
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& globals->get_sim_time_sec() - a_start_time > 3
|
|
&& ! mk->io_handler.flaps_down()
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_ainput(uncorrected_barometric_altitude).ncd)
|
|
{
|
|
// [SPEC] 6.2.2.2: mode 2A envelope violated for more
|
|
// than 3 seconds, enable altitude gain feature
|
|
|
|
a_altitude_gain_timer.start();
|
|
a_altitude_gain_alt = mk_ainput(uncorrected_barometric_altitude).get();
|
|
|
|
unsigned int alerts = mk_alert(MODE2A_ALTITUDE_GAIN);
|
|
if (closure_rate_filter.output.get() > 0)
|
|
alerts |= mk_alert(MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING);
|
|
|
|
mk_set_alerts(alerts);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::update_b ()
|
|
{
|
|
bool b = is_b();
|
|
|
|
// handle normal mode
|
|
|
|
if (b && (! mk_dinput(landing_gear) || ! mk->io_handler.flaps_down()))
|
|
{
|
|
if (mk_test_alert(MODE2B_PREFACE))
|
|
check_pull_up(mk_alert(MODE2B_PREFACE), mk_alert(MODE2B));
|
|
else if (! mk_test_alert(MODE2B))
|
|
{
|
|
mk_set_alerts(mk_alert(MODE2B_PREFACE));
|
|
pull_up_timer.stop();
|
|
}
|
|
}
|
|
else
|
|
mk_unset_alerts(mk_alert(MODE2B_PREFACE) | mk_alert(MODE2B));
|
|
|
|
// handle landing mode ([SPEC] 6.2.2.3: "If both landing gear and
|
|
// flaps are in the landing configuration, then the message will be
|
|
// Terrain")
|
|
|
|
if (b && mk_dinput(landing_gear) && mk->io_handler.flaps_down())
|
|
mk_set_alerts(mk_alert(MODE2B_LANDING_MODE));
|
|
else
|
|
mk_unset_alerts(mk_alert(MODE2B_LANDING_MODE));
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::boot ()
|
|
{
|
|
closure_rate_filter.init();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::power_off ()
|
|
{
|
|
// [SPEC] 6.0.4: "This latching function is not power saved and a
|
|
// system reset will force it false."
|
|
takeoff_timer.stop();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::leave_ground ()
|
|
{
|
|
// takeoff, reset timer
|
|
takeoff_timer.start();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::enter_takeoff ()
|
|
{
|
|
// reset timer, in case it's a go-around
|
|
takeoff_timer.start();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode2Handler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
closure_rate_filter.update();
|
|
|
|
if (takeoff_timer.running && takeoff_timer.elapsed() >= 60)
|
|
takeoff_timer.stop();
|
|
|
|
update_a();
|
|
update_b();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Mode3Handler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
double
|
|
MK_VIII::Mode3Handler::max_alt_loss (double _bias)
|
|
{
|
|
return conf->max_alt_loss(mk->io_handler.flap_override(), mk_data(radio_altitude).get()) + mk_data(radio_altitude).get() * _bias;
|
|
}
|
|
|
|
double
|
|
MK_VIII::Mode3Handler::get_bias (double initial_bias, double alt_loss)
|
|
{
|
|
// do not repeat altitude-loss alerts below 30ft agl
|
|
if (mk_data(radio_altitude).get() > 30)
|
|
{
|
|
if (initial_bias < 0.0) // sanity check
|
|
initial_bias = 0.0;
|
|
// mk-viii spec: repeat alerts whenever losing 20% of initial altitude
|
|
while ((alt_loss > max_alt_loss(initial_bias))&&
|
|
(initial_bias < 1.0))
|
|
initial_bias += 0.2;
|
|
}
|
|
|
|
return initial_bias;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode3Handler::is (double *alt_loss)
|
|
{
|
|
if (has_descent_alt)
|
|
{
|
|
int max_agl = conf->max_agl(mk->io_handler.flap_override());
|
|
|
|
if (mk_ainput(uncorrected_barometric_altitude).ncd
|
|
|| mk_ainput(uncorrected_barometric_altitude).get() > descent_alt
|
|
|| mk_data(radio_altitude).ncd
|
|
|| mk_data(radio_altitude).get() > max_agl)
|
|
{
|
|
armed = false;
|
|
has_descent_alt = false;
|
|
}
|
|
else
|
|
{
|
|
// gear/flaps: [SPEC] 1.3.1.3
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& (! mk_dinput(landing_gear) || ! mk->io_handler.flaps_down())
|
|
&& ! mk_data(barometric_altitude_rate).ncd
|
|
&& ! mk_ainput(uncorrected_barometric_altitude).ncd
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& mk_data(barometric_altitude_rate).get() < 0)
|
|
{
|
|
double _alt_loss = descent_alt - mk_ainput(uncorrected_barometric_altitude).get();
|
|
if (armed || (mk_data(radio_altitude).get() > conf->min_agl
|
|
&& mk_data(radio_altitude).get() < max_agl
|
|
&& _alt_loss > max_alt_loss(0)))
|
|
{
|
|
*alt_loss = _alt_loss;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (! mk_data(barometric_altitude_rate).ncd
|
|
&& ! mk_ainput(uncorrected_barometric_altitude).ncd
|
|
&& mk_data(barometric_altitude_rate).get() < 0)
|
|
{
|
|
has_descent_alt = true;
|
|
descent_alt = mk_ainput(uncorrected_barometric_altitude).get();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode3Handler::enter_takeoff ()
|
|
{
|
|
armed = false;
|
|
has_descent_alt = false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode3Handler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
if (mk->state_handler.takeoff)
|
|
{
|
|
double alt_loss;
|
|
|
|
if (! mk->state_handler.ground /* [1] */ && is(&alt_loss))
|
|
{
|
|
if (mk_test_alert(MODE3))
|
|
{
|
|
double new_bias = get_bias(bias, alt_loss);
|
|
if (new_bias > bias)
|
|
{
|
|
bias = new_bias;
|
|
mk_repeat_alert(mk_alert(MODE3));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
armed = true;
|
|
bias = get_bias(0.2, alt_loss);
|
|
mk_set_alerts(mk_alert(MODE3));
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
mk_unset_alerts(mk_alert(MODE3));
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Mode4Handler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// FIXME: for turbofans, the upper limit should be reduced from 1000
|
|
// to 800 ft if a sudden change in radio altitude is detected, in
|
|
// order to reduce nuisance alerts caused by overflying another
|
|
// aircraft (see [PILOT] page 16).
|
|
|
|
double
|
|
MK_VIII::Mode4Handler::get_upper_agl (const EnvelopesConfiguration *c)
|
|
{
|
|
if (mk_ainput(computed_airspeed).get() >= c->airspeed2)
|
|
return c->min_agl3;
|
|
else if (mk_ainput(computed_airspeed).get() >= c->airspeed1)
|
|
return c->min_agl2(mk_ainput(computed_airspeed).get());
|
|
else
|
|
return c->min_agl1;
|
|
}
|
|
|
|
const MK_VIII::Mode4Handler::EnvelopesConfiguration *
|
|
MK_VIII::Mode4Handler::get_ab_envelope ()
|
|
{
|
|
// [SPEC] 6.2.4.1: "The Mode 4B envelope can also be selected by
|
|
// setting flaps to landing configuration or by selecting flap
|
|
// override."
|
|
return mk_dinput(landing_gear) || mk->io_handler.flaps_down()
|
|
? conf.modes->b
|
|
: conf.modes->ac;
|
|
}
|
|
|
|
double
|
|
MK_VIII::Mode4Handler::get_bias (double initial_bias, double min_agl)
|
|
{
|
|
// do not repeat terrain/gear/flap alerts below 30ft agl
|
|
if (mk_data(radio_altitude).get() > 30.0)
|
|
{
|
|
if (initial_bias < 0.0) // sanity check
|
|
initial_bias = 0.0;
|
|
while ((mk_data(radio_altitude).get() < min_agl - min_agl * initial_bias)&&
|
|
(initial_bias < 1.0))
|
|
initial_bias += 0.2;
|
|
}
|
|
|
|
return initial_bias;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode4Handler::handle_alert (unsigned int alert,
|
|
double min_agl,
|
|
double *bias)
|
|
{
|
|
if (mk_test_alerts(alert))
|
|
{
|
|
double new_bias = get_bias(*bias, min_agl);
|
|
if (new_bias > *bias)
|
|
{
|
|
*bias = new_bias;
|
|
mk_repeat_alert(alert);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*bias = get_bias(0.2, min_agl);
|
|
mk_set_alerts(alert);
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode4Handler::update_ab ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground
|
|
&& ! mk->state_handler.takeoff
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_ainput(computed_airspeed).ncd
|
|
&& mk_data(radio_altitude).get() > 30)
|
|
{
|
|
const EnvelopesConfiguration *c = get_ab_envelope();
|
|
if (mk_ainput(computed_airspeed).get() < c->airspeed1)
|
|
{
|
|
if (mk_dinput(landing_gear))
|
|
{
|
|
if (! mk->io_handler.flaps_down() && mk_data(radio_altitude).get() < c->min_agl1)
|
|
{
|
|
handle_alert(mk_alert(MODE4_TOO_LOW_FLAPS), c->min_agl1, &ab_bias);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (mk_data(radio_altitude).get() < c->min_agl1)
|
|
{
|
|
handle_alert(mk_alert(MODE4_TOO_LOW_GEAR), c->min_agl1, &ab_bias);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mk_unset_alerts(mk_alert(MODE4_TOO_LOW_FLAPS) | mk_alert(MODE4_TOO_LOW_GEAR));
|
|
ab_bias=0.0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode4Handler::update_ab_expanded ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground
|
|
&& ! mk->state_handler.takeoff
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_ainput(computed_airspeed).ncd
|
|
&& mk_data(radio_altitude).get() > 30)
|
|
{
|
|
const EnvelopesConfiguration *c = get_ab_envelope();
|
|
if (mk_ainput(computed_airspeed).get() >= c->airspeed1)
|
|
{
|
|
double min_agl = get_upper_agl(c);
|
|
if (mk_data(radio_altitude).get() < min_agl)
|
|
{
|
|
handle_alert(mk_alert(MODE4AB_TOO_LOW_TERRAIN), min_agl, &ab_expanded_bias);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
mk_unset_alerts(mk_alert(MODE4AB_TOO_LOW_TERRAIN));
|
|
ab_expanded_bias=0.0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode4Handler::update_c ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& mk->state_handler.takeoff
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(terrain_clearance).ncd
|
|
&& mk_data(radio_altitude).get() > 30
|
|
&& (! mk_dinput(landing_gear) || ! mk->io_handler.flaps_down())
|
|
&& mk_data(radio_altitude).get() < get_upper_agl(conf.modes->ac)
|
|
&& mk_data(radio_altitude).get() < mk_data(terrain_clearance).get())
|
|
handle_alert(mk_alert(MODE4C_TOO_LOW_TERRAIN), mk_data(terrain_clearance).get(), &c_bias);
|
|
else
|
|
{
|
|
mk_unset_alerts(mk_alert(MODE4C_TOO_LOW_TERRAIN));
|
|
c_bias=0.0;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode4Handler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
update_ab();
|
|
update_ab_expanded();
|
|
update_c();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Mode5Handler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool
|
|
MK_VIII::Mode5Handler::is_hard ()
|
|
{
|
|
if (mk_data(radio_altitude).get() > 30
|
|
&& mk_data(radio_altitude).get() < 300
|
|
&& mk_data(glideslope_deviation_dots).get() > 2)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 150)
|
|
{
|
|
if (mk_data(radio_altitude).get() > 293 - 71.43 * mk_data(glideslope_deviation_dots).get())
|
|
return true;
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode5Handler::is_soft (double bias)
|
|
{
|
|
// do not repeat glide-slope alerts below 30ft agl
|
|
if (mk_data(radio_altitude).get() > 30)
|
|
{
|
|
double bias_dots = 1.3 * bias;
|
|
if (mk_data(glideslope_deviation_dots).get() > 1.3 + bias_dots)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 150)
|
|
{
|
|
if (mk_data(radio_altitude).get() > 243 - 71.43 * (mk_data(glideslope_deviation_dots).get() - bias_dots))
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
double upper_limit;
|
|
|
|
if (mk_data(barometric_altitude_rate).ncd)
|
|
upper_limit = 1000;
|
|
else
|
|
{
|
|
if (mk_data(barometric_altitude_rate).get() >= 0)
|
|
upper_limit = 500;
|
|
else if (mk_data(barometric_altitude_rate).get() < -500)
|
|
upper_limit = 1000;
|
|
else
|
|
upper_limit = -mk_data(barometric_altitude_rate).get() + 500;
|
|
}
|
|
|
|
if (mk_data(radio_altitude).get() < upper_limit)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
double
|
|
MK_VIII::Mode5Handler::get_soft_bias (double initial_bias)
|
|
{
|
|
if (initial_bias < 0.0) // sanity check
|
|
initial_bias = 0.0;
|
|
while ((is_soft(initial_bias))&&
|
|
(initial_bias < 1.0))
|
|
initial_bias += 0.2;
|
|
|
|
return initial_bias;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode5Handler::update_hard (bool is)
|
|
{
|
|
if (is)
|
|
{
|
|
if (! mk_test_alert(MODE5_HARD))
|
|
{
|
|
if (hard_timer.start_or_elapsed() >= 0.8)
|
|
mk_set_alerts(mk_alert(MODE5_HARD));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hard_timer.stop();
|
|
mk_unset_alerts(mk_alert(MODE5_HARD));
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode5Handler::update_soft (bool is)
|
|
{
|
|
if (is)
|
|
{
|
|
if (! mk_test_alert(MODE5_SOFT))
|
|
{
|
|
double new_bias = get_soft_bias(soft_bias);
|
|
if (new_bias > soft_bias)
|
|
{
|
|
soft_bias = new_bias;
|
|
mk_repeat_alert(mk_alert(MODE5_SOFT));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (soft_timer.start_or_elapsed() >= 0.8)
|
|
{
|
|
soft_bias = get_soft_bias(0.2);
|
|
mk_set_alerts(mk_alert(MODE5_SOFT));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
soft_timer.stop();
|
|
mk_unset_alerts(mk_alert(MODE5_SOFT));
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode5Handler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_dinput(glideslope_inhibit) // not on backcourse
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(glideslope_deviation_dots).ncd
|
|
&& (! mk->io_handler.conf.localizer_enabled
|
|
|| mk_data(localizer_deviation_dots).ncd
|
|
|| mk_data(radio_altitude).get() < 500
|
|
|| fabs(mk_data(localizer_deviation_dots).get()) < 2)
|
|
&& (! mk->state_handler.takeoff || mk->io_handler.flaps_down())
|
|
&& mk_dinput(landing_gear)
|
|
&& ! mk_doutput(glideslope_cancel))
|
|
{
|
|
update_hard(is_hard());
|
|
update_soft(is_soft(0));
|
|
}
|
|
else
|
|
{
|
|
update_hard(false);
|
|
update_soft(false);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Mode6Handler ///////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// keep sorted in descending order
|
|
const int MK_VIII::Mode6Handler::altitude_callout_definitions[] =
|
|
{ 1000, 500, 400, 300, 200, 100, 50, 40, 30, 20, 10 };
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::reset_minimums ()
|
|
{
|
|
minimums_issued = false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::reset_altitude_callouts ()
|
|
{
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
altitude_callouts_issued[i] = false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::is_playing_altitude_callout ()
|
|
{
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
if (mk->voice_player.voice == mk_altitude_voice(i)
|
|
|| mk->voice_player.next_voice == mk_altitude_voice(i))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::is_near_minimums (double callout)
|
|
{
|
|
// [SPEC] 6.4.2
|
|
|
|
if (! mk_data(decision_height).ncd)
|
|
{
|
|
double diff = callout - mk_data(decision_height).get();
|
|
|
|
if (mk_data(radio_altitude).get() >= 200)
|
|
{
|
|
if (fabs(diff) <= 30)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (diff >= -3 && diff <= 6)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::is_outside_band (double elevation, double callout)
|
|
{
|
|
// [SPEC] 6.4.2
|
|
return elevation < callout - (elevation > 150 ? 20 : 10);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::inhibit_smart_500 ()
|
|
{
|
|
// [SPEC] 6.4.3
|
|
|
|
if (! mk_data(glideslope_deviation_dots).ncd
|
|
&& ! mk_dinput(glideslope_inhibit) // backcourse
|
|
&& ! mk_doutput(glideslope_cancel))
|
|
{
|
|
if (mk->io_handler.conf.localizer_enabled
|
|
&& ! mk_data(localizer_deviation_dots).ncd)
|
|
{
|
|
if (fabs(mk_data(localizer_deviation_dots).get()) <= 2)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (fabs(mk_data(glideslope_deviation_dots).get()) <= 2)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::boot ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
last_decision_height = mk_dinput(decision_height);
|
|
last_radio_altitude.set(&mk_data(radio_altitude));
|
|
|
|
// [SPEC] 6.4.2
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
altitude_callouts_issued[i] = ! mk_data(radio_altitude).ncd
|
|
&& mk_data(radio_altitude).get() <= altitude_callout_definitions[i];
|
|
|
|
// extrapolated from [SPEC] 6.4.2
|
|
minimums_issued = mk_dinput(decision_height);
|
|
|
|
if (conf.above_field_voice)
|
|
{
|
|
update_runway();
|
|
get_altitude_above_field(&last_altitude_above_field);
|
|
// extrapolated from [SPEC] 6.4.2
|
|
above_field_issued = ! last_altitude_above_field.ncd
|
|
&& last_altitude_above_field.get() < 550;
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::power_off ()
|
|
{
|
|
runway_timer.stop();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::enter_takeoff ()
|
|
{
|
|
reset_altitude_callouts(); // [SPEC] 6.4.2
|
|
reset_minimums(); // omitted by [SPEC]; common sense
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::leave_takeoff ()
|
|
{
|
|
reset_altitude_callouts(); // [SPEC] 6.0.2
|
|
reset_minimums(); // [SPEC] 6.0.2
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::set_volume (float volume)
|
|
{
|
|
mk_voice(minimums_minimums)->set_volume(volume);
|
|
mk_voice(five_hundred_above)->set_volume(volume);
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
mk_altitude_voice(i)->set_volume(volume);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::altitude_callouts_enabled ()
|
|
{
|
|
if (conf.minimums_enabled || conf.smart_500_enabled || conf.above_field_voice)
|
|
return true;
|
|
|
|
for (unsigned i = 0; i < n_altitude_callouts; i++)
|
|
if (conf.altitude_callouts_enabled[i])
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::update_minimums ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& (mk->voice_player.voice == mk_voice(minimums_minimums)
|
|
|| mk->voice_player.next_voice == mk_voice(minimums_minimums)))
|
|
goto end;
|
|
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& conf.minimums_enabled
|
|
&& ! minimums_issued
|
|
&& mk_dinput(landing_gear)
|
|
&& mk_dinput(decision_height)
|
|
&& ! last_decision_height)
|
|
{
|
|
minimums_issued = true;
|
|
|
|
// If an altitude callout is playing, stop it now so that the
|
|
// minimums callout can be played (note that proper connection
|
|
// and synchronization of the radio-altitude ARINC 529 input,
|
|
// decision-height discrete and decision-height ARINC 529 input
|
|
// will prevent an altitude callout from being played near the
|
|
// decision height).
|
|
|
|
if (is_playing_altitude_callout())
|
|
mk->voice_player.stop(VoicePlayer::STOP_NOW);
|
|
|
|
mk_set_alerts(mk_alert(MODE6_MINIMUMS));
|
|
}
|
|
else
|
|
mk_unset_alerts(mk_alert(MODE6_MINIMUMS));
|
|
|
|
end:
|
|
last_decision_height = mk_dinput(decision_height);
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::update_altitude_callouts ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit() && is_playing_altitude_callout())
|
|
goto end;
|
|
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(radio_altitude).ncd)
|
|
for (unsigned i = 0; i < n_altitude_callouts && mk_data(radio_altitude).get() <= altitude_callout_definitions[i]; i++)
|
|
if ((conf.altitude_callouts_enabled[i]
|
|
|| (altitude_callout_definitions[i] == 500
|
|
&& conf.smart_500_enabled))
|
|
&& ! altitude_callouts_issued[i]
|
|
&& (last_radio_altitude.ncd
|
|
|| last_radio_altitude.get() > altitude_callout_definitions[i]))
|
|
{
|
|
// lock out all callouts superior or equal to this one
|
|
for (unsigned j = 0; j <= i; j++)
|
|
altitude_callouts_issued[j] = true;
|
|
|
|
altitude_callouts_issued[i] = true;
|
|
if (! is_near_minimums(altitude_callout_definitions[i])
|
|
&& ! is_outside_band(mk_data(radio_altitude).get(), altitude_callout_definitions[i])
|
|
&& (! conf.smart_500_enabled
|
|
|| altitude_callout_definitions[i] != 500
|
|
|| ! inhibit_smart_500()))
|
|
{
|
|
mk->alert_handler.set_altitude_callout_alert(mk_altitude_voice(i));
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
mk_unset_alerts(mk_alert(MODE6_ALTITUDE_CALLOUT));
|
|
|
|
end:
|
|
last_radio_altitude.set(&mk_data(radio_altitude));
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::test_runway (const FGRunway *_runway)
|
|
{
|
|
if (_runway->lengthFt() < mk->conf.runway_database)
|
|
return false;
|
|
|
|
SGGeod pos(
|
|
SGGeod::fromDeg(mk_data(gps_longitude).get(), mk_data(gps_latitude).get()));
|
|
|
|
// get distance to threshold
|
|
double distance, az1, az2;
|
|
SGGeodesy::inverse(pos, _runway->threshold(), az1, az2, distance);
|
|
return distance * SG_METER_TO_NM <= 5;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::test_airport (const FGAirport *airport)
|
|
{
|
|
for (unsigned int r=0; r<airport->numRunways(); ++r) {
|
|
FGRunway* rwy(airport->getRunwayByIndex(r));
|
|
|
|
if (test_runway(rwy)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MK_VIII::Mode6Handler::AirportFilter::passAirport(FGAirport* a) const
|
|
{
|
|
bool ok = self->test_airport(a);
|
|
return ok;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::update_runway ()
|
|
{
|
|
|
|
if (mk_data(gps_latitude).ncd || mk_data(gps_longitude).ncd) {
|
|
has_runway.unset();
|
|
return;
|
|
}
|
|
|
|
// Search for the closest runway threshold in range 5
|
|
// nm. Passing 30nm to
|
|
// get_closest_airport() provides enough margin for large
|
|
// airports, which may have a runway located far away from the
|
|
// airport's reference point.
|
|
AirportFilter filter(this);
|
|
FGPositionedRef apt = FGPositioned::findClosest(
|
|
SGGeod::fromDeg(mk_data(gps_longitude).get(), mk_data(gps_latitude).get()),
|
|
30.0, &filter);
|
|
if (apt) {
|
|
runway.elevation = apt->elevation();
|
|
}
|
|
|
|
has_runway.set(apt != NULL);
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::get_altitude_above_field (Parameter<double> *parameter)
|
|
{
|
|
if (! has_runway.ncd && has_runway.get() && ! mk_data(geometric_altitude).ncd)
|
|
parameter->set(mk_data(geometric_altitude).get() - runway.elevation);
|
|
else
|
|
parameter->unset();
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::update_above_field_callout ()
|
|
{
|
|
if (! conf.above_field_voice)
|
|
return;
|
|
|
|
// update_runway() has to iterate over the whole runway database
|
|
// (which contains thousands of runways), so it would be unwise to
|
|
// run it every frame. Run it every 3 seconds. Note that the first
|
|
// iteration is run in boot().
|
|
if (runway_timer.start_or_elapsed() >= 3)
|
|
{
|
|
update_runway();
|
|
runway_timer.start();
|
|
}
|
|
|
|
Parameter<double> altitude_above_field;
|
|
get_altitude_above_field(&altitude_above_field);
|
|
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& (mk->voice_player.voice == conf.above_field_voice
|
|
|| mk->voice_player.next_voice == conf.above_field_voice))
|
|
goto end;
|
|
|
|
// handle reset term
|
|
if (above_field_issued)
|
|
{
|
|
if ((! has_runway.ncd && ! has_runway.get())
|
|
|| (! altitude_above_field.ncd && altitude_above_field.get() > 700))
|
|
above_field_issued = false;
|
|
}
|
|
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! above_field_issued
|
|
&& ! altitude_above_field.ncd
|
|
&& altitude_above_field.get() < 550
|
|
&& (last_altitude_above_field.ncd
|
|
|| last_altitude_above_field.get() >= 550))
|
|
{
|
|
above_field_issued = true;
|
|
|
|
if (! is_outside_band(altitude_above_field.get(), 500))
|
|
{
|
|
mk->alert_handler.set_altitude_callout_alert(conf.above_field_voice);
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
mk_unset_alerts(mk_alert(MODE6_ALTITUDE_CALLOUT));
|
|
|
|
end:
|
|
last_altitude_above_field.set(&altitude_above_field);
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::is_bank_angle (double abs_roll_angle, double bias)
|
|
{
|
|
return conf.is_bank_angle(&mk_data(radio_altitude),
|
|
abs_roll_angle - abs_roll_angle * bias,
|
|
mk_dinput(autopilot_engaged));
|
|
}
|
|
|
|
bool
|
|
MK_VIII::Mode6Handler::is_high_bank_angle ()
|
|
{
|
|
return mk_data(radio_altitude).ncd || mk_data(radio_altitude).get() >= 210;
|
|
}
|
|
|
|
unsigned int
|
|
MK_VIII::Mode6Handler::get_bank_angle_alerts ()
|
|
{
|
|
if (! mk->io_handler.gpws_inhibit()
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(roll_angle).ncd)
|
|
{
|
|
double abs_roll_angle = fabs(mk_data(roll_angle).get());
|
|
|
|
if (is_bank_angle(abs_roll_angle, 0.4))
|
|
return is_high_bank_angle()
|
|
? (mk_alert(MODE6_HIGH_BANK_ANGLE_1) | mk_alert(MODE6_HIGH_BANK_ANGLE_2) | mk_alert(MODE6_HIGH_BANK_ANGLE_3))
|
|
: (mk_alert(MODE6_LOW_BANK_ANGLE_1) | mk_alert(MODE6_LOW_BANK_ANGLE_2) | mk_alert(MODE6_LOW_BANK_ANGLE_3));
|
|
else if (is_bank_angle(abs_roll_angle, 0.2))
|
|
return is_high_bank_angle()
|
|
? (mk_alert(MODE6_HIGH_BANK_ANGLE_1) | mk_alert(MODE6_HIGH_BANK_ANGLE_2))
|
|
: (mk_alert(MODE6_LOW_BANK_ANGLE_1) | mk_alert(MODE6_LOW_BANK_ANGLE_2));
|
|
else if (is_bank_angle(abs_roll_angle, 0))
|
|
return is_high_bank_angle()
|
|
? mk_alert(MODE6_HIGH_BANK_ANGLE_1)
|
|
: mk_alert(MODE6_LOW_BANK_ANGLE_1);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::update_bank_angle ()
|
|
{
|
|
if (! conf.bank_angle_enabled)
|
|
return;
|
|
|
|
unsigned int alerts = get_bank_angle_alerts();
|
|
|
|
// [SPEC] 6.4.4 specifies that the non-continuous alerts
|
|
// (MODE6_*_BANK_ANGLE_1 and MODE6_*_BANK_ANGLE_2) are locked out
|
|
// until the initial envelope is exited.
|
|
|
|
if (! test_bits(alerts, mk_alert(MODE6_LOW_BANK_ANGLE_3)))
|
|
mk_unset_alerts(mk_alert(MODE6_LOW_BANK_ANGLE_3));
|
|
if (! test_bits(alerts, mk_alert(MODE6_HIGH_BANK_ANGLE_3)))
|
|
mk_unset_alerts(mk_alert(MODE6_HIGH_BANK_ANGLE_3));
|
|
|
|
if (alerts != 0)
|
|
mk_set_alerts(alerts);
|
|
else
|
|
mk_unset_alerts(mk_alert(MODE6_LOW_BANK_ANGLE_1)
|
|
| mk_alert(MODE6_HIGH_BANK_ANGLE_1)
|
|
| mk_alert(MODE6_LOW_BANK_ANGLE_2)
|
|
| mk_alert(MODE6_HIGH_BANK_ANGLE_2));
|
|
}
|
|
|
|
void
|
|
MK_VIII::Mode6Handler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK)
|
|
return;
|
|
|
|
if (! mk->state_handler.takeoff
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& mk_data(radio_altitude).get() > 1000)
|
|
{
|
|
reset_altitude_callouts(); // [SPEC] 6.4.2
|
|
reset_minimums(); // common sense
|
|
}
|
|
|
|
update_minimums();
|
|
update_altitude_callouts();
|
|
update_above_field_callout();
|
|
update_bank_angle();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// TCFHandler /////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Gets the difference between the azimuth from @from_lat,@from_lon to
|
|
// @to_lat,@to_lon, and @to_heading, in degrees.
|
|
double
|
|
MK_VIII::TCFHandler::get_azimuth_difference (double from_lat,
|
|
double from_lon,
|
|
double to_lat,
|
|
double to_lon,
|
|
double to_heading)
|
|
{
|
|
double az1, az2, distance;
|
|
geo_inverse_wgs_84(0, from_lat, from_lon, to_lat, to_lon, &az1, &az2, &distance);
|
|
return get_heading_difference(az1, to_heading);
|
|
}
|
|
|
|
// Gets the difference between the azimuth from the current GPS
|
|
// position to the center of @_runway, and the heading of @_runway, in
|
|
// degrees.
|
|
double
|
|
MK_VIII::TCFHandler::get_azimuth_difference (const FGRunway *_runway)
|
|
{
|
|
return get_azimuth_difference(mk_data(gps_latitude).get(),
|
|
mk_data(gps_longitude).get(),
|
|
_runway->latitude(),
|
|
_runway->longitude(),
|
|
_runway->headingDeg());
|
|
}
|
|
|
|
// Selects the most likely intended destination runway of @airport,
|
|
// and returns it in @_runway. For each runway, the difference between
|
|
// the azimuth from the current GPS position to the center of the
|
|
// runway and its heading is computed. The runway having the smallest
|
|
// difference wins.
|
|
//
|
|
// This selection algorithm is not specified in [SPEC], but
|
|
// http://www.egpws.com/general_information/description/runway_select.htm
|
|
// talks about automatic runway selection.
|
|
FGRunway*
|
|
MK_VIII::TCFHandler::select_runway (const FGAirport *airport)
|
|
{
|
|
FGRunway* _runway = 0;
|
|
double min_diff = 360;
|
|
|
|
for (unsigned int r=0; r<airport->numRunways(); ++r) {
|
|
FGRunway* rwy(airport->getRunwayByIndex(r));
|
|
double diff = get_azimuth_difference(rwy);
|
|
if (diff < min_diff)
|
|
{
|
|
min_diff = diff;
|
|
_runway = rwy;
|
|
}
|
|
} // of airport runways iteration
|
|
return _runway;
|
|
}
|
|
|
|
bool MK_VIII::TCFHandler::AirportFilter::passAirport(FGAirport* aApt) const
|
|
{
|
|
return aApt->hasHardRunwayOfLengthFt(mk->conf.runway_database);
|
|
}
|
|
|
|
void
|
|
MK_VIII::TCFHandler::update_runway ()
|
|
{
|
|
has_runway = false;
|
|
if (mk_data(gps_latitude).ncd || mk_data(gps_longitude).ncd) {
|
|
return;
|
|
}
|
|
|
|
// Search for the intended destination runway of the closest
|
|
// airport in range 15 nm. Passing 30nm to get_closest_airport()
|
|
// provides enough margin for
|
|
// large airports, which may have a runway located far away from
|
|
// the airport's reference point.
|
|
AirportFilter filter(mk);
|
|
FGAirport* apt = FGAirport::findClosest(
|
|
SGGeod::fromDeg(mk_data(gps_longitude).get(), mk_data(gps_latitude).get()),
|
|
30.0, &filter);
|
|
|
|
if (!apt) return;
|
|
|
|
FGRunway* _runway = select_runway(apt);
|
|
|
|
if (!_runway) return;
|
|
|
|
has_runway = true;
|
|
|
|
runway.center.latitude = _runway->latitude();
|
|
runway.center.longitude = _runway->longitude();
|
|
|
|
runway.elevation = apt->elevation();
|
|
|
|
double half_length_m = _runway->lengthM() * 0.5;
|
|
runway.half_length = half_length_m * SG_METER_TO_NM;
|
|
|
|
// b3 ________________ b0
|
|
// | |
|
|
// h1>>> | e1<<<<<<<<e0 | <<<h0
|
|
// |________________|
|
|
// b2 b1
|
|
|
|
// get heading to runway threshold (h0) and end (h1)
|
|
runway.edges[0].heading = _runway->headingDeg();
|
|
runway.edges[1].heading = get_reciprocal_heading(_runway->headingDeg());
|
|
|
|
double az;
|
|
|
|
// get position of runway threshold (e0)
|
|
geo_direct_wgs_84(0,
|
|
runway.center.latitude,
|
|
runway.center.longitude,
|
|
runway.edges[1].heading,
|
|
half_length_m,
|
|
&runway.edges[0].position.latitude,
|
|
&runway.edges[0].position.longitude,
|
|
&az);
|
|
|
|
// get position of runway end (e1)
|
|
geo_direct_wgs_84(0,
|
|
runway.center.latitude,
|
|
runway.center.longitude,
|
|
runway.edges[0].heading,
|
|
half_length_m,
|
|
&runway.edges[1].position.latitude,
|
|
&runway.edges[1].position.longitude,
|
|
&az);
|
|
|
|
double half_width_m = _runway->widthM() * 0.5;
|
|
|
|
// get position of threshold bias area edges (b0 and b1)
|
|
get_bias_area_edges(&runway.edges[0].position,
|
|
runway.edges[1].heading,
|
|
half_width_m,
|
|
&runway.bias_area[0],
|
|
&runway.bias_area[1]);
|
|
|
|
// get position of end bias area edges (b2 and b3)
|
|
get_bias_area_edges(&runway.edges[1].position,
|
|
runway.edges[0].heading,
|
|
half_width_m,
|
|
&runway.bias_area[2],
|
|
&runway.bias_area[3]);
|
|
}
|
|
|
|
void
|
|
MK_VIII::TCFHandler::get_bias_area_edges (Position *edge,
|
|
double reciprocal,
|
|
double half_width_m,
|
|
Position *bias_edge1,
|
|
Position *bias_edge2)
|
|
{
|
|
double half_bias_width_m = k * SG_NM_TO_METER + half_width_m;
|
|
double tmp_latitude = 0.0, tmp_longitude = 0.0, az = 0.0;
|
|
|
|
geo_direct_wgs_84(0,
|
|
edge->latitude,
|
|
edge->longitude,
|
|
reciprocal,
|
|
k * SG_NM_TO_METER,
|
|
&tmp_latitude,
|
|
&tmp_longitude,
|
|
&az);
|
|
geo_direct_wgs_84(0,
|
|
tmp_latitude,
|
|
tmp_longitude,
|
|
heading_substract(reciprocal, 90),
|
|
half_bias_width_m,
|
|
&bias_edge1->latitude,
|
|
&bias_edge1->longitude,
|
|
&az);
|
|
geo_direct_wgs_84(0,
|
|
tmp_latitude,
|
|
tmp_longitude,
|
|
heading_add(reciprocal, 90),
|
|
half_bias_width_m,
|
|
&bias_edge2->latitude,
|
|
&bias_edge2->longitude,
|
|
&az);
|
|
}
|
|
|
|
// Returns true if the current GPS position is inside the edge
|
|
// triangle of @edge. The edge triangle, which is specified and
|
|
// represented in [SPEC] 6.3.1, is defined as an isocele right
|
|
// triangle of infinite height, whose right angle is located at the
|
|
// position of @edge, and whose height is parallel to the heading of
|
|
// @edge.
|
|
bool
|
|
MK_VIII::TCFHandler::is_inside_edge_triangle (RunwayEdge *edge)
|
|
{
|
|
return get_azimuth_difference(mk_data(gps_latitude).get(),
|
|
mk_data(gps_longitude).get(),
|
|
edge->position.latitude,
|
|
edge->position.longitude,
|
|
edge->heading) <= 45;
|
|
}
|
|
|
|
// Returns true if the current GPS position is inside the bias area of
|
|
// the currently selected runway.
|
|
bool
|
|
MK_VIII::TCFHandler::is_inside_bias_area ()
|
|
{
|
|
double az1[4];
|
|
double angles_sum = 0;
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
double az2, distance;
|
|
geo_inverse_wgs_84(0,
|
|
mk_data(gps_latitude).get(),
|
|
mk_data(gps_longitude).get(),
|
|
runway.bias_area[i].latitude,
|
|
runway.bias_area[i].longitude,
|
|
&az1[i], &az2, &distance);
|
|
}
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
double angle = az1[i == 3 ? 0 : i + 1] - az1[i];
|
|
if (angle < -180)
|
|
angle += 360;
|
|
|
|
angles_sum += angle;
|
|
}
|
|
|
|
return angles_sum > 180;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::TCFHandler::is_tcf ()
|
|
{
|
|
if (mk_data(radio_altitude).get() > 10)
|
|
{
|
|
if (has_runway)
|
|
{
|
|
double distance, az1, az2;
|
|
|
|
geo_inverse_wgs_84(0,
|
|
mk_data(gps_latitude).get(),
|
|
mk_data(gps_longitude).get(),
|
|
runway.center.latitude,
|
|
runway.center.longitude,
|
|
&az1, &az2, &distance);
|
|
|
|
distance *= SG_METER_TO_NM;
|
|
|
|
// distance to the inner envelope edge
|
|
double edge_distance = distance - runway.half_length - k;
|
|
|
|
if (edge_distance >= 15)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 700)
|
|
return true;
|
|
}
|
|
else if (edge_distance >= 12)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 100 * edge_distance - 800)
|
|
return true;
|
|
}
|
|
else if (edge_distance >= 4)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 400)
|
|
return true;
|
|
}
|
|
else if (edge_distance >= 2.45)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 66.666667 * edge_distance + 133.33333)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (is_inside_edge_triangle(&runway.edges[0]) || is_inside_edge_triangle(&runway.edges[1]))
|
|
{
|
|
if (edge_distance >= 1)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 66.666667 * edge_distance + 133.33333)
|
|
return true;
|
|
}
|
|
else if (edge_distance >= 0.05)
|
|
{
|
|
if (mk_data(radio_altitude).get() < 200 * edge_distance)
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (! is_inside_bias_area())
|
|
{
|
|
if (mk_data(radio_altitude).get() < 245)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (mk_data(radio_altitude).get() < 700)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MK_VIII::TCFHandler::is_rfcf ()
|
|
{
|
|
if (has_runway)
|
|
{
|
|
double distance, az1, az2;
|
|
geo_inverse_wgs_84(0,
|
|
mk_data(gps_latitude).get(),
|
|
mk_data(gps_longitude).get(),
|
|
runway.center.latitude,
|
|
runway.center.longitude,
|
|
&az1, &az2, &distance);
|
|
|
|
double krf = k + mk_data(gps_vertical_figure_of_merit).get() / 200;
|
|
distance = distance * SG_METER_TO_NM - runway.half_length - krf;
|
|
|
|
if (distance <= 5)
|
|
{
|
|
double altitude_above_field = mk_data(geometric_altitude).get() - runway.elevation;
|
|
|
|
if (distance >= 1.5)
|
|
{
|
|
if (altitude_above_field < 300)
|
|
return true;
|
|
}
|
|
else if (distance >= 0)
|
|
{
|
|
if (altitude_above_field < 200 * distance)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MK_VIII::TCFHandler::update ()
|
|
{
|
|
if (mk->configuration_module.state != ConfigurationModule::STATE_OK || ! conf.enabled)
|
|
return;
|
|
|
|
// update_runway() has to iterate over the whole runway database
|
|
// (which contains thousands of runways), so it would be unwise to
|
|
// run it every frame. Run it every 3 seconds.
|
|
if (! runway_timer.running || runway_timer.elapsed() >= 3)
|
|
{
|
|
update_runway();
|
|
runway_timer.start();
|
|
}
|
|
|
|
if (! mk_dinput(ta_tcf_inhibit)
|
|
&& ! mk->state_handler.ground // [1]
|
|
&& ! mk_data(gps_latitude).ncd
|
|
&& ! mk_data(gps_longitude).ncd
|
|
&& ! mk_data(radio_altitude).ncd
|
|
&& ! mk_data(geometric_altitude).ncd
|
|
&& ! mk_data(gps_vertical_figure_of_merit).ncd)
|
|
{
|
|
double *_reference;
|
|
|
|
if (is_tcf())
|
|
_reference = mk_data(radio_altitude).get_pointer();
|
|
else if (is_rfcf())
|
|
_reference = mk_data(geometric_altitude).get_pointer();
|
|
else
|
|
_reference = NULL;
|
|
|
|
if (_reference)
|
|
{
|
|
if (mk_test_alert(TCF_TOO_LOW_TERRAIN))
|
|
{
|
|
double new_bias = bias;
|
|
// do not repeat terrain alerts below 30ft agl
|
|
if (mk_data(radio_altitude).get() > 30)
|
|
{
|
|
if (new_bias < 0.0) // sanity check
|
|
new_bias = 0.0;
|
|
while ((*reference < initial_value - initial_value * new_bias)&&
|
|
(new_bias < 1.0))
|
|
new_bias += 0.2;
|
|
}
|
|
|
|
if (new_bias > bias)
|
|
{
|
|
bias = new_bias;
|
|
mk_repeat_alert(mk_alert(TCF_TOO_LOW_TERRAIN));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bias = 0.2;
|
|
reference = _reference;
|
|
initial_value = *reference;
|
|
mk_set_alerts(mk_alert(TCF_TOO_LOW_TERRAIN));
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
mk_unset_alerts(mk_alert(TCF_TOO_LOW_TERRAIN));
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// MK_VIII ////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
MK_VIII::MK_VIII (SGPropertyNode *node)
|
|
: properties_handler(this),
|
|
name("mk-viii"),
|
|
num(0),
|
|
power_handler(this),
|
|
system_handler(this),
|
|
configuration_module(this),
|
|
fault_handler(this),
|
|
io_handler(this),
|
|
voice_player(this),
|
|
self_test_handler(this),
|
|
alert_handler(this),
|
|
state_handler(this),
|
|
mode1_handler(this),
|
|
mode2_handler(this),
|
|
mode3_handler(this),
|
|
mode4_handler(this),
|
|
mode5_handler(this),
|
|
mode6_handler(this),
|
|
tcf_handler(this)
|
|
{
|
|
for (int i = 0; i < node->nChildren(); ++i)
|
|
{
|
|
SGPropertyNode *child = node->getChild(i);
|
|
string cname = child->getName();
|
|
string cval = child->getStringValue();
|
|
|
|
if (cname == "name")
|
|
name = cval;
|
|
else if (cname == "number")
|
|
num = child->getIntValue();
|
|
else
|
|
{
|
|
SG_LOG(SG_INSTR, SG_WARN, "Error in mk-viii config logic");
|
|
if (name.length())
|
|
SG_LOG(SG_INSTR, SG_WARN, "Section = " << name);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MK_VIII::init ()
|
|
{
|
|
properties_handler.init();
|
|
voice_player.init();
|
|
}
|
|
|
|
void
|
|
MK_VIII::bind ()
|
|
{
|
|
SGPropertyNode *node = fgGetNode(("/instrumentation/" + name).c_str(), num, true);
|
|
|
|
configuration_module.bind(node);
|
|
power_handler.bind(node);
|
|
io_handler.bind(node);
|
|
voice_player.bind(node);
|
|
}
|
|
|
|
void
|
|
MK_VIII::unbind ()
|
|
{
|
|
properties_handler.unbind();
|
|
}
|
|
|
|
void
|
|
MK_VIII::update (double dt)
|
|
{
|
|
power_handler.update();
|
|
system_handler.update();
|
|
|
|
if (system_handler.state != SystemHandler::STATE_ON)
|
|
return;
|
|
|
|
io_handler.update_inputs();
|
|
io_handler.update_input_faults();
|
|
|
|
voice_player.update();
|
|
state_handler.update();
|
|
|
|
if (self_test_handler.update())
|
|
return;
|
|
|
|
io_handler.update_internal_latches();
|
|
io_handler.update_lamps();
|
|
|
|
mode1_handler.update();
|
|
mode2_handler.update();
|
|
mode3_handler.update();
|
|
mode4_handler.update();
|
|
mode5_handler.update();
|
|
mode6_handler.update();
|
|
tcf_handler.update();
|
|
|
|
alert_handler.update();
|
|
io_handler.update_outputs();
|
|
}
|