diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index fa6db1e8931..80b3d03c34e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -12,6 +12,8 @@ ## 0.15.0 +1. [A380X/GPWS] Fix mode 4 logic - @Jonny23787 (Jonathan) + ## 0.14.0 1. [A380X/FMS] Fix speed margins being displayed in the wrong place for a Mach target - @BlueberryKing (BlueberryKing) diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Climb.flt b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Climb.flt index cd197c9a621..0fa6315172c 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Climb.flt +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Climb.flt @@ -267,6 +267,8 @@ A380X_EFIS_R_TRAF_BUTTON_IS_ON=1 A32NX_FAC_1_PUSHBUTTON_PRESSED=1 A32NX_FAC_2_PUSHBUTTON_PRESSED=1 A32NX_GEAR_LEVER_POSITION_REQUEST = 0 +A32NX_GPWS_GROUND_STATE=0 +A32NX_GPWS_APPROACH_STATE=1 A32NX_ISIS_BUGS_ALT_ACTIVE:0=0 A32NX_ISIS_BUGS_ALT_ACTIVE:1=0 A32NX_ISIS_BUGS_ALT_VALUE:0=100 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/approach.FLT b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/approach.FLT index 5c2aec9597b..4c7b2e43da0 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/approach.FLT +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/approach.FLT @@ -301,6 +301,8 @@ A32NX_FAC_2_PUSHBUTTON_PRESSED=1 A32NX_FMS_SWITCHING_KNOB=1 A32NX_FWC_SKIP_STARTUP=1 A32NX_GEAR_LEVER_POSITION_REQUEST = 0 +A32NX_GPWS_GROUND_STATE=0 +A32NX_GPWS_APPROACH_STATE=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 A32NX_INITIAL_FLIGHT_PHASE = 5 A32NX_ISIS_BUGS_ALT_ACTIVE:0=0 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/apron.FLT b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/apron.FLT index ec4f223d6c1..83b3768651b 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/apron.FLT +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/apron.FLT @@ -297,6 +297,8 @@ A32NX_GEAR_CENTER_POSITION = 100 A32NX_GEAR_LEFT_POSITION = 100 A32NX_GEAR_LEVER_POSITION_REQUEST = 1 A32NX_GEAR_RIGHT_POSITION = 100 +A32NX_GPWS_GROUND_STATE=1 +A32NX_GPWS_APPROACH_STATE=0 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 A32NX_ISIS_BUGS_ALT_ACTIVE:0=0 A32NX_ISIS_BUGS_ALT_ACTIVE:1=0 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/cruise.FLT b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/cruise.FLT index adcf6a26d3e..e621e0e54f9 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/cruise.FLT +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/cruise.FLT @@ -320,6 +320,8 @@ A32NX_FAC_2_PUSHBUTTON_PRESSED=1 A32NX_FMS_SWITCHING_KNOB=1 A32NX_FWC_SKIP_STARTUP=1 A32NX_GEAR_LEVER_POSITION_REQUEST = 0 +A32NX_GPWS_GROUND_STATE=0 +A32NX_GPWS_APPROACH_STATE=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 A32NX_INITIAL_FLIGHT_PHASE = 3 A32NX_ISIS_BUGS_ALT_ACTIVE:0=0 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/hangar.flt b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/hangar.flt index e4a077a0c1b..7b5958cd8e4 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/hangar.flt +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/hangar.flt @@ -272,6 +272,8 @@ A32NX_GEAR_CENTER_POSITION = 100 A32NX_GEAR_LEFT_POSITION = 100 A32NX_GEAR_LEVER_POSITION_REQUEST = 1 A32NX_GEAR_RIGHT_POSITION = 100 +A32NX_GPWS_GROUND_STATE=1 +A32NX_GPWS_APPROACH_STATE=0 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 A32NX_HYD_ELECPUMPY_FAULT=0 A32NX_HYD_ELECPUMPY_TOGGLE=1 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/runway.FLT b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/runway.FLT index c635446df5e..ef534e02e6e 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/runway.FLT +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/runway.FLT @@ -333,6 +333,8 @@ A32NX_GEAR_CENTER_POSITION = 100 A32NX_GEAR_LEFT_POSITION = 100 A32NX_GEAR_LEVER_POSITION_REQUEST = 1 A32NX_GEAR_RIGHT_POSITION = 100 +A32NX_GPWS_GROUND_STATE=1 +A32NX_GPWS_APPROACH_STATE=0 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 A32NX_ISIS_BUGS_ALT_ACTIVE:0=0 A32NX_ISIS_BUGS_ALT_ACTIVE:1=0 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/taxi.flt b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/taxi.flt index c06b139684b..5ba5e22ee2b 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/taxi.flt +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/taxi.flt @@ -315,6 +315,8 @@ A32NX_GEAR_CENTER_POSITION = 100 A32NX_GEAR_LEFT_POSITION = 100 A32NX_GEAR_LEVER_POSITION_REQUEST = 1 A32NX_GEAR_RIGHT_POSITION = 100 +A32NX_GPWS_GROUND_STATE=1 +A32NX_GPWS_APPROACH_STATE=0 A32NX_ISIS_BUGS_ALT_ACTIVE:0=0 A32NX_ISIS_BUGS_ALT_ACTIVE:1=0 A32NX_ISIS_BUGS_ALT_VALUE:0=100 diff --git a/fbw-a380x/src/systems/systems-host/Misc/LegacyGpws.ts b/fbw-a380x/src/systems/systems-host/Misc/LegacyGpws.ts index b78b2a3a918..b87a5c03933 100644 --- a/fbw-a380x/src/systems/systems-host/Misc/LegacyGpws.ts +++ b/fbw-a380x/src/systems/systems-host/Misc/LegacyGpws.ts @@ -1,9 +1,16 @@ // @ts-strict-ignore -import { Arinc429SignStatusMatrix, Arinc429Word, NXDataStore, UpdateThrottler } from '@flybywiresim/fbw-sdk'; +import { + Arinc429SignStatusMatrix, + Arinc429Word, + NXDataStore, + NXLogicConfirmNode, + NXLogicTriggeredMonostableNode, + UpdateThrottler, +} from '@flybywiresim/fbw-sdk'; import { FmgcFlightPhase } from '@shared/flightphase'; import { LegacySoundManager, soundList } from 'systems-host/Misc/LegacySoundManager'; import { A380X_DEFAULT_RADIO_AUTO_CALL_OUTS, A380XRadioAutoCallOutFlags } from '../../shared/src/AutoCallOuts'; -import { EventBus, SimVarValueType } from '@microsoft/msfs-sdk'; +import { EventBus } from '@microsoft/msfs-sdk'; import { FwsSoundManagerControlEvents } from 'systems-host/CpiomC/FlightWarningSystem/FwsSoundManager'; type ModesType = { @@ -64,7 +71,7 @@ export class LegacyGpws { this.Mode3MaxBaroAlt = NaN; - this.Mode4MaxRAAlt = NaN; + this.Mode4MaxRAAlt = 0; this.Mode2BoundaryLeaveAlt = NaN; this.Mode2NumTerrain = 0; @@ -131,6 +138,21 @@ export class LegacyGpws { this.RetardState = createStateMachine(RetardStateMachine); this.RetardState.setState('landed'); } + private isAirVsGroundMode = SimVar.GetSimVarValue('L:A32NX_GPWS_GROUND_STATE', 'Bool') !== 1; + private airborneFor5s = new NXLogicConfirmNode(5); + private airborneFor10s = new NXLogicConfirmNode(10); + + private isApproachVsTakeoffState = SimVar.GetSimVarValue('L:A32NX_GPWS_APPROACH_STATE', 'Bool') === 1; + + private isOverflightDetected = new NXLogicTriggeredMonostableNode(60, false); + // Only relevant if alternate mode 4b is enabled + private isMode4aInhibited = false; + + // PIN PROGs + private isAudioDeclutterEnabled = false; + private isAlternateMode4bEnabled = false; + private isTerrainClearanceFloorEnabled = false; + private isTerrainAwarenessEnabled = false; gpwsUpdateDiscreteWords() { this.egpwsAlertDiscreteWord1.ssm = Arinc429SignStatusMatrix.NormalOperation; @@ -226,20 +248,41 @@ export class LegacyGpws { gpws(deltaTime) { // EGPWS receives ADR1 only const baroAlt = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_BARO_CORRECTED_ALTITUDE_1'); + const computedAirspeed = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_COMPUTED_AIRSPEED'); + const pitch = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_IR_1_PITCH'); + const inertialVs = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_IR_1_VERTICAL_SPEED'); + const barometricVs = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_BAROMETRIC_VERTICAL_SPEED'); const radioAlt1 = Arinc429Word.fromSimVarValue('L:A32NX_RA_1_RADIO_ALTITUDE'); const radioAlt2 = Arinc429Word.fromSimVarValue('L:A32NX_RA_2_RADIO_ALTITUDE'); const radioAlt = radioAlt1.isFailureWarning() || radioAlt1.isNoComputedData() ? radioAlt2 : radioAlt1; const radioAltValid = radioAlt.isNormalOperation(); - const onGround = SimVar.GetSimVarValue('SIM ON GROUND', 'Bool'); + const isOnGround = !this.isAirVsGroundMode; + + const isGpwsSysOff = SimVar.GetSimVarValue('L:A32NX_GPWS_SYS_OFF', 'Bool') === 1; + const isTerrModeOff = SimVar.GetSimVarValue('L:A32NX_GPWS_TERR_OFF', 'Bool') === 1; + const isFlapModeOff = SimVar.GetSimVarValue('L:A32NX_GPWS_FLAP_OFF', 'Bool') === 1; + const isLdgFlap3On = SimVar.GetSimVarValue('L:A32NX_GPWS_FLAPS3', 'Bool') === 1; + + const sfccPositionWord = Arinc429Word.fromSimVarValue('L:A32NX_SFCC_SLAT_FLAP_ACTUAL_POSITION_WORD'); + const isFlapsFull = sfccPositionWord.bitValueOr(22, false); + const isFlaps3 = sfccPositionWord.bitValueOr(21, false) && !isFlapsFull; + + const areFlapsInLandingConfig = + !sfccPositionWord.isNormalOperation() || isFlapModeOff || (isLdgFlap3On ? isFlaps3 : isFlapsFull); + const isGearDownLocked = SimVar.GetSimVarValue('L:A32NX_LGCIU_1_LEFT_GEAR_DOWNLOCKED', 'Bool') === 1; + + // TODO only use this in the air? + const isNavAccuracyHigh = SimVar.GetSimVarValue('L:A32NX_FMGC_L_NAV_ACCURACY_HIGH', 'Bool') === 1; + const isTcfOperational = this.isTerrainClearanceFloorOperational(isTerrModeOff, radioAlt, isNavAccuracyHigh); + const isTafOperational = this.isTerrainAwarenessOperational(isTerrModeOff); this.UpdateAltState(radioAltValid ? radioAlt.value : NaN); this.differentiateRadioalt(radioAltValid ? radioAlt.value : NaN, deltaTime); const mda = SimVar.GetSimVarValue('L:AIRLINER_MINIMUM_DESCENT_ALTITUDE', 'feet'); const dh = SimVar.GetSimVarValue('L:AIRLINER_DECISION_HEIGHT', 'feet'); - const phase = SimVar.GetSimVarValue('L:A32NX_FMGC_FLIGHT_PHASE', 'Enum'); - if (mda !== 0 || (dh !== -1 && dh !== -2 && phase === FmgcFlightPhase.Approach)) { + if (mda !== 0 || (dh !== -1 && dh !== -2 && this.isApproachVsTakeoffState)) { let minimumsDA; // MDA or DH let minimumsIA; // radio or baro altitude if (dh >= 0) { @@ -254,34 +297,40 @@ export class LegacyGpws { } } - const tawsSelected: number = SimVar.GetSimVarValue('L:A32NX_WXR_TAWS_SYS_SELECTED', SimVarValueType.Number); - const gpwsFailed = - tawsSelected === 1 || tawsSelected === 2 - ? SimVar.GetSimVarValue(`L:A32NX_GPWS_${tawsSelected}_FAILED`, 'Bool') - : true; + this.updateMaxRA(radioAlt, isOnGround); - if ( - radioAltValid && - radioAlt.value >= 10 && - radioAlt.value <= 2450 && - !SimVar.GetSimVarValue('L:A32NX_GPWS_SYS_OFF', 'Bool') && - !gpwsFailed - ) { - // Activate between 10 - 2450 radio alt unless SYS is off - const flapsThreeSelected = SimVar.GetSimVarValue('L:A32NX_SPEEDS_LANDING_CONF3', 'Bool'); - const FlapPosition = SimVar.GetSimVarValue('L:A32NX_FLAPS_HANDLE_INDEX', 'Number'); - const FlapsInLandingConfig = flapsThreeSelected ? FlapPosition === 3 : FlapPosition === 4; // fixme should be actual flap angle - const vSpeed = Simplane.getVerticalSpeed(); - const Airspeed = SimVar.GetSimVarValue('AIRSPEED INDICATED', 'Knots'); - const gearExtended = SimVar.GetSimVarValue('GEAR TOTAL PCT EXTENDED', 'Percent') > 0.9; + const isOverflightDetected = this.updateOverflightState(deltaTime); + this.updateMode4aInhibited(isGearDownLocked, areFlapsInLandingConfig); - this.updateMaxRA(radioAlt.value, onGround, phase); + this.updateAirGroundState(deltaTime, computedAirspeed, radioAlt, pitch); + this.updateApproachTakeoffState( + computedAirspeed, + radioAlt, + isGearDownLocked, + areFlapsInLandingConfig, + isTcfOperational, + isTafOperational, + isOverflightDetected, + ); + + if (!isGpwsSysOff && radioAltValid && radioAlt.value >= 10 && radioAlt.value <= 2450) { + //Activate between 10 - 2450 radio alt unless SYS is off + const altRate = this.selectAltitudeRate(inertialVs, barometricVs); - this.GPWSMode1(this.modes[0], radioAlt.value, vSpeed); + this.GPWSMode1(this.modes[0], radioAlt.value, altRate); // Mode 2 is disabled because of an issue with the terrain height simvar which causes false warnings very frequently. See PR#1742 for more info - // this.GPWSMode2(this.modes[1], radioAlt, Airspeed, FlapsInLandingConfig, gearExtended); - this.GPWSMode3(this.modes[2], radioAlt.value, phase); - this.GPWSMode4(this.modes[3], radioAlt.value, Airspeed, FlapsInLandingConfig, gearExtended, phase); + //this.GPWSMode2(this.modes[1], radioAlt, computedAirspeed, areFlapsInLandingConfig, isGearDownLocked); + this.GPWSMode3(this.modes[2], baroAlt, radioAlt.value, altRate, areFlapsInLandingConfig, isGearDownLocked); + this.GPWSMode4( + this.modes[3], + radioAlt.value, + computedAirspeed, + areFlapsInLandingConfig, + isGearDownLocked, + isTcfOperational, + isTafOperational, + isOverflightDetected, + ); this.GPWSMode5(this.modes[4], radioAlt.value); } else { this.modes.forEach((mode) => { @@ -289,9 +338,6 @@ export class LegacyGpws { }); this.Mode3MaxBaroAlt = NaN; - if (onGround || (radioAltValid && radioAlt.value < 10)) { - this.Mode4MaxRAAlt = NaN; - } this.setGlideSlopeWarning(false); this.setGpwsWarning(false); @@ -319,12 +365,12 @@ export class LegacyGpws { } } - updateMaxRA(radioAlt, onGround, phase) { + updateMaxRA(radioAlt, isOnGround) { // on ground check is to get around the fact that radio alt is set to around 300 while loading - if (onGround || phase === FmgcFlightPhase.GoAround) { - this.Mode4MaxRAAlt = NaN; - } else if (this.Mode4MaxRAAlt < radioAlt || Number.isNaN(this.Mode4MaxRAAlt)) { - this.Mode4MaxRAAlt = radioAlt; + if (isOnGround || this.isApproachVsTakeoffState) { + this.Mode4MaxRAAlt = 0; + } else if (radioAlt.isNormalOperation()) { + this.Mode4MaxRAAlt = Math.max(this.Mode4MaxRAAlt, 0.75 * radioAlt.value); } } @@ -391,12 +437,12 @@ export class LegacyGpws { * Compute the GPWS Mode 1 state. * @param mode - The mode object which stores the state. * @param radioAlt - Radio altitude in feet - * @param vSpeed - Vertical speed, in feet/min, should be inertial vertical speed, not sure if simconnect provides that + * @param vSpeed - Vertical speed, in feet/min, NaN if not available */ GPWSMode1(mode, radioAlt, vSpeed) { const sinkrate = -vSpeed; - if (sinkrate <= 1000) { + if (!Number.isFinite(sinkrate) || sinkrate <= 1000) { mode.current = 0; return; } @@ -417,23 +463,27 @@ export class LegacyGpws { * Compute the GPWS Mode 2 state. * @param mode - The mode object which stores the state. * @param radioAlt - Radio altitude in feet - * @param speed - Airspeed in knots. - * @param FlapsInLandingConfig - If flaps is in landing config + * @param computedAirspeed - Arinc429 value of the computed airspeed in knots. + * @param areFlapsInLandingConfig - If flaps is in landing config * @param gearExtended - If the gear is deployed */ - GPWSMode2(mode, radioAlt, speed, FlapsInLandingConfig, gearExtended) { + GPWSMode2(mode, radioAlt, computedAirspeed, areFlapsInLandingConfig, gearExtended) { + if (!computedAirspeed.isNormalOperation()) { + return false; + } + let IsInBoundary = false; const UpperBoundaryRate = -this.RadioAltRate < 3500 ? 0.7937 * -this.RadioAltRate - 1557.5 : 0.19166 * -this.RadioAltRate + 610; - const UpperBoundarySpeed = Math.max(1650, Math.min(2450, 8.8888 * speed - 305.555)); + const UpperBoundarySpeed = Math.max(1650, Math.min(2450, 8.8888 * computedAirspeed.value - 305.555)); - if (!FlapsInLandingConfig && -this.RadioAltRate > 2000) { + if (!areFlapsInLandingConfig && -this.RadioAltRate > 2000) { if (radioAlt < UpperBoundarySpeed && radioAlt < UpperBoundaryRate) { this.Mode2NumFramesInBoundary += 1; } else { this.Mode2NumFramesInBoundary = 0; } - } else if (FlapsInLandingConfig && -this.RadioAltRate > 2000) { + } else if (areFlapsInLandingConfig && -this.RadioAltRate > 2000) { if (radioAlt < 775 && radioAlt < UpperBoundaryRate && -this.RadioAltRate < 10000) { this.Mode2NumFramesInBoundary += 1; } else { @@ -470,31 +520,33 @@ export class LegacyGpws { /** * Compute the GPWS Mode 3 state. - * @param mode - The mode object which stores the state. - * @param radioAlt - Radio altitude in feet - * @param phase - Flight phase index - * @param FlapsInLandingConfig - If flaps is in landing config - * @constructor + * @param {*} mode - The mode object which stores the state. + * @param {*} baroAlt - Arinc429 value of the barometric altitude in feet. + * @param {*} radioAlt - Radio altitude in feet + * @param {*} altRate - Altitude rate in feet per minute + * @param {*} areFlapsInLandingConfig - True if flaps is in landing config + * @param {*} isGearDownLocked - True if the gear is down and locked */ - GPWSMode3(mode, radioAlt, phase) { + GPWSMode3(mode, baroAlt, radioAlt, altRate, areFlapsInLandingConfig, isGearDownLocked) { if ( - !(phase === FmgcFlightPhase.Takeoff || phase === FmgcFlightPhase.GoAround) || + (isGearDownLocked && areFlapsInLandingConfig) || + this.isApproachVsTakeoffState || radioAlt > 1500 || - radioAlt < 10 + radioAlt < 10 || + !baroAlt.isNormalOperation() ) { this.Mode3MaxBaroAlt = NaN; mode.current = 0; return; } - const baroAlt = SimVar.GetSimVarValue('PLANE ALTITUDE', 'feet'); - const maxAltLoss = 0.09 * radioAlt + 7.1; + const hasPositiveAltRate = Number.isFinite(altRate) && altRate > 0; - if (baroAlt > this.Mode3MaxBaroAlt || Number.isNaN(this.Mode3MaxBaroAlt)) { - this.Mode3MaxBaroAlt = baroAlt; + if (baroAlt.value > this.Mode3MaxBaroAlt || Number.isNaN(this.Mode3MaxBaroAlt)) { + this.Mode3MaxBaroAlt = baroAlt.value; mode.current = 0; - } else if (this.Mode3MaxBaroAlt - baroAlt > maxAltLoss) { + } else if (!hasPositiveAltRate && this.Mode3MaxBaroAlt - baroAlt.value > maxAltLoss) { mode.current = 1; } else { mode.current = 0; @@ -505,42 +557,70 @@ export class LegacyGpws { * Compute the GPWS Mode 4 state. * @param mode - The mode object which stores the state. * @param radioAlt - Radio altitude in feet - * @param speed - Airspeed in knots. - * @param FlapsInLandingConfig - If flaps is in landing config + * @param computedAirspeed - ARINC value of the computed airspeed in knots. + * @param areFlapsInLandingConfig - If flaps is in landing config * @param gearExtended - If the gear is extended - * @param phase - Flight phase index + * @param tcfOperational - If the terrain clearance floor is operational + * @param tafOperational - If the terrain awareness floor is operational + * @param isOverflightDetected - If an overflight is detected * @constructor */ - GPWSMode4(mode, radioAlt, speed, FlapsInLandingConfig, gearExtended, phase) { - if (radioAlt < 30 || radioAlt > 1000) { - mode.current = 0; + GPWSMode4( + mode, + radioAlt, + computedAirspeed, + areFlapsInLandingConfig, + gearExtended, + tcfOperational, + tafOperational, + isOverflightDetected, + ) { + mode.current = 0; + + if (!computedAirspeed.isNormalOperation() || radioAlt < 30 || radioAlt > 1000 || !this.isAirVsGroundMode) { return; } - const FlapModeOff = SimVar.GetSimVarValue('L:A32NX_GPWS_FLAP_OFF', 'Bool'); - // Mode 4 A and B logic - if (!gearExtended && phase === FmgcFlightPhase.Approach) { - if (speed < 190 && radioAlt < 500) { - mode.current = 1; - } else if (speed >= 190) { - const maxWarnAlt = 8.333 * speed - 1083.333; - mode.current = radioAlt < maxWarnAlt ? 3 : 0; + const isMode4cEnabled = + !this.isApproachVsTakeoffState && (!areFlapsInLandingConfig || !gearExtended) && this.isAirVsGroundMode; + + // Mode 4 A + if (this.isApproachVsTakeoffState && !gearExtended && !this.isMode4aInhibited) { + const boundary = this.mode4aUpperBoundary( + computedAirspeed.value, + areFlapsInLandingConfig, + tcfOperational, + tafOperational, + isOverflightDetected, + ); + + if (computedAirspeed.value < 190 && radioAlt < 500) { + mode.current = 1; // TOO LOW GEAR + } else if (computedAirspeed.value >= 190 && radioAlt < boundary) { + mode.current = areFlapsInLandingConfig ? 1 : 3; // TOO LOW GEAR or TOO LOW TERRAIN } - } else if (!FlapsInLandingConfig && !FlapModeOff && phase === FmgcFlightPhase.Approach) { - if (speed < 159 && radioAlt < 245) { - mode.current = 2; - } else if (speed >= 159) { - const maxWarnAlt = 8.2967 * speed - 1074.18; - mode.current = radioAlt < maxWarnAlt ? 3 : 0; + // Mode 4 B + } else if (this.isApproachVsTakeoffState && (!areFlapsInLandingConfig || !gearExtended)) { + // Normal 4b mode, flaps down, what is the boundary? + const boundary = this.mode4bUpperBoundary( + computedAirspeed.value, + areFlapsInLandingConfig, + tcfOperational, + tafOperational, + isOverflightDetected, + ); + + if (computedAirspeed.value < 159 && radioAlt < 245) { + mode.current = !gearExtended ? 1 : 2; // TOO LOW GEAR or TOO LOW FLAPS + } else if (computedAirspeed.value >= 159 && radioAlt < boundary) { + mode.current = this.isMode4aInhibited ? 1 : 3; // TOO LOW GEAR or TOO LOW TERRAIN } - } else { - mode.current = 0; - } - if (!FlapsInLandingConfig || !gearExtended) { - const maxWarnAltSpeed = Math.max(Math.min(8.3333 * speed - 1083.33, 1000), 500); - const maxWarnAlt = 0.750751 * this.Mode4MaxRAAlt - 0.750751; + // Mode 4 C + } else if (isMode4cEnabled) { + const maximumFloor = this.mode4cUpperBoundary(computedAirspeed.value); + const maximumFilterValue = Math.min(maximumFloor, this.Mode4MaxRAAlt); - if (this.Mode4MaxRAAlt > 100 && radioAlt < maxWarnAltSpeed && radioAlt < maxWarnAlt) { + if (radioAlt < maximumFilterValue) { mode.current = 3; } } @@ -808,6 +888,151 @@ export class LegacyGpws { break; } } + updateAirGroundState(deltaTime, computedAirspeed, radioAlt, pitchAngle) { + if (!computedAirspeed.isNormalOperation() || !radioAlt.isNormalOperation()) { + // Stay in current state + return; + } + + this.airborneFor5s.write( + computedAirspeed.value > 90 && radioAlt.value > 25 && pitchAngle.isNormalOperation() && pitchAngle.value > 5, + deltaTime, + ); + this.airborneFor10s.write(computedAirspeed.value > 90 && radioAlt.value > 25, deltaTime); + + if (this.isAirVsGroundMode) { + if (radioAlt.value < 25) { + this.isAirVsGroundMode = false; + SimVar.SetSimVarValue('L:A32NX_GPWS_GROUND_STATE', 'Bool', 1); + } + } else { + if (this.airborneFor5s.read() || this.airborneFor5s.read()) { + this.isAirVsGroundMode = true; + SimVar.SetSimVarValue('L:A32NX_GPWS_GROUND_STATE', 'Bool', 0); + } + } + } + + updateApproachTakeoffState( + computedAirspeed, + radioAlt, + isGearDown, + areFlapsInLandingConfig, + tcfOperational, + tafOperational, + isOverflightDetected, + ) { + // TODO: what do we do if computedAirspeed is not NO? + if (!computedAirspeed.isNormalOperation()) { + return; + } + + if (this.isApproachVsTakeoffState) { + // Switch to TO if we pass below 245 ft mode 4b floor without an alert (i.e gear down and flaps in landing config) + if (radioAlt.isNormalOperation() && radioAlt.value < 245 && isGearDown && areFlapsInLandingConfig) { + this.isApproachVsTakeoffState = false; + SimVar.SetSimVarValue('L:A32NX_GPWS_APPROACH_STATE', 'Bool', 0); + } + } else { + const isFirstAlgorithmSatisfied = false; + // - 4C filter value exceeds 4A alert boundary + const isSecondAlgorithmSatisfied = + this.Mode4MaxRAAlt > + this.mode4aUpperBoundary( + computedAirspeed.value, + areFlapsInLandingConfig, + tcfOperational, + tafOperational, + isOverflightDetected, + ); + + if ((isFirstAlgorithmSatisfied || !this.isAudioDeclutterEnabled) && isSecondAlgorithmSatisfied) { + this.isApproachVsTakeoffState = true; + SimVar.SetSimVarValue('L:A32NX_GPWS_APPROACH_STATE', 'Bool', 1); + } + } + } + + mode4aUpperBoundary(computedAirspeed, areFlapsInLandingConfig, tcfOperational, tafOperational, isOverflightDetected) { + let expandedBoundary = 1000; + if (areFlapsInLandingConfig || tcfOperational || tafOperational) { + expandedBoundary = 500; + } else if (isOverflightDetected) { + expandedBoundary = 800; + } + + return Math.max(500, Math.min(expandedBoundary, 8.333 * computedAirspeed - 1083.33)); + } + + mode4bUpperBoundary(computedAirspeed, areFlapsInLandingConfig, tcfOperational, tafOperational, isOverflightDetected) { + let expandedBoundary = 1000; + if (areFlapsInLandingConfig || tcfOperational || tafOperational) { + expandedBoundary = 245; + } else if (isOverflightDetected) { + expandedBoundary = 800; + } + + return Math.max(245, Math.min(expandedBoundary, 8.333 * computedAirspeed - 1083.33)); + } + + mode4cUpperBoundary(computedAirspeed) { + return Math.max(500, Math.min(1000, 8.3333 * computedAirspeed.value - 1083.33)); + } + + updateOverflightState(deltaTime) { + // Need -2200 ft/s to trigger an overflight state + return this.isOverflightDetected.write(this.RadioAltRate < -2200 * 60, deltaTime); + } + + isTerrainClearanceFloorOperational(terrPbOff, radioAlt, fmcNavAccuracyHigh) { + // TODO when ground speed is below 60 kts, always consider fms nav accuracy high + // where does GS come from? + return this.isTerrainAwarenessEnabled && !terrPbOff && radioAlt.isNormalOperation() && fmcNavAccuracyHigh; + } + + isTerrainAwarenessOperational(terrPbOff) { + // TODO replace placeholders + const doesTerrainAwarenessUseGeometricAltitude = true; + const isGeometricAltitudeVfomValid = true; + const isGeometricAltitudeHilValid = true; + const geometricAltitudeVfom = 0; + const isRaimFailureDetected = false; + + return ( + this.isTerrainAwarenessEnabled && + !terrPbOff && + !doesTerrainAwarenessUseGeometricAltitude && + isGeometricAltitudeVfomValid && + isGeometricAltitudeHilValid && + !isRaimFailureDetected && + geometricAltitudeVfom < 200 + ); + } + + updateMode4aInhibited(isGearDownLocked, isFlapsInLandingConfig) { + if (this.isMode4aInhibited) { + if (!this.isAirVsGroundMode || !this.isApproachVsTakeoffState) { + // Reset + this.isMode4aInhibited = false; + } + } else if (this.isAlternateMode4bEnabled) { + if (isGearDownLocked || isFlapsInLandingConfig) { + this.isMode4aInhibited = true; + } + } + } + + selectAltitudeRate(inertialVs, baroVs) { + if (inertialVs.isNormalOperation()) { + return inertialVs.value; + } else if (Number.isFinite(this.RadioAltRate)) { + return this.RadioAltRate; + } else if (baroVs.isNormalOperation()) { + return baroVs.value; + } + + return NaN; + } } const RetardStateMachine = {