From acd09b99cb4e1dac877d55f51a372f7f3283c439 Mon Sep 17 00:00:00 2001 From: fredb Date: Sat, 4 Mar 2006 20:21:32 +0000 Subject: [PATCH] Jean-Yves Lefort : I have implemented a Honeywell MK VIII EGPWS emulation for FlightGear. The MK VIII is an Enhanced Ground Proximity Warning System aimed at regional turboprop and small turbofan aircrafts such as the Citation, Citation Bravo, B1900D, Beechcraft 99 and L410. Frederic Bouvier: make the MSVC compilation possible. Rearrange base package directories. --- src/Instrumentation/Makefile.am | 2 +- src/Instrumentation/instrument_mgr.cxx | 4 + src/Instrumentation/mk_viii.cxx | 5002 ++++++++++++++++++++++++ src/Instrumentation/mk_viii.hxx | 1608 ++++++++ src/Main/fg_commands.cxx | 45 + 5 files changed, 6660 insertions(+), 1 deletion(-) create mode 100755 src/Instrumentation/mk_viii.cxx create mode 100755 src/Instrumentation/mk_viii.hxx diff --git a/src/Instrumentation/Makefile.am b/src/Instrumentation/Makefile.am index 4c76b634b..1a1892023 100644 --- a/src/Instrumentation/Makefile.am +++ b/src/Instrumentation/Makefile.am @@ -26,7 +26,7 @@ libInstrumentation_a_SOURCES = \ vertical_speed_indicator.cxx vertical_speed_indicator.hxx \ inst_vertical_speed_indicator.cxx inst_vertical_speed_indicator.hxx \ od_gauge.hxx od_gauge.cxx wxradar.hxx wxradar.cxx \ - tacan.cxx tacan.hxx \ + tacan.cxx tacan.hxx mk_viii.cxx mk_viii.hxx \ dclgps.cxx dclgps.hxx \ render_area_2d.cxx render_area_2d.hxx diff --git a/src/Instrumentation/instrument_mgr.cxx b/src/Instrumentation/instrument_mgr.cxx index 01de836b3..bf0bce8ed 100644 --- a/src/Instrumentation/instrument_mgr.cxx +++ b/src/Instrumentation/instrument_mgr.cxx @@ -43,6 +43,7 @@ #include "od_gauge.hxx" #include "wxradar.hxx" #include "tacan.hxx" +#include "mk_viii.hxx" FGInstrumentMgr::FGInstrumentMgr () @@ -165,6 +166,9 @@ bool FGInstrumentMgr::build () } else if ( name == "tacan" ) { set_subsystem( "instrument" + temp.str(), new TACAN( node ) ); + } else if ( name == "mk-viii" ) { + set_subsystem( "instrument" + temp.str(), + new MK_VIII( node ) ); } else { SG_LOG( SG_ALL, SG_ALERT, "Unknown top level section: " << name ); diff --git a/src/Instrumentation/mk_viii.cxx b/src/Instrumentation/mk_viii.cxx new file mode 100755 index 000000000..da8cdb744 --- /dev/null +++ b/src/Instrumentation/mk_viii.cxx @@ -0,0 +1,5002 @@ +// 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 +#endif + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +SG_USING_STD(string); + +#include "Airports/runways.hxx" +#include "Airports/simple.hxx" +#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); +} + +// Searches for the closest airport whose Manhattan distance to +// @lat,@lon is inferior to @min_manhattan_distance (expressed in +// degrees) and for which @test_airport returns true. Returns NULL if +// no airport was found. +template +static const FGAirport * +get_closest_airport (double lat, + double lon, + double min_manhattan_distance, + C &obj, + bool (C::*test_airport) (const FGAirport *)) +{ + const FGAirport *airport = NULL; + const airport_list *airport_list = globals->get_airports()->getAirportList(); + + for (size_t i = 0; i < airport_list->size(); i++) + { + const FGAirport *a = (*airport_list)[i]; + double dist = fabs(lat - a->getLatitude()) + fabs(lon - a->getLongitude()); + if (dist < min_manhattan_distance && (obj.*test_airport)(a)) + { + airport = a; + min_manhattan_distance = dist; + } + } + + return airport; +} + +/////////////////////////////////////////////////////////////////////////////// +// 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(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::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(&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 *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 *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 (int 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 } } + }; + + int 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 (int 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) +{ + // unimplemented + 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) +{ + // unimplemented + 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 (int 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 (int 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(&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. + + deque< Sample >::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(agl)); + + // calculate average + double new_value = 0; + if (samples.size() > 0) + { + 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; +} + +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)) + { + // Some flight models may return negative values when on the + // ground or after a crash; do not allow them. + double agl = mk_node(altitude_agl)->getDoubleValue(); + 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 (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(); + } + 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 >(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. + + deque< Sample< Parameter > >::iterator erase_last = altitude_samples.begin(); + deque< Sample< Parameter > >::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 (int 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 (int 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 (int 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 (int 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(*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(feed)); +} + +void +MK_VIII::IOHandler::tie_input (SGPropertyNode *node, + const char *name, + Parameter *input, + bool *feed) +{ + mk->properties_handler.tie(node, (string("inputs/arinc429/") + name).c_str(), SGRawValuePointer(input->get_pointer())); + mk->properties_handler.tie(node, (string("inputs/arinc429/") + name + "-ncd").c_str(), SGRawValuePointer(&input->ncd)); + if (feed) + mk->properties_handler.tie(node, (string("input-feeders/arinc429/") + name).c_str(), SGRawValuePointer(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(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(output)); + child->setAttribute(SGPropertyNode::WRITE, false); +} + +void +MK_VIII::IOHandler::bind (SGPropertyNode *node) +{ + mk->properties_handler.tie(node, "inputs/rs-232/present-status", SGRawValueMethods(*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 (int 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 (int 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); + tie(node, "position/x", &position[0]); + tie(node, "position/y", &position[1]); + tie(node, "position/z", &position[2]); + tie(node, "orientation/x", &orientation[0]); + tie(node, "orientation/y", &orientation[1]); + tie(node, "orientation/z", &orientation[2]); + tie(node, "orientation/inner-cone", &inner_cone); + tie(node, "orientation/outer-cone", &outer_cone); + tie(node, "reference-dist", &reference_dist); + tie(node, "max-dist", &max_dist); +} + +void +MK_VIII::VoicePlayer::Speaker::update_configuration () +{ + map::iterator iter; + for (iter = player->samples.begin(); iter != player->samples.end(); iter++) + { + SGSoundSample *sample = (*iter).second; + + sample->set_pitch(pitch); + sample->set_offset_pos(position); + sample->set_orientation(orientation, + inner_cone, + outer_cone, + outer_gain); + sample->set_reference_dist(reference_dist); + sample->set_max_dist(max_dist); + } + + 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 (double _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::iterator iter1; + for (iter1 = _voices.begin(); iter1 != _voices.end(); iter1++) + delete *iter1; + _voices.clear(); + +/* sound mgr already destroyed - samples already deleted + map::iterator iter2; + for (iter2 = samples.begin(); iter2 != samples.end(); iter2++) + { + bool status = globals->get_soundmgr()->remove((*iter2).first); + assert(status); + } +*/ + samples.clear(); +} + +void +MK_VIII::VoicePlayer::init () +{ +#define STDPAUSE 0.75 // [SPEC] 6.4.4: "the standard 0.75 second delay" + + 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 (int 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 = globals->get_soundmgr()->find(refname.str()); + if (! sample) + { + SGPath sample_path(globals->get_fg_root()); + sample_path.append("Sounds/mk-viii"); + + string filename = string(name) + ".wav"; + try + { + sample = new SGSoundSample(sample_path.c_str(), filename.c_str()); + } + catch (const sg_exception &e) + { + SG_LOG(SG_INSTR, SG_ALERT, "Error loading MK VIII sound sample \"" + filename + "\": " + e.getFormattedMessage()); + exit(1); + } + + globals->get_soundmgr()->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 (double _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 (int 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; + } +} + +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); + } + } + 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 = alerts; + repeated_alerts = 0; + altitude_callout_voice = NULL; +} + +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; +} + +/////////////////////////////////////////////////////////////////////////////// +// 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) +{ + if (mk_data(radio_altitude).get() > 0) + while (alt_loss > max_alt_loss(initial_bias)) + 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) +{ + while (mk_data(radio_altitude).get() < min_agl - min_agl * initial_bias) + 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)); +} + +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)); +} + +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)); +} + +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) +{ + 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) +{ + while (is_soft(initial_bias)) + 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 (int i = 0; i < n_altitude_callouts; i++) + altitude_callouts_issued[i] = false; +} + +bool +MK_VIII::Mode6Handler::is_playing_altitude_callout () +{ + for (int 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 (int 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 (double volume) +{ + mk_voice(minimums_minimums)->set_volume(volume); + mk_voice(five_hundred_above)->set_volume(volume); + for (int 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 (int 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 (int 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 (int 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->_length < mk->conf.runway_database) + return false; + + // get position of threshold + double latitude, longitude, az; + geo_direct_wgs_84(0, + _runway->_lat, + _runway->_lon, + get_reciprocal_heading(_runway->_heading), + _runway->_length / 2 * SG_FEET_TO_METER, + &latitude, + &longitude, + &az); + + // get distance to threshold + double distance, az1, az2; + geo_inverse_wgs_84(0, + mk_data(gps_latitude).get(), + mk_data(gps_longitude).get(), + latitude, + longitude, + &az1, &az2, &distance); + + return distance * SG_METER_TO_NM <= 5; +} + +bool +MK_VIII::Mode6Handler::test_airport (const FGAirport *airport) +{ + FGRunway r; + if (globals->get_runways()->search(airport->getId(), &r)) + do + { + if (test_runway(&r)) + return true; + + // reciprocal runway + r._heading = get_reciprocal_heading(r._heading); + if (test_runway(&r)) + return true; + } + while (globals->get_runways()->next(&r) && r._id == airport->getId()); + + return false; +} + +void +MK_VIII::Mode6Handler::update_runway () +{ + if (! mk_data(gps_latitude).ncd && ! mk_data(gps_longitude).ncd) + { + // Search for the closest runway threshold in range 5 + // nm. Passing 0.5 degrees (approximatively 30 nm) to + // get_closest_airport() provides enough margin for large + // airports, which may have a runway located far away from the + // airport's reference point. + + const FGAirport *airport = get_closest_airport(mk_data(gps_latitude).get(), + mk_data(gps_longitude).get(), + 0.5, + *this, + &MK_VIII::Mode6Handler::test_airport); + + if (airport) + runway.elevation = airport->getElevation(); + + has_runway.set(airport != NULL); + } + else + has_runway.unset(); +} + +void +MK_VIII::Mode6Handler::get_altitude_above_field (Parameter *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 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->_lat, + _runway->_lon, + _runway->_heading); +} + +// 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. +void +MK_VIII::TCFHandler::select_runway (const FGAirport *airport, + FGRunway *_runway) +{ + FGRunway r; + bool status = globals->get_runways()->search(airport->getId(), &r); + assert(status); + + double min_diff = 360; + do + { + double diff; + + diff = get_azimuth_difference(&r); + if (diff < min_diff) + { + min_diff = diff; + *_runway = r; + } + + // reciprocal runway + r._heading = get_reciprocal_heading(r._heading); + diff = get_azimuth_difference(&r); + if (diff < min_diff) + { + min_diff = diff; + *_runway = r; + } + } + while (globals->get_runways()->next(&r) && r._id == airport->getId()); +} + +bool +MK_VIII::TCFHandler::test_airport (const FGAirport *airport) +{ + FGRunway r; + if (globals->get_runways()->search(airport->getId(), &r)) + do + { + if (r._length >= mk->conf.runway_database) + return true; + } + while (globals->get_runways()->next(&r) && r._id == airport->getId()); + + return false; +} + +void +MK_VIII::TCFHandler::update_runway () +{ + has_runway = false; + if (! mk_data(gps_latitude).ncd && ! mk_data(gps_longitude).ncd) + { + // Search for the intended destination runway of the closest + // airport in range 15 nm. Passing 0.5 degrees (approximatively + // 30 nm) to get_closest_airport() provides enough margin for + // large airports, which may have a runway located far away from + // the airport's reference point. + + const FGAirport *airport = get_closest_airport(mk_data(gps_latitude).get(), + mk_data(gps_longitude).get(), + 0.5, + *this, + &MK_VIII::TCFHandler::test_airport); + + if (airport) + { + has_runway = true; + + FGRunway _runway; + select_runway(airport, &_runway); + + runway.center.latitude = _runway._lat; + runway.center.longitude = _runway._lon; + + runway.elevation = airport->getElevation(); + + double half_length_m = _runway._length / 2 * SG_FEET_TO_METER; + runway.half_length = half_length_m * SG_METER_TO_NM; + + // b3 ________________ b0 + // | | + // h1>>> | e1<<<<<<<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; + while (*reference < initial_value - initial_value * new_bias) + 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) + : name("mk-viii"), + num(0), + + properties_handler(this), + 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(); +} diff --git a/src/Instrumentation/mk_viii.hxx b/src/Instrumentation/mk_viii.hxx new file mode 100755 index 000000000..4969a137d --- /dev/null +++ b/src/Instrumentation/mk_viii.hxx @@ -0,0 +1,1608 @@ +// 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. + + +#ifndef __INSTRUMENTS_MK_VIII_HXX +#define __INSTRUMENTS_MK_VIII_HXX + +#include + +#include +#include +#include + +#include +#include +#include + +SG_USING_STD(vector); +SG_USING_STD(deque); +SG_USING_STD(map); + +#include "Airports/runways.hxx" +#include "Airports/simple.hxx" +#include "Main/globals.hxx" + +/////////////////////////////////////////////////////////////////////////////// +// MK_VIII //////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +class MK_VIII : public SGSubsystem +{ + // keep in sync with Mode6Handler::altitude_callout_definitions[] + static const int n_altitude_callouts = 11; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::RawValueMethodsData ///////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + template + class RawValueMethodsData : public SGRawValue + { + public: + typedef VT (C::*getter_t) (DT) const; + typedef void (C::*setter_t) (DT, VT); + + RawValueMethodsData (C &obj, DT data, getter_t getter = 0, setter_t setter = 0) + : _obj(obj), _data(data), _getter(getter), _setter(setter) {} + + virtual VT getValue () const + { + if (_getter) + return (_obj.*_getter)(_data); + else + return SGRawValue::DefaultValue; + } + virtual bool setValue (VT value) + { + if (_setter) + { + (_obj.*_setter)(_data, value); + return true; + } + else + return false; + } + virtual SGRawValue *clone () const + { + return new RawValueMethodsData(_obj, _data, _getter, _setter); + } + + private: + C &_obj; + DT _data; + getter_t _getter; + setter_t _setter; + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Parameter /////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + template + class Parameter + { + T _value; + + public: + bool ncd; + + inline Parameter () + : _value(0), ncd(true) {} + + inline T get () const { assert(! ncd); return _value; } + inline T *get_pointer () { return &_value; } + inline void set (T value) { ncd = false; _value = value; } + inline void unset () { ncd = true; } + + inline void set (const Parameter *parameter) + { + if (parameter->ncd) + unset(); + else + set(parameter->get()); + } + + inline void set (const Parameter *parameter, double factor) + { + if (parameter->ncd) + unset(); + else + set(parameter->get() * factor); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Sample ////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + template + class Sample + { + public: + double timestamp; + T value; + + inline Sample (T _value) + : timestamp(globals->get_sim_time_sec()), value(_value) {} + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Timer /////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Timer + { + double start_time; + + public: + bool running; + + inline Timer () + : running(false) {} + + inline void start () { running = true; start_time = globals->get_sim_time_sec(); } + inline void stop () { running = false; } + inline double elapsed () const { assert(running); return globals->get_sim_time_sec() - start_time; } + inline double start_or_elapsed () + { + if (running) + return elapsed(); + else + { + start(); + return 0; + } + } + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::PropertiesHandler /////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class PropertiesHandler + { + MK_VIII *mk; + + vector tied_properties; + + public: + struct + { + SGPropertyNode *ai_caged; + SGPropertyNode *ai_roll; + SGPropertyNode *ai_serviceable; + SGPropertyNode *altimeter_altitude; + SGPropertyNode *altimeter_serviceable; + SGPropertyNode *altitude; + SGPropertyNode *altitude_agl; + SGPropertyNode *asi_serviceable; + SGPropertyNode *asi_speed; + SGPropertyNode *autopilot_heading_lock; + SGPropertyNode *flaps; + SGPropertyNode *gear_down; + SGPropertyNode *latitude; + SGPropertyNode *longitude; + SGPropertyNode *nav0_cdi_serviceable; + SGPropertyNode *nav0_gs_distance; + SGPropertyNode *nav0_gs_needle_deflection; + SGPropertyNode *nav0_gs_serviceable; + SGPropertyNode *nav0_has_gs; + SGPropertyNode *nav0_heading_needle_deflection; + SGPropertyNode *nav0_in_range; + SGPropertyNode *nav0_nav_loc; + SGPropertyNode *nav0_serviceable; + SGPropertyNode *power; + SGPropertyNode *replay_state; + SGPropertyNode *vs; + } external_properties; + + inline PropertiesHandler (MK_VIII *device) + : mk(device) {} + + template + inline void tie (SGPropertyNode *node, const SGRawValue &raw_value) + { + node->tie(raw_value); + tied_properties.push_back(node); + } + + template + inline void tie (SGPropertyNode *node, + const char *relative_path, + const SGRawValue &raw_value) + { + tie(node->getNode(relative_path, true), raw_value); + } + + void init (); + void unbind (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::PowerHandler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class PowerHandler + { + MK_VIII *mk; + + bool serviceable; + bool powered; + + Timer power_loss_timer; + Timer abnormal_timer; + Timer low_surge_timer; + Timer high_surge_timer; + Timer very_high_surge_timer; + + bool handle_abnormal_voltage (bool abnormal, + Timer *timer, + double max_duration); + + void power_on (); + void power_off (); + + public: + inline PowerHandler (MK_VIII *device) + : mk(device), serviceable(false), powered(false) {} + + void bind (SGPropertyNode *node); + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::SystemHandler /////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class SystemHandler + { + MK_VIII *mk; + + double boot_delay; + Timer boot_timer; + + int last_replay_state; + Timer reposition_timer; + + public: + typedef enum + { + STATE_OFF, + STATE_BOOTING, + STATE_ON, + STATE_REPOSITION + } State; + + State state; + + inline SystemHandler (MK_VIII *device) + : mk(device), state(STATE_OFF) {} + + void power_on (); + void power_off (); + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::ConfigurationModule ///////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class ConfigurationModule + { + public: + // keep in sync with IOHandler::present_status() + typedef enum + { + CATEGORY_AIRCRAFT_MODE_TYPE_SELECT, + CATEGORY_AIR_DATA_INPUT_SELECT, + CATEGORY_POSITION_INPUT_SELECT, + CATEGORY_ALTITUDE_CALLOUTS, + CATEGORY_AUDIO_MENU_SELECT, + CATEGORY_TERRAIN_DISPLAY_SELECT, + CATEGORY_OPTIONS_SELECT_GROUP_1, + CATEGORY_RADIO_ALTITUDE_INPUT_SELECT, + CATEGORY_NAVIGATION_INPUT_SELECT, + CATEGORY_ATTITUDE_INPUT_SELECT, + CATEGORY_HEADING_INPUT_SELECT, + CATEGORY_WINDSHEAR_INPUT_SELECT, + CATEGORY_INPUT_OUTPUT_DISCRETE_TYPE_SELECT, + CATEGORY_AUDIO_OUTPUT_LEVEL, + CATEGORY_UNDEFINED_INPUT_SELECT_1, + CATEGORY_UNDEFINED_INPUT_SELECT_2, + CATEGORY_UNDEFINED_INPUT_SELECT_3, + N_CATEGORIES + } Category; + + typedef enum + { + STATE_OK, + STATE_INVALID_DATABASE, + STATE_INVALID_AIRCRAFT_TYPE + } State; + State state; + + int effective_categories[N_CATEGORIES]; + + ConfigurationModule (MK_VIII *device); + + void boot (); + void bind (SGPropertyNode *node); + + private: + MK_VIII *mk; + + int categories[N_CATEGORIES]; + + bool read_aircraft_mode_type_select (int value); + bool read_air_data_input_select (int value); + bool read_position_input_select (int value); + bool read_altitude_callouts (int value); + bool read_audio_menu_select (int value); + bool read_terrain_display_select (int value); + bool read_options_select_group_1 (int value); + bool read_radio_altitude_input_select (int value); + bool read_navigation_input_select (int value); + bool read_attitude_input_select (int value); + bool read_heading_input_select (int value); + bool read_windshear_input_select (int value); + bool read_input_output_discrete_type_select (int value); + bool read_audio_output_level (int value); + bool read_undefined_input_select (int value); + + static bool m6_t2_is_bank_angle (Parameter *agl, + double abs_roll_deg, + bool ap_engaged); + static bool m6_t4_is_bank_angle (Parameter *agl, + double abs_roll_deg, + bool ap_engaged); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::FaultHandler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class FaultHandler + { + enum + { + INOP_GPWS = 1 << 0, + INOP_TAD = 1 << 1 + }; + + MK_VIII *mk; + + static const unsigned int fault_inops[]; + + bool has_faults (unsigned int inop); + + public: + // keep in sync with IOHandler::present_status() + typedef enum + { + FAULT_ALL_MODES_INHIBIT, + FAULT_GEAR_SWITCH, + FAULT_FLAPS_SWITCH, + FAULT_MOMENTARY_FLAP_OVERRIDE_INVALID, + FAULT_SELF_TEST_INVALID, + FAULT_GLIDESLOPE_CANCEL_INVALID, + FAULT_STEEP_APPROACH_INVALID, + FAULT_GPWS_INHIBIT, + FAULT_TA_TCF_INHIBIT, + FAULT_MODES14_INPUTS_INVALID, + FAULT_MODE5_INPUTS_INVALID, + FAULT_MODE6_INPUTS_INVALID, + FAULT_BANK_ANGLE_INPUTS_INVALID, + FAULT_TCF_INPUTS_INVALID, + N_FAULTS + } Fault; + + bool faults[N_FAULTS]; + + inline FaultHandler (MK_VIII *device) + : mk(device) {} + + void boot (); + + void set_fault (Fault fault); + void unset_fault (Fault fault); + + bool has_faults () const; + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::IOHandler /////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class IOHandler + { + public: + enum Lamp + { + LAMP_NONE, + LAMP_GLIDESLOPE, + LAMP_CAUTION, + LAMP_WARNING + }; + + struct LampConfiguration + { + bool format2; + bool flashing; + }; + + struct FaultsConfiguration + { + double max_flaps_down_airspeed; + double max_gear_down_airspeed; + }; + + struct _s_Conf + { + const LampConfiguration *lamp; + const FaultsConfiguration *faults; + bool flap_reversal; + bool steep_approach_enabled; + bool gpws_inhibit_enabled; + bool momentary_flap_override_enabled; + bool alternate_steep_approach; + bool use_internal_gps; + bool localizer_enabled; + } conf; + + struct _s_input_feeders + { + struct _s_discretes + { + bool landing_gear; + bool landing_flaps; + bool glideslope_inhibit; + bool decision_height; + bool autopilot_engaged; + } discretes; + + struct _s_arinc429 + { + bool uncorrected_barometric_altitude; + bool barometric_altitude_rate; + bool radio_altitude; + bool glideslope_deviation; + bool roll_angle; + bool localizer_deviation; + bool computed_airspeed; + bool decision_height; + } arinc429; + } input_feeders; + + struct _s_inputs + { + struct _s_discretes + { + bool landing_gear; // appendix E 6.6.2, 3.15.1.4 + bool landing_flaps; // appendix E 6.6.4, 3.15.1.2 + bool momentary_flap_override; // appendix E 6.6.6, 3.15.1.6 + bool self_test; // appendix E 6.6.7, 3.15.1.10 + bool glideslope_inhibit; // appendix E 6.6.11, 3.15.1.1 + bool glideslope_cancel; // appendix E 6.6.13, 3.15.1.5 + bool decision_height; // appendix E 6.6.14, 3.10.2 + bool mode6_low_volume; // appendix E 6.6.15, 3.15.1.7 + bool audio_inhibit; // appendix E 6.6.16, 3.15.1.3 + bool ta_tcf_inhibit; // appendix E 6.6.20, 3.15.1.9 + bool autopilot_engaged; // appendix E 6.6.21, 3.15.1.8 + bool steep_approach; // appendix E 6.6.25, 3.15.1.11 + bool gpws_inhibit; // appendix E 6.6.27, 3.15.1.12 + } discretes; + + struct _s_arinc429 + { + Parameter uncorrected_barometric_altitude; // appendix E 6.2.1 + Parameter barometric_altitude_rate; // appendix E 6.2.2 + Parameter gps_altitude; // appendix E 6.2.4 + Parameter gps_latitude; // appendix E 6.2.7 + Parameter gps_longitude; // appendix E 6.2.8 + Parameter gps_vertical_figure_of_merit; // appendix E 6.2.13 + Parameter radio_altitude; // appendix E 6.2.29 + Parameter glideslope_deviation; // appendix E 6.2.30 + Parameter roll_angle; // appendix E 6.2.31 + Parameter localizer_deviation; // appendix E 6.2.33 + Parameter computed_airspeed; // appendix E 6.2.39 + Parameter decision_height; // appendix E 6.2.41 + } arinc429; + } inputs; + + struct Outputs + { + struct _s_discretes + { + bool gpws_warning; // appendix E 7.4.1, 3.15.2.5 + bool gpws_alert; // appendix E 7.4.1, 3.15.2.6 + bool audio_on; // appendix E 7.4.2, 3.15.2.10 + bool gpws_inop; // appendix E 7.4.3, 3.15.2.3 + bool tad_inop; // appendix E 7.4.3, 3.15.2.4 + bool flap_override; // appendix E 7.4.5, 3.15.2.8 + bool glideslope_cancel; // appendix E 7.4.6, 3.15.2.7 + bool steep_approach; // appendix E 7.4.12, 3.15.2.9 + } discretes; + + struct _s_arinc429 + { + int egpws_alert_discrete_1; // appendix E 7.1.1.1 + int egpwc_logic_discretes; // appendix E 7.1.1.2 + int mode6_callouts_discrete_1; // appendix E 7.1.1.3 + int mode6_callouts_discrete_2; // appendix E 7.1.1.4 + int egpws_alert_discrete_2; // appendix E 7.1.1.5 + int egpwc_alert_discrete_3; // appendix E 7.1.1.6 + } arinc429; + }; + + Outputs outputs; + + struct _s_data + { + Parameter barometric_altitude_rate; + Parameter decision_height; + Parameter geometric_altitude; + Parameter glideslope_deviation_dots; + Parameter gps_altitude; + Parameter gps_latitude; + Parameter gps_longitude; + Parameter gps_vertical_figure_of_merit; + Parameter localizer_deviation_dots; + Parameter radio_altitude; + Parameter roll_angle; + Parameter terrain_clearance; + } data; + + IOHandler (MK_VIII *device); + + void boot (); + void post_boot (); + void power_off (); + + void enter_ground (); + void enter_takeoff (); + + void update_inputs (); + void update_input_faults (); + void update_alternate_discrete_input (bool *ptr); + void update_internal_latches (); + + void update_egpws_alert_discrete_1 (); + void update_egpwc_logic_discretes (); + void update_mode6_callouts_discrete_1 (); + void update_mode6_callouts_discrete_2 (); + void update_egpws_alert_discrete_2 (); + void update_egpwc_alert_discrete_3 (); + void update_outputs (); + + void update_lamps (); + void set_lamp (Lamp lamp); + + bool gpws_inhibit () const; + bool real_flaps_down () const; + bool flaps_down () const; + bool flap_override () const; + bool steep_approach () const; + bool momentary_steep_approach_enabled () const; + + void bind (SGPropertyNode *node); + + private: + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::IOHandler::TerrainClearanceFilter ///////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + class TerrainClearanceFilter + { + deque< Sample > samples; + double value; + + public: + inline TerrainClearanceFilter () + : value(0) {} + + double update (double agl); + void reset (); + }; + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::IOHandler (continued) ///////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + MK_VIII *mk; + + TerrainClearanceFilter terrain_clearance_filter; + + Lamp _lamp; + Timer lamp_timer; + + Timer audio_inhibit_fault_timer; + Timer landing_gear_fault_timer; + Timer flaps_down_fault_timer; + Timer momentary_flap_override_fault_timer; + Timer self_test_fault_timer; + Timer glideslope_cancel_fault_timer; + Timer steep_approach_fault_timer; + Timer gpws_inhibit_fault_timer; + Timer ta_tcf_inhibit_fault_timer; + + bool last_landing_gear; + bool last_real_flaps_down; + + deque< Sample< Parameter > > altitude_samples; + + struct + { + bool glideslope_cancel; + } power_saved; + + void update_terrain_clearance (); + void reset_terrain_clearance (); + + void handle_input_fault (bool test, FaultHandler::Fault fault); + void handle_input_fault (bool test, + Timer *timer, + double max_duration, + FaultHandler::Fault fault); + + void tie_input (SGPropertyNode *node, + const char *name, + bool *input, + bool *feed = NULL); + void tie_input (SGPropertyNode *node, + const char *name, + Parameter *input, + bool *feed = NULL); + void tie_output (SGPropertyNode *node, + const char *name, + bool *output); + void tie_output (SGPropertyNode *node, + const char *name, + int *output); + + bool get_discrete_input (bool *ptr) const; + void set_discrete_input (bool *ptr, bool value); + + void present_status (); + void present_status_section (const char *name); + void present_status_item (const char *name, const char *value = NULL); + void present_status_subitem (const char *name); + + bool get_present_status () const; + void set_present_status (bool value); + + bool *get_lamp_output (Lamp lamp); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer ///////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class VoicePlayer + { + public: + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer::Voice //////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + class Voice + { + public: + + ///////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer::Voice::Element //////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////// + + class Element + { + public: + bool silence; + + virtual inline void play (double volume) {} + virtual inline void stop () {} + virtual bool is_playing () = 0; + virtual inline void set_volume (double volume) {} + }; + + ///////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer::Voice::SampleElement /////////////////////////// + ///////////////////////////////////////////////////////////////////////// + + class SampleElement : public Element + { + SGSoundSample *_sample; + double _volume; + + public: + inline SampleElement (SGSoundSample *sample, double volume = 1.0) + : _sample(sample), _volume(volume) { silence = false; } + + virtual inline void play (double volume) { set_volume(volume); _sample->play_once(); } + virtual inline void stop () { _sample->stop(); } + virtual inline bool is_playing () { return _sample->is_playing(); } + virtual inline void set_volume (double volume) { _sample->set_volume(volume * _volume); } + }; + + ///////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer::Voice::SilenceElement ////////////////////////// + ///////////////////////////////////////////////////////////////////////// + + class SilenceElement : public Element + { + double _duration; + double start_time; + + public: + inline SilenceElement (double duration) + : _duration(duration) { silence = true; } + + virtual inline void play (double volume) { start_time = globals->get_sim_time_sec(); } + virtual inline bool is_playing () { return globals->get_sim_time_sec() - start_time < _duration; } + }; + + ///////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer::Voice (continued) ////////////////////////////// + ///////////////////////////////////////////////////////////////////////// + + Element *element; + + inline Voice (VoicePlayer *_player) + : player(_player), volume(1.0), element(NULL) {} + + ~Voice (); + + inline void append (Element *_element) { elements.push_back(_element); } + + void play (); + void stop (bool now); + void set_volume (double _volume); + void volume_changed (); + void update (); + + private: + VoicePlayer *player; + + double volume; + + vector elements; + vector::iterator iter; + + inline double get_volume () const { return player->volume * player->speaker.volume * volume; } + }; + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer (continued) /////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + struct + { + double volume; + } conf; + + double volume; + + Voice *voice; + Voice *next_voice; + + struct + { + Voice *application_data_base_failed; + Voice *bank_angle; + Voice *bank_angle_bank_angle; + Voice *bank_angle_bank_angle_3; + Voice *bank_angle_inop; + Voice *bank_angle_pause_bank_angle; + Voice *bank_angle_pause_bank_angle_3; + Voice *callouts_inop; + Voice *configuration_type_invalid; + Voice *dont_sink; + Voice *dont_sink_pause_dont_sink; + Voice *five_hundred_above; + Voice *glideslope; + Voice *glideslope_inop; + Voice *gpws_inop; + Voice *hard_glideslope; + Voice *minimums; + Voice *minimums_minimums; + Voice *pull_up; + Voice *sink_rate; + Voice *sink_rate_pause_sink_rate; + Voice *soft_glideslope; + Voice *terrain; + Voice *terrain_pause_terrain; + Voice *too_low_flaps; + Voice *too_low_gear; + Voice *too_low_terrain; + Voice *altitude_callouts[n_altitude_callouts]; + } voices; + + inline VoicePlayer (MK_VIII *device) + : mk(device), speaker(this), voice(NULL), next_voice(NULL) {} + + ~VoicePlayer (); + + void init (); + + enum + { + PLAY_NOW = 1 << 0, + PLAY_LOOPED = 1 << 1 + }; + void play (Voice *_voice, unsigned int flags = 0); + + enum + { + STOP_NOW = 1 << 0 + }; + void stop (unsigned int flags = 0); + + void set_volume (double _volume); + void update (); + + inline void bind (SGPropertyNode *node) { speaker.bind(node); } + + private: + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer::Speaker ////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + class Speaker + { + VoicePlayer *player; + + double pitch; + float position[3]; + float orientation[3]; + float inner_cone; + float outer_cone; + float outer_gain; + float reference_dist; + float max_dist; + + template + inline void tie (SGPropertyNode *node, const char *name, T *ptr) + { + player->mk->properties_handler.tie + (node, (string("speaker/") + name).c_str(), + RawValueMethodsData + (*this, ptr, + &MK_VIII::VoicePlayer::Speaker::get_property, + &MK_VIII::VoicePlayer::Speaker::set_property)); + } + + template + inline void set_property (T *ptr, T value) { *ptr = value; update_configuration(); } + + template + inline T get_property (T *ptr) const { return *ptr; } + + public: + double volume; + + inline Speaker (VoicePlayer *_player) + : player(_player), + volume(1), + pitch(1), + inner_cone(360), + outer_cone(360), + outer_gain(0), + reference_dist(3), + max_dist(10) + { + position[0] = 0; position[1] = 0; position[2] = 0; + orientation[0] = 0; orientation[1] = 0; orientation[2] = 0; + } + + void bind (SGPropertyNode *node); + void update_configuration (); + }; + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::VoicePlayer (continued) /////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + MK_VIII *mk; + + Speaker speaker; + + map samples; + vector _voices; + + bool looped; + bool next_looped; + + SGSoundSample *get_sample (const char *name); + + inline void append (Voice *voice, Voice::Element *element) { voice->append(element); } + inline void append (Voice *voice, const char *sample_name) { voice->append(new Voice::SampleElement(get_sample(sample_name))); } + inline void append (Voice *voice, double silence) { voice->append(new Voice::SilenceElement(silence)); } + + inline void make_voice (Voice **voice) { *voice = new Voice(this); _voices.push_back(*voice); } + + template + inline void make_voice (Voice **voice, T1 e1) { make_voice(voice); append(*voice, e1); } + template + inline void make_voice (Voice **voice, T1 e1, T2 e2) { make_voice(voice, e1); append(*voice, e2); } + template + inline void make_voice (Voice **voice, T1 e1, T2 e2, T3 e3) { make_voice(voice, e1, e2); append(*voice, e3); } + template + inline void make_voice (Voice **voice, T1 e1, T2 e2, T3 e3, T4 e4) { make_voice(voice, e1, e2, e3); append(*voice, e4); } + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::SelfTestHandler ///////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class SelfTestHandler + { + MK_VIII *mk; + + typedef enum + { + CANCEL_NONE, + CANCEL_SHORT, + CANCEL_LONG + } Cancel; + + enum + { + ACTION_SLEEP = 1 << 0, + ACTION_VOICE = 1 << 1, + ACTION_DISCRETE_ON_OFF = 1 << 2, + ACTION_DONE = 1 << 3 + }; + + typedef struct + { + unsigned int flags; + double sleep_duration; + bool *discrete; + } Action; + + Cancel cancel; + Action action; + int current; + bool button_pressed; + double button_press_timestamp; + IOHandler::Outputs saved_outputs; + double sleep_start; + + bool _was_here (int position); + + Action sleep (double duration); + Action play (VoicePlayer::Voice *voice); + Action discrete_on (bool *discrete, double duration); + Action discrete_on_off (bool *discrete, double duration); + Action discrete_on_off (bool *discrete, VoicePlayer::Voice *voice); + Action done (); + + Action run (); + + void start (); + void stop (); + void shutdown (); + + public: + typedef enum + { + STATE_NONE, + STATE_START, + STATE_RUNNING + } State; + + State state; + + inline SelfTestHandler (MK_VIII *device) + : mk(device), state(STATE_NONE), button_pressed(false) {} + + inline void power_off () { stop(); } + inline void set_inop () { stop(); } + void handle_button_event (bool value); + bool update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::AlertHandler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class AlertHandler + { + MK_VIII *mk; + + unsigned int old_alerts; + unsigned int voice_alerts; + unsigned int repeated_alerts; + VoicePlayer::Voice *altitude_callout_voice; + + void reset (); + inline bool has_alerts (unsigned int test) const { return (alerts & test) != 0; } + inline bool has_old_alerts (unsigned int test) const { return (old_alerts & test) != 0; } + inline bool must_play_voice (unsigned int test) const { return ! has_old_alerts(test) || (repeated_alerts & test) != 0; } + bool select_voice_alerts (unsigned int test); + + public: + enum + { + ALERT_MODE1_PULL_UP = 1 << 0, + ALERT_MODE1_SINK_RATE = 1 << 1, + + ALERT_MODE2A_PREFACE = 1 << 2, + ALERT_MODE2B_PREFACE = 1 << 3, + ALERT_MODE2A = 1 << 4, + ALERT_MODE2B = 1 << 5, + ALERT_MODE2B_LANDING_MODE = 1 << 6, + ALERT_MODE2A_ALTITUDE_GAIN = 1 << 7, + ALERT_MODE2A_ALTITUDE_GAIN_TERRAIN_CLOSING = 1 << 8, + + ALERT_MODE3 = 1 << 9, + + ALERT_MODE4_TOO_LOW_FLAPS = 1 << 10, + ALERT_MODE4_TOO_LOW_GEAR = 1 << 11, + ALERT_MODE4AB_TOO_LOW_TERRAIN = 1 << 12, + ALERT_MODE4C_TOO_LOW_TERRAIN = 1 << 13, + + ALERT_MODE5_SOFT = 1 << 14, + ALERT_MODE5_HARD = 1 << 15, + + ALERT_MODE6_MINIMUMS = 1 << 16, + ALERT_MODE6_ALTITUDE_CALLOUT = 1 << 17, + ALERT_MODE6_LOW_BANK_ANGLE_1 = 1 << 18, + ALERT_MODE6_HIGH_BANK_ANGLE_1 = 1 << 19, + ALERT_MODE6_LOW_BANK_ANGLE_2 = 1 << 20, + ALERT_MODE6_HIGH_BANK_ANGLE_2 = 1 << 21, + ALERT_MODE6_LOW_BANK_ANGLE_3 = 1 << 22, + ALERT_MODE6_HIGH_BANK_ANGLE_3 = 1 << 23, + + ALERT_TCF_TOO_LOW_TERRAIN = 1 << 24 + }; + + enum + { + ALERT_FLAG_REPEAT = 1 << 0 + }; + + unsigned int alerts; + + inline AlertHandler (MK_VIII *device) + : mk(device) {} + + void boot (); + void reposition (); + void update (); + + void set_alerts (unsigned int _alerts, + unsigned int flags = 0, + VoicePlayer::Voice *_altitude_callout_voice = NULL); + void unset_alerts (unsigned int _alerts); + + inline void repeat_alert (unsigned int alert) { set_alerts(alert, ALERT_FLAG_REPEAT); } + inline void set_altitude_callout_alert (VoicePlayer::Voice *voice) { set_alerts(ALERT_MODE6_ALTITUDE_CALLOUT, 0, voice); } + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::StateHandler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class StateHandler + { + MK_VIII *mk; + + Timer potentially_airborne_timer; + + void update_ground (); + void enter_ground (); + void leave_ground (); + + void update_takeoff (); + void enter_takeoff (); + void leave_takeoff (); + + public: + bool ground; + bool takeoff; + + inline StateHandler (MK_VIII *device) + : mk(device), ground(true), takeoff(true) {} + + void post_reposition (); + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode1Handler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Mode1Handler + { + MK_VIII *mk; + + Timer pull_up_timer; + Timer sink_rate_timer; + + double sink_rate_tti; // time-to-impact in minutes + + double get_pull_up_bias (); + bool is_pull_up (); + + double get_sink_rate_bias (); + bool is_sink_rate (); + double get_sink_rate_tti (); + + void update_pull_up (); + void update_sink_rate (); + + public: + typedef struct + { + bool flap_override_bias; + int min_agl; + double (*pull_up_min_agl1) (double vs); + int pull_up_max_agl1; + double (*pull_up_min_agl2) (double vs); + int pull_up_max_agl2; + } EnvelopesConfiguration; + + struct + { + const EnvelopesConfiguration *envelopes; + } conf; + + inline Mode1Handler (MK_VIII *device) + : mk(device) {} + + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode2Handler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Mode2Handler + { + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode2Handler::ClosureRateFilter /////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + class ClosureRateFilter + { + ///////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode2Handler::ClosureRateFilter::PassFilter ///////////////// + ///////////////////////////////////////////////////////////////////////// + + class PassFilter + { + double a0; + double a1; + double b1; + + double last_input; + double last_output; + + public: + inline PassFilter (double _a0, double _a1, double _b1) + : a0(_a0), a1(_a1), b1(_b1) {} + + inline double filter (double input) + { + last_output = a0 * input + a1 * last_input + b1 * last_output; + last_input = input; + + return last_output; + } + + inline void reset () + { + last_input = 0; + last_output = 0; + } + }; + + ///////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode2Handler::ClosureRateFilter (continued) ///////////////// + ///////////////////////////////////////////////////////////////////////// + + MK_VIII *mk; + + Timer timer; + Parameter last_ra; // last radio altitude + Parameter last_ba; // last barometric altitude + PassFilter ra_filter; // radio altitude rate filter + PassFilter ba_filter; // barometric altitude rate filter + + double limit_radio_altitude_rate (double r); + + public: + Parameter output; + + inline ClosureRateFilter (MK_VIII *device) + : mk(device), + ra_filter(0.05, 0, 0.95), // low-pass filter + ba_filter(0.93, -0.93, 0.86) {} // high-pass-filter + + void init (); + void update (); + }; + + /////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode2Handler (continued) ////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + MK_VIII *mk; + + ClosureRateFilter closure_rate_filter; + + Timer takeoff_timer; + Timer pull_up_timer; + + double a_start_time; + Timer a_altitude_gain_timer; + double a_altitude_gain_alt; + + void check_pull_up (unsigned int preface_alert, unsigned int alert); + + bool b_conditions (); + + bool is_a (); + bool is_b (); + + void update_a (); + void update_b (); + + public: + typedef struct + { + int airspeed1; + int airspeed2; + } Configuration; + + const Configuration *conf; + + inline Mode2Handler (MK_VIII *device) + : mk(device), closure_rate_filter(device) {} + + void boot (); + void power_off (); + void leave_ground (); + void enter_takeoff (); + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode3Handler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Mode3Handler + { + MK_VIII *mk; + + bool armed; + bool has_descent_alt; + double descent_alt; + double bias; + + double max_alt_loss (double _bias); + double get_bias (double initial_bias, double alt_loss); + bool is (double *alt_loss); + + public: + typedef struct + { + int min_agl; + int (*max_agl) (bool flap_override); + double (*max_alt_loss) (bool flap_override, double agl); + } Configuration; + + const Configuration *conf; + + inline Mode3Handler (MK_VIII *device) + : mk(device), armed(false), has_descent_alt(false) {} + + void enter_takeoff (); + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode4Handler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Mode4Handler + { + public: + typedef struct + { + int airspeed1; + int airspeed2; + int min_agl1; + double (*min_agl2) (double airspeed); + int min_agl3; + } EnvelopesConfiguration; + + typedef struct + { + const EnvelopesConfiguration *ac; + const EnvelopesConfiguration *b; + } ModesConfiguration; + + struct + { + VoicePlayer::Voice *voice_too_low_gear; + const ModesConfiguration *modes; + } conf; + + inline Mode4Handler (MK_VIII *device) + : mk(device) {} + + double get_upper_agl (const EnvelopesConfiguration *c); + void update (); + + private: + MK_VIII *mk; + + double ab_bias; + double ab_expanded_bias; + double c_bias; + + const EnvelopesConfiguration *get_ab_envelope (); + double get_bias (double initial_bias, double min_agl); + void handle_alert (unsigned int alert, double min_agl, double *bias); + + void update_ab (); + void update_ab_expanded (); + void update_c (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode5Handler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Mode5Handler + { + MK_VIII *mk; + + Timer hard_timer; + Timer soft_timer; + + double soft_bias; + + bool is_hard (); + bool is_soft (double bias); + + double get_soft_bias (double initial_bias); + + void update_hard (bool is); + void update_soft (bool is); + + public: + inline Mode5Handler (MK_VIII *device) + : mk(device) {} + + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::Mode6Handler //////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class Mode6Handler + { + public: + // keep in sync with altitude_callout_definitions[] + typedef enum + { + ALTITUDE_CALLOUT_1000, + ALTITUDE_CALLOUT_500, + ALTITUDE_CALLOUT_400, + ALTITUDE_CALLOUT_300, + ALTITUDE_CALLOUT_200, + ALTITUDE_CALLOUT_100, + ALTITUDE_CALLOUT_50, + ALTITUDE_CALLOUT_40, + ALTITUDE_CALLOUT_30, + ALTITUDE_CALLOUT_20, + ALTITUDE_CALLOUT_10 + } AltitudeCallout; + + typedef bool (*BankAnglePredicate) (Parameter *agl, + double abs_roll_deg, + bool ap_engaged); + + struct + { + bool minimums_enabled; + bool smart_500_enabled; + VoicePlayer::Voice *above_field_voice; + + bool altitude_callouts_enabled[n_altitude_callouts]; + bool bank_angle_enabled; + BankAnglePredicate is_bank_angle; + } conf; + + static const int altitude_callout_definitions[]; + + inline Mode6Handler (MK_VIII *device) + : mk(device) {} + + void boot (); + void power_off (); + void enter_takeoff (); + void leave_takeoff (); + void set_volume (double volume); + bool altitude_callouts_enabled (); + void update (); + + private: + MK_VIII *mk; + + bool last_decision_height; + Parameter last_radio_altitude; + Parameter last_altitude_above_field; + + bool altitude_callouts_issued[n_altitude_callouts]; + bool minimums_issued; + bool above_field_issued; + + Timer runway_timer; + Parameter has_runway; + + struct + { + double elevation; // elevation in feet + } runway; + + void reset_minimums (); + void reset_altitude_callouts (); + bool is_playing_altitude_callout (); + bool is_near_minimums (double callout); + bool is_outside_band (double elevation, double callout); + bool inhibit_smart_500 (); + + void update_minimums (); + void update_altitude_callouts (); + + bool test_runway (const FGRunway *_runway); + bool test_airport (const FGAirport *airport); + void update_runway (); + + void get_altitude_above_field (Parameter *parameter); + void update_above_field_callout (); + + bool is_bank_angle (double abs_roll_angle, double bias); + bool is_high_bank_angle (); + unsigned int get_bank_angle_alerts (); + void update_bank_angle (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII::TCFHandler ////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + class TCFHandler + { + typedef struct + { + double latitude; // latitude in degrees + double longitude; // longitude in degrees + } Position; + + typedef struct + { + Position position; // position of threshold + double heading; // runway heading + } RunwayEdge; + + MK_VIII *mk; + + static const double k; + + Timer runway_timer; + bool has_runway; + + struct + { + Position center; // center point + double elevation; // elevation in feet + double half_length; // runway half length, in nautical miles + RunwayEdge edges[2]; // runway threshold and end + Position bias_area[4]; // vertices of the bias area + } runway; + + double bias; + double *reference; + double initial_value; + + double get_azimuth_difference (double from_lat, + double from_lon, + double to_lat, + double to_lon, + double to_heading); + double get_azimuth_difference (const FGRunway *_runway); + + void select_runway (const FGAirport *airport, FGRunway *_runway); + bool test_airport (const FGAirport *airport); + void update_runway (); + + void get_bias_area_edges (Position *edge, + double reciprocal, + double half_width_m, + Position *bias_edge1, + Position *bias_edge2); + + bool is_inside_edge_triangle (RunwayEdge *edge); + bool is_inside_bias_area (); + + bool is_tcf (); + bool is_rfcf (); + + public: + struct + { + bool enabled; + } conf; + + inline TCFHandler (MK_VIII *device) + : mk(device) {} + + void update (); + }; + + ///////////////////////////////////////////////////////////////////////////// + // MK_VIII (continued) ////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + string name; + int num; + + PropertiesHandler properties_handler; + PowerHandler power_handler; + SystemHandler system_handler; + ConfigurationModule configuration_module; + FaultHandler fault_handler; + IOHandler io_handler; + VoicePlayer voice_player; + SelfTestHandler self_test_handler; + AlertHandler alert_handler; + StateHandler state_handler; + Mode1Handler mode1_handler; + Mode2Handler mode2_handler; + Mode3Handler mode3_handler; + Mode4Handler mode4_handler; + Mode5Handler mode5_handler; + Mode6Handler mode6_handler; + TCFHandler tcf_handler; + + struct + { + int runway_database; + } conf; + +public: + MK_VIII (SGPropertyNode *node); + + virtual void init (); + virtual void bind (); + virtual void unbind (); + virtual void update (double dt); +}; + +#endif // __INSTRUMENTS_MK_VIII_HXX diff --git a/src/Main/fg_commands.cxx b/src/Main/fg_commands.cxx index 2a1863411..e95cbea6b 100644 --- a/src/Main/fg_commands.cxx +++ b/src/Main/fg_commands.cxx @@ -1353,6 +1353,49 @@ do_save_xml_from_proptree(const SGPropertyNode * node) return true; } +static bool +do_press_cockpit_button (const SGPropertyNode *arg) +{ + const char *prefix = arg->getStringValue("prefix"); + + if (arg->getBoolValue("guarded") && fgGetDouble((string(prefix) + "-guard").c_str()) < 1) + return true; + + string prop = string(prefix) + "-button"; + double value; + + if (arg->getBoolValue("latching")) + value = fgGetDouble(prop.c_str()) > 0 ? 0 : 1; + else + value = 1; + + fgSetDouble(prop.c_str(), value); + fgSetBool(arg->getStringValue("discrete"), value > 0); + + return true; +} + +static bool +do_release_cockpit_button (const SGPropertyNode *arg) +{ + const char *prefix = arg->getStringValue("prefix"); + + if (arg->getBoolValue("guarded")) { + string prop = string(prefix) + "-guard"; + if (fgGetDouble(prop.c_str()) < 1) { + fgSetDouble(prop.c_str(), 1); + return true; + } + } + + if (! arg->getBoolValue("latching")) { + fgSetDouble((string(prefix) + "-button").c_str(), 0); + fgSetBool(arg->getStringValue("discrete"), false); + } + + return true; +} + //////////////////////////////////////////////////////////////////////// // Command setup. @@ -1419,6 +1462,8 @@ static struct { { "hud-init2", do_hud_init2 }, { "loadxml", do_load_xml_to_proptree}, { "savexml", do_save_xml_from_proptree }, + { "press-cockpit-button", do_press_cockpit_button }, + { "release-cockpit-button", do_release_cockpit_button }, { 0, 0 } // zero-terminated };