diff --git a/fbw-a380x/src/systems/systems-host/index.ts b/fbw-a380x/src/systems/systems-host/index.ts index 52aab2f732e..9c177609ceb 100644 --- a/fbw-a380x/src/systems/systems-host/index.ts +++ b/fbw-a380x/src/systems/systems-host/index.ts @@ -3,17 +3,22 @@ // SPDX-License-Identifier: GPL-3.0 import { EventBus, HEventPublisher, KeyEventManager, Wait, GameStateProvider } from '@microsoft/msfs-sdk'; -import { AtsuSystem } from './systems/atsu'; -import { PowerSupplyBusses } from './systems/powersupply'; +import { LegacyGpws } from 'systems-host/systems/LegacyGpws'; +import { LegacyFwc } from 'systems-host/systems/LegacyFwc'; +import { LegacySoundManager } from 'systems-host/systems/LegacySoundManager'; class SystemsHost extends BaseInstrument { private readonly bus: EventBus; private readonly hEventPublisher: HEventPublisher; - // Uncomment once migrated from the A32NX to the A380X - // private readonly powerSupply: PowerSupplyBusses; - // private readonly atsu: AtsuSystem; + // TODO: Migrate PowerSupplyBusses and AtsuSystem, if needed + + private fwc: LegacyFwc; + + private gpws: LegacyGpws; + + private soundManager: LegacySoundManager; private keyInterceptManager: KeyEventManager; @@ -30,8 +35,23 @@ class SystemsHost extends BaseInstrument { this.bus = new EventBus(); this.hEventPublisher = new HEventPublisher(this.bus); - // this.powerSupply = new PowerSupplyBusses(this.bus); - // this.atsu = new AtsuSystem(this.bus); + this.fwc = new LegacyFwc(); + this.soundManager = new LegacySoundManager(); + this.gpws = new LegacyGpws(this.soundManager); + this.gpws.init(); + + let lastUpdateTime = Date.now(); + setInterval(() => { + const now = Date.now(); + const dt = now - lastUpdateTime; + + this.fwc.update(dt); + this.soundManager.update(dt); + this.gpws.update(dt); + + lastUpdateTime = now; + }, 75); + Promise.all([ KeyEventManager.getManager(this.bus), Wait.awaitSubscribable(GameStateProvider.get(), (state) => state === GameState.ingame, true), @@ -56,9 +76,6 @@ class SystemsHost extends BaseInstrument { public connectedCallback(): void { super.connectedCallback(); - // this.powerSupply.connectedCallback(); - // this.atsu.connectedCallback(); - // Needed to fetch METARs from the sim RegisterViewListener('JS_LISTENER_FACILITY', () => { console.log('JS_LISTENER_FACILITY registered.'); @@ -72,14 +89,9 @@ class SystemsHost extends BaseInstrument { const gamestate = this.getGameState(); if (gamestate === 3) { this.hEventPublisher.startPublish(); - // this.powerSupply.startPublish(); - // this.atsu.startPublish(); } this.gameState = gamestate; } - - // this.powerSupply.update(); - // this.atsu.update(); } private initLighting() { @@ -101,14 +113,14 @@ class SystemsHost extends BaseInstrument { // Instruments Cpt this.setPotentiometer(88, autoBrightness); // PFD this.setPotentiometer(89, autoBrightness); // ND - this.setPotentiometer(94, autoBrightness/2); // wxRadar + this.setPotentiometer(94, autoBrightness / 2); // wxRadar this.setPotentiometer(98, autoBrightness); // MFD this.setPotentiometer(8, autoBrightness < 50 ? 20 : 0); // console light // Instruments F/O this.setPotentiometer(90, autoBrightness); // PFD this.setPotentiometer(91, autoBrightness); // ND - this.setPotentiometer(95, autoBrightness/2); // wxRadar + this.setPotentiometer(95, autoBrightness / 2); // wxRadar this.setPotentiometer(99, autoBrightness); // MFD this.setPotentiometer(9, autoBrightness < 50 ? 20 : 0); // console light @@ -122,7 +134,6 @@ class SystemsHost extends BaseInstrument { this.setPotentiometer(83, autoBrightness); // mainPnlFloodLightLevel this.setPotentiometer(85, autoBrightness); // integralLightLevel this.setPotentiometer(7, autoBrightness); // ambientLightLevel - } private setPotentiometer(potentiometer: number, brightness: number) { diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts b/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts new file mode 100644 index 00000000000..07e80472a40 --- /dev/null +++ b/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts @@ -0,0 +1,660 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable camelcase */ +// TODO remove this once Rust implementation is up and running +import { Arinc429Word } from '@flybywiresim/fbw-sdk'; + +enum FwcFlightPhase { + ElecPwr = 1, + FirstEngineStarted = 2, + FirstEngineTakeOffPower = 3, + AtOrAboveEightyKnots = 4, + LiftOff = 5, + AtOrAbove1500Feet = 6, + AtOrBelow800Feet = 7, + TouchDown = 8, + AtOrBelowEightyKnots = 9, + EnginesShutdown = 10, +} + +/** + * This 1:1 port from the A32NX's FWC serves as temporary replacement, until a more sophisticated system simulation is in place. + * After merge of PR #4872 (https://github.com/flybywiresim/aircraft/pull/4872) (intended for A32NX), the FWS architecture has to + * be ported to the A380X, then this class can be removed. + */ +export class LegacyFwc { + toConfigTest: boolean; + + flightPhase: FwcFlightPhase; + + ldgMemo: boolean; + + toMemo: boolean; + + gndMemo: NXLogic_ConfirmNode; + + eng1OrTwoRunningConf: NXLogic_ConfirmNode; + + speedAbove80KtsMemo: NXLogic_MemoryNode; + + mctMemo: NXLogic_ConfirmNode; + + firePBOutConf: NXLogic_ConfirmNode; + + firePBOutMemo: NXLogic_TriggeredMonostableNode; + + firePBClear10: NXLogic_MemoryNode; + + phase110Memo: NXLogic_TriggeredMonostableNode; + + phase8GroundMemo: NXLogic_TriggeredMonostableNode; + + ac80KtsMemo: NXLogic_TriggeredMonostableNode; + + prevPhase9InvertMemo: NXLogic_TriggeredMonostableNode; + + eng1Or2TOPowerInvertMemo: NXLogic_TriggeredMonostableNode; + + phase9Nvm: NXLogic_MemoryNode; + + prevPhase9: boolean; + + groundImmediateMemo: NXLogic_TriggeredMonostableNode; + + phase5Memo: NXLogic_TriggeredMonostableNode; + + phase67Memo: NXLogic_TriggeredMonostableNode; + + memoTo_conf01: NXLogic_ConfirmNode; + + memoTo_memo: NXLogic_MemoryNode; + + memoLdgMemo_conf01: NXLogic_ConfirmNode; + + memoLdgMemo_inhibit: NXLogic_MemoryNode; + + memoLdgMemo_conf02: NXLogic_ConfirmNode; + + memoLdgMemo_below2000ft: NXLogic_MemoryNode; + + memoToInhibit_conf01: NXLogic_ConfirmNode; + + memoLdgInhibit_conf01: NXLogic_ConfirmNode; + + warningPressed: boolean; + + cautionPressed: boolean; + + previousTargetAltitude: number; + + _wasBellowThreshold: boolean; + + _wasAboveThreshold: boolean; + + _wasInRange: boolean; + + _wasReach200ft: boolean; + + aircraft: Aircraft; + + constructor() { + // momentary + this.toConfigTest = null; // WTOCT + + // persistent + this.flightPhase = null; + this.ldgMemo = null; + this.toMemo = null; + + // ESDL 1. 0. 60 + this.gndMemo = new NXLogic_ConfirmNode(1); // outptuts ZGND + + // ESDL 1. 0. 60 + this.eng1OrTwoRunningConf = new NXLogic_ConfirmNode(30); + + // ESDL 1. 0. 73 + this.speedAbove80KtsMemo = new NXLogic_MemoryNode(true); + + // ESDL 1. 0. 79 / ESDL 1. 0. 80 + this.mctMemo = new NXLogic_ConfirmNode(60, false); + + // ESDL 1. 0.100 + this.firePBOutConf = new NXLogic_ConfirmNode(0.2); // CONF01 + this.firePBOutMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 05 + this.firePBClear10 = new NXLogic_MemoryNode(false); + this.phase110Memo = new NXLogic_TriggeredMonostableNode(300); // MTRIG 03 + this.phase8GroundMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 06 + this.ac80KtsMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 04 + this.prevPhase9InvertMemo = new NXLogic_TriggeredMonostableNode(3, false); // MTRIG 02 + this.eng1Or2TOPowerInvertMemo = new NXLogic_TriggeredMonostableNode(1, false); // MTRIG 01 + this.phase9Nvm = new NXLogic_MemoryNode(true, true); + this.prevPhase9 = false; + + // ESDL 1. 0.110 + this.groundImmediateMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 03 + this.phase5Memo = new NXLogic_TriggeredMonostableNode(120); // MTRIG 01 + this.phase67Memo = new NXLogic_TriggeredMonostableNode(180); // MTRIG 02 + + // ESDL 1. 0.180 + this.memoTo_conf01 = new NXLogic_ConfirmNode(120, true); // CONF 01 + this.memoTo_memo = new NXLogic_MemoryNode(false); + + // ESDL 1. 0.190 + this.memoLdgMemo_conf01 = new NXLogic_ConfirmNode(1, true); // CONF 01 + this.memoLdgMemo_inhibit = new NXLogic_MemoryNode(false); + this.memoLdgMemo_conf02 = new NXLogic_ConfirmNode(10, true); // CONF 01 + this.memoLdgMemo_below2000ft = new NXLogic_MemoryNode(true); + + // ESDL 1. 0.310 + this.memoToInhibit_conf01 = new NXLogic_ConfirmNode(3, true); // CONF 01 + + // ESDL 1. 0.320 + this.memoLdgInhibit_conf01 = new NXLogic_ConfirmNode(3, true); // CONF 01 + + // master warning & caution buttons + this.warningPressed = false; + this.cautionPressed = false; + + // altitude warning + this.previousTargetAltitude = NaN; + this._wasBellowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._wasReach200ft = false; + } + + update(_deltaTime: number) { + this._updateFlightPhase(_deltaTime); + this._updateButtons(_deltaTime); + this._updateTakeoffMemo(_deltaTime); + this._updateLandingMemo(_deltaTime); + this._updateAltitudeWarning(); + } + + _updateButtons(_deltaTime: number) { + this.toConfigTest = SimVar.GetSimVarValue('L:A32NX_FWS_TO_CONFIG_TEST', 'boolean'); + + if (SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_L', 'Bool') || SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_R', 'Bool')) { + this.warningPressed = true; + } else { + this.warningPressed = false; + } + if (SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERCAUT_L', 'Bool') || SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERCAUT_R', 'Bool')) { + this.cautionPressed = true; + } else { + this.cautionPressed = false; + } + } + + _updateFlightPhase(_deltaTime: number) { + const radioHeight1 = Arinc429Word.fromSimVarValue('L:A32NX_RA_1_RADIO_ALTITUDE'); + const radioHeight2 = Arinc429Word.fromSimVarValue('L:A32NX_RA_2_RADIO_ALTITUDE'); + const radioHeight = radioHeight1.isFailureWarning() || radioHeight1.isNoComputedData() ? radioHeight2 : radioHeight1; + const eng1N1 = SimVar.GetSimVarValue('ENG N1 RPM:1', 'Percent'); + const eng2N1 = SimVar.GetSimVarValue('ENG N1 RPM:2', 'Percent'); + // TODO find a better source for the following value ("core speed at or above idle") + // Note that N1 starts below idle on spawn on the runway, so this should be below 16 to not jump back to phase 1 + const oneEngRunning = ( + eng1N1 > 15 || eng2N1 > 15 + ); + const eng1Or2Running = this.eng1OrTwoRunningConf.write(oneEngRunning, _deltaTime); + const engOneAndTwoNotRunning = !eng1Or2Running; + const hFail = radioHeight1.isFailureWarning() && radioHeight2.isFailureWarning(); + const adcTestInhib = false; + + // ESLD 1.0.60 + const groundImmediate = Simplane.getIsGrounded(); + const ground = this.gndMemo.write(groundImmediate, _deltaTime); + + // ESLD 1.0.73 + const ias = SimVar.GetSimVarValue('AIRSPEED INDICATED', 'knots'); + const acSpeedAbove80kts = this.speedAbove80KtsMemo.write(ias > 83, ias < 77); + + // ESLD 1.0.90 + const hAbv1500 = radioHeight.isNoComputedData() || radioHeight.value > 1500; + const hAbv800 = radioHeight.isNoComputedData() || radioHeight.value > 800; + + // ESLD 1.0.79 + 1.0.80 + const eng1TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:1', 'number'); + const eng1TLAFTO = SimVar.GetSimVarValue('L:AIRLINER_TO_FLEX_TEMP', 'number') !== 0; // is a flex temp is set? + const eng1MCT = eng1TLA > 33.3 && eng1TLA < 36.7; + const eng1TLAFullPwr = eng1TLA > 43.3; + const eng2TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:2', 'number'); + const eng2TLAFTO = eng1TLAFTO; // until we have proper FADECs + const eng2MCT = eng2TLA > 33.3 && eng2TLA < 36.7; + const eng2TLAFullPwr = eng2TLA > 43.3; + const eng1OrEng2SupMCT = !(eng1TLA < 36.7) || !(eng2TLA < 36.7); + const eng1AndEng2MCL = eng1TLA > 22.9 && eng2TLA > 22.9; + const eng1Or2TOPowerSignal = ( + (eng1TLAFTO && eng1MCT) + || (eng2TLAFTO && eng2MCT) + || (eng1OrEng2SupMCT || eng1OrEng2SupMCT) + || (eng1TLAFullPwr || eng2TLAFullPwr) + ); + const eng1Or2TOPower = ( + eng1Or2TOPowerSignal + || (this.mctMemo.write(eng1Or2TOPowerSignal, _deltaTime) && !hAbv1500 && eng1AndEng2MCL) + ); + + // ESLD 1.0.100 + const eng1FirePbOut = SimVar.GetSimVarValue('L:A32NX_FIRE_BUTTON_ENG1', 'Bool'); + const eng1FirePbMemo = this.firePBOutMemo.write( + this.firePBOutConf.write(eng1FirePbOut, _deltaTime), + _deltaTime, + ); + const resetFirePbClear10 = eng1FirePbMemo && ground; + + const phase8 = ( + (this.phase8GroundMemo.write(groundImmediate, _deltaTime) || groundImmediate) + && !eng1Or2TOPower + && acSpeedAbove80kts + ); + + const phase34Cond = ground && eng1Or2TOPower; + const phase3 = !acSpeedAbove80kts && eng1Or2Running && phase34Cond; + const phase4 = acSpeedAbove80kts && phase34Cond; + + const setPhase9Nvm = phase3 || phase8; + const resetPhase9Nvm = ( + ( + !this.ac80KtsMemo.write(!acSpeedAbove80kts, _deltaTime) + && ( + (ground && this.prevPhase9InvertMemo.write(this.prevPhase9, _deltaTime)) + || resetFirePbClear10 + || (ground && this.eng1Or2TOPowerInvertMemo.write(eng1Or2TOPower, _deltaTime)) + ) + && !this.prevPhase9 + ) || adcTestInhib + ); + const phase9Nvm = this.phase9Nvm.write(setPhase9Nvm, resetPhase9Nvm); // S* / R (NVM) + const phase29Cond = ground && !eng1Or2TOPower && !acSpeedAbove80kts; + const phase9 = oneEngRunning && phase9Nvm && phase29Cond; + const phase2 = phase29Cond && !phase9Nvm && eng1Or2Running; + + const phase110MemoA = this.firePBClear10.write(phase9, resetFirePbClear10); // S / R* + const phase110Cond = !phase9 && engOneAndTwoNotRunning && groundImmediate; + const phase110Memo = this.phase110Memo.write(phase110MemoA && phase110Cond, _deltaTime); // MTRIG 03 + const phase1 = phase110Cond && !phase110Memo; + const phase10 = phase110Cond && phase110Memo; + + this.prevPhase9 = phase9; + + // ESLD 1.0.110 + const ground2sMemorized = this.groundImmediateMemo.write(groundImmediate, _deltaTime) || groundImmediate; + const phase5Cond = !hAbv1500 && eng1Or2TOPower && !hFail && !ground2sMemorized; + const phase5 = this.phase5Memo.write(phase5Cond, _deltaTime) && phase5Cond; + + const phase67Cond = ( + !ground2sMemorized + && !hFail + && !eng1Or2TOPower + && !hAbv1500 + && !hAbv800 + ); + const phase67Memo = this.phase67Memo.write(phase67Cond, _deltaTime) && phase67Cond; + + const phase6 = !phase5 && !ground2sMemorized && !phase67Memo; + const phase7 = phase67Memo && !phase8; + + /** * End of ESLD logic ** */ + + // consolidate into single variable (just to be safe) + const phases = [phase1, phase2, phase3, phase4, phase5, phase6, phase7, phase8, phase9, phase10]; + + if (this.flightPhase === null && phases.indexOf(true) !== -1) { + // if we aren't initialized, just grab the first one that is valid + this._setFlightPhase(phases.indexOf(true) + 1); + console.log(`FWC flight phase: ${this.flightPhase}`); + return; + } + + const activePhases = phases.map((x, i) => [x ? 1 : 0, i + 1]).filter((y) => y[0] === 1).map((z) => z[1]); + + // the usual and easy case: only one flight phase is valid + if (activePhases.length === 1) { + if (activePhases[0] !== this.flightPhase) { + console.log(`FWC flight phase: ${this.flightPhase} => ${activePhases[0]}`); + this._setFlightPhase(activePhases[0]); + } + return; + } + + // the mixed case => warn + if (activePhases.length > 1) { + console.warn(`Multiple FWC flight phases are valid: ${activePhases.join(', ')}`); + if (activePhases.indexOf(this.flightPhase) !== -1) { + // if the currently active one is present, keep it + console.warn(`Remaining in FWC flight phase ${this.flightPhase}`); + return; + } + // pick the earliest one + this._setFlightPhase(activePhases[0]); + console.log(`Resolving by switching FWC flight phase: ${this.flightPhase} => ${activePhases[0]}`); + return; + } + + // otherwise, no flight phase is valid => warn + console.warn('No valid FWC flight phase'); + if (this.flightPhase === null) { + this._setFlightPhase(null); + } + } + + _setFlightPhase(flightPhase: FwcFlightPhase) { + if (flightPhase === this.flightPhase) { + return; + } + + // update flight phase + this.flightPhase = flightPhase; + SimVar.SetSimVarValue('L:A32NX_FWC_FLIGHT_PHASE', 'Enum', this.flightPhase || 0); + } + + _updateTakeoffMemo(_deltaTime: number) { + /// FWC ESLD 1.0.180 + const setFlightPhaseMemo = this.flightPhase === 2 && this.toConfigTest; + const resetFlightPhaseMemo = ( + this.flightPhase === 10 + || this.flightPhase === 3 + || this.flightPhase === 1 + || this.flightPhase === 6 + ); + const flightPhaseMemo = this.memoTo_memo.write(setFlightPhaseMemo, resetFlightPhaseMemo); + + const eng1NotRunning = SimVar.GetSimVarValue('ENG N1 RPM:1', 'Percent') < 15; + const eng2NotRunning = SimVar.GetSimVarValue('ENG N1 RPM:2', 'Percent') < 15; + const toTimerElapsed = this.memoTo_conf01.write(!eng1NotRunning && !eng2NotRunning, _deltaTime); + + this.toMemo = flightPhaseMemo || (this.flightPhase === 2 && toTimerElapsed); + SimVar.SetSimVarValue('L:A32NX_FWC_TOMEMO', 'Bool', this.toMemo); + } + + _updateLandingMemo(_deltaTime: number) { + const radioHeight1 = Arinc429Word.fromSimVarValue('L:A32NX_RA_1_RADIO_ALTITUDE'); + const radioHeight2 = Arinc429Word.fromSimVarValue('L:A32NX_RA_2_RADIO_ALTITUDE'); + const radioHeight1Invalid = radioHeight1.isFailureWarning() || radioHeight1.isNoComputedData(); + const radioHeight2Invalid = radioHeight2.isFailureWarning() || radioHeight2.isNoComputedData(); + const gearDownlocked = SimVar.GetSimVarValue('GEAR TOTAL PCT EXTENDED', 'percent') > 0.95; + + // FWC ESLD 1.0.190 + const setBelow2000ft = (radioHeight1.value < 2000 && !radioHeight1Invalid) || (radioHeight2.value < 2000 && !radioHeight2Invalid); + const resetBelow2000ft = (radioHeight1.value > 2200 || radioHeight1Invalid) && (radioHeight2.value > 2200 || radioHeight2Invalid); + const memo2 = this.memoLdgMemo_below2000ft.write(setBelow2000ft, resetBelow2000ft); + + const setInhibitMemo = this.memoLdgMemo_conf01.write(resetBelow2000ft && !radioHeight1Invalid && !radioHeight2Invalid, _deltaTime); + const resetInhibitMemo = !(this.flightPhase === 7 || this.flightPhase === 8 || this.flightPhase === 6); + const memo1 = this.memoLdgMemo_inhibit.write(setInhibitMemo, resetInhibitMemo); + + const showInApproach = memo1 && memo2 && this.flightPhase === 6; + + const invalidRadioMemo = this.memoLdgMemo_conf02.write(radioHeight1Invalid && radioHeight2Invalid && gearDownlocked && this.flightPhase === 6, _deltaTime); + + this.ldgMemo = showInApproach || invalidRadioMemo || this.flightPhase === 8 || this.flightPhase === 7; + SimVar.SetSimVarValue('L:A32NX_FWC_LDGMEMO', 'Bool', this.ldgMemo); + } + + _updateAltitudeWarning() { + const indicatedAltitude = Simplane.getAltitude(); + const shortAlert = SimVar.GetSimVarValue('L:A32NX_ALT_DEVIATION_SHORT', 'Bool'); + if (shortAlert === 1) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION_SHORT', 'Bool', false); + } + + if (this.warningPressed === true) { + this._wasBellowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + return; + } + + if (Simplane.getIsGrounded()) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + } + + // Use FCU displayed value + const currentAltitudeConstraint = SimVar.GetSimVarValue('L:A32NX_FG_ALTITUDE_CONSTRAINT', 'feet'); + const currentFCUAltitude = SimVar.GetSimVarValue('AUTOPILOT ALTITUDE LOCK VAR:3', 'feet'); + const targetAltitude = currentAltitudeConstraint && !this.hasAltitudeConstraint() ? currentAltitudeConstraint : currentFCUAltitude; + + // Exit when selected altitude is being changed + if (this.previousTargetAltitude !== targetAltitude) { + this.previousTargetAltitude = targetAltitude; + this._wasBellowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._wasReach200ft = false; + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION_SHORT', 'Bool', false); + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + return; + } + + // Exit when: + // - Landing gear down & slats extended + // - Glide slope captured + // - Landing locked down + + const landingGearIsDown = SimVar.GetSimVarValue('L:A32NX_FLAPS_HANDLE_INDEX', 'Enum') >= 1 && SimVar.GetSimVarValue('L:A32NX_GEAR_HANDLE_POSITION', 'Percent over 100') > 0.5; + const verticalMode = SimVar.GetSimVarValue('L:A32NX_FMA_VERTICAL_MODE', 'Number'); + const glideSlopeCaptured = verticalMode >= 30 && verticalMode <= 34; + const landingGearIsLockedDown = SimVar.GetSimVarValue('GEAR POSITION:0', 'Enum') > 0.9; + const isTcasResolutionAdvisoryActive = SimVar.GetSimVarValue('L:A32NX_TCAS_STATE', 'Enum') > 1; + if (landingGearIsDown || glideSlopeCaptured || landingGearIsLockedDown || isTcasResolutionAdvisoryActive) { + this._wasBellowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._wasReach200ft = false; + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION_SHORT', 'Bool', false); + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + return; + } + + const delta = Math.abs(indicatedAltitude - targetAltitude); + + if (delta < 200) { + this._wasBellowThreshold = true; + this._wasAboveThreshold = false; + this._wasReach200ft = true; + } + if (delta > 750) { + this._wasAboveThreshold = true; + this._wasBellowThreshold = false; + } + if (delta >= 200 && delta <= 750) { + this._wasInRange = true; + } + + if (this._wasBellowThreshold && this._wasReach200ft) { + if (delta >= 200) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', true); + } else if (delta < 200) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + } + } else if (this._wasAboveThreshold && delta <= 750 && !this._wasReach200ft) { + if (!SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_1_ACTIVE', 'Bool') && !SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_2_ACTIVE', 'Bool')) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION_SHORT', 'Bool', true); + } + } else if (delta > 750 && this._wasInRange && !this._wasReach200ft) { + if (delta > 750) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', true); + } else if (delta >= 750) { + SimVar.SetSimVarValue('L:A32NX_ALT_DEVIATION', 'Bool', false); + } + } + } + + hasAltitudeConstraint() { + if (Simplane.getAutoPilotAltitudeManaged() && SimVar.GetSimVarValue('L:AP_CURRENT_TARGET_ALTITUDE_IS_CONSTRAINT', 'number') !== 0) { + return false; + } + return true; + } +} + +/* + * This file contains various nodes that can be used for logical processing. Systems like the FWC may use them to + * accurately implement their functionality. + */ + +/** + * The following class represents a monostable circuit. It is inspired by the MTRIG nodes as described in the ESLD and + * used by the FWC. + * When it detects either a rising or a falling edge (depending on it's type) it will emit a signal for a certain time t + * after the detection. It is not retriggerable, so a rising/falling edge within t will not reset the timer. + */ +class NXLogic_TriggeredMonostableNode { + t: number; + + risingEdge: boolean; + + _timer: number; + + _previousValue: boolean; + + constructor(t: number, risingEdge = true) { + this.t = t; + this.risingEdge = risingEdge; + this._timer = 0; + this._previousValue = null; + } + + write(value: boolean, _deltaTime: number) { + if (this._previousValue === null && SimVar.GetSimVarValue('L:A32NX_FWC_SKIP_STARTUP', 'Bool')) { + this._previousValue = value; + } + if (this.risingEdge) { + if (this._timer > 0) { + this._timer = Math.max(this._timer - _deltaTime / 1000, 0); + this._previousValue = value; + return true; + } if (!this._previousValue && value) { + this._timer = this.t; + this._previousValue = value; + return true; + } + } else { + if (this._timer > 0) { + this._timer = Math.max(this._timer - _deltaTime / 1000, 0); + this._previousValue = value; + return true; + } if (this._previousValue && !value) { + this._timer = this.t; + this._previousValue = value; + return true; + } + } + this._previousValue = value; + return false; + } +} + +/** + * The following class represents a "confirmation" circuit, which only passes a signal once it has been stable for a + * certain amount of time. It is inspired by the CONF nodes as described in the ESLD and used by the FWC. + * When it detects either a rising or falling edge (depending on it's type) it will wait for up to time t and emit the + * incoming signal if it was stable throughout t. If at any point the signal reverts during t the state is fully reset, + * and the original signal will be emitted again. + */ +class NXLogic_ConfirmNode { + t: number; + + risingEdge: boolean; + + _timer: number; + + _previousInput: boolean; + + _previousOutput: boolean; + + constructor(t: number, risingEdge = true) { + this.t = t; + this.risingEdge = risingEdge; + this._timer = 0; + this._previousInput = null; + this._previousOutput = null; + } + + write(value: boolean, _deltaTime: number) { + if (this._previousInput === null && SimVar.GetSimVarValue('L:A32NX_FWC_SKIP_STARTUP', 'Bool')) { + this._previousInput = value; + this._previousOutput = value; + } + if (this.risingEdge) { + if (!value) { + this._timer = 0; + } else if (this._timer > 0) { + this._timer = Math.max(this._timer - _deltaTime / 1000, 0); + this._previousInput = value; + this._previousOutput = !value; + return !value; + } else if (!this._previousInput && value) { + this._timer = this.t; + this._previousInput = value; + this._previousOutput = !value; + return !value; + } + } else if (value) { + this._timer = 0; + } else if (this._timer > 0) { + this._timer = Math.max(this._timer - _deltaTime / 1000, 0); + this._previousInput = value; + this._previousOutput = !value; + return !value; + } else if (this._previousInput && !value) { + this._timer = this.t; + this._previousInput = value; + this._previousOutput = !value; + return !value; + } + this._previousInput = value; + this._previousOutput = value; + return value; + } + + read() { + return this._previousOutput; + } +} + +/** + * The following class represents a flip-flop or memory circuit that can be used to store a single bit. It is inspired + * by the S+R nodes as described in the ESLD. + * It has two inputs: Set and Reset. At first it will always emit a falsy value, until it receives a signal on the set + * input, at which point it will start emitting a truthy value. This will continue until a signal is received on the + * reset input, at which point it reverts to the original falsy output. It a signal is sent on both set and reset at the + * same time, the input with a star will have precedence. + * The NVM flag is not implemented right now but can be used to indicate non-volatile memory storage, which means the + * value will persist even when power is lost and subsequently restored. + */ +class NXLogic_MemoryNode { + setStar: boolean; + + nvm: boolean; + + _value: boolean; + + /** + * @param setStar Whether set has precedence over reset if both are applied simultaneously. + * @param nvm Whether the is non-volatile and will be kept even when power is lost. + */ + constructor(setStar = true, nvm = false) { + this.setStar = setStar; + this.nvm = nvm; // TODO in future, reset non-nvm on power cycle + this._value = false; + } + + write(set, reset) { + if (set && reset) { + this._value = this.setStar; + } else if (set && !this._value) { + this._value = true; + } else if (reset && this._value) { + this._value = false; + } + return this._value; + } + + read() { + return this._value; + } +} diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts b/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts new file mode 100644 index 00000000000..5049cdde4d2 --- /dev/null +++ b/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts @@ -0,0 +1,791 @@ +import { Arinc429Word, NXDataStore } from '@flybywiresim/fbw-sdk'; +import { FmgcFlightPhase } from '@shared/flightphase'; +import { LegacySoundManager, soundList } from 'systems-host/systems/LegacySoundManager'; +import { RadioAutoCallOutFlags } from '../../../../../fbw-a32nx/src/systems/shared/src/AutoCallOuts'; + +/** The default (Airbus basic configuration) radio altitude auto call outs. */ +const DEFAULT_RADIO_AUTO_CALL_OUTS = RadioAutoCallOutFlags.TwoThousandFiveHundred | RadioAutoCallOutFlags.OneThousand | RadioAutoCallOutFlags.FourHundred + | RadioAutoCallOutFlags.ThreeHundred | RadioAutoCallOutFlags.TwoHundred | RadioAutoCallOutFlags.OneHundred + | RadioAutoCallOutFlags.Fifty | RadioAutoCallOutFlags.Forty | RadioAutoCallOutFlags.Thirty | RadioAutoCallOutFlags.Twenty + | RadioAutoCallOutFlags.Ten | RadioAutoCallOutFlags.Five; + +type ModesType = { + current: number; + previous: number; + type: any[]; + onChange?: (arg0: number, arg1: number) => void; +} + +/** + * This 1:1 port from the A32NX's GPWS+FWS serves as temporary replacement, until a more sophisticated system simulation is in place. + * After merge of PR #4872 (https://github.com/flybywiresim/aircraft/pull/4872) (intended for A32NX), the FWS architecture has to + * be ported to the A380X, then the FWS callout parts of this class can be removed. + */ +export class LegacyGpws { + autoCallOutPins: number; + + minimumsState = 0; + + Mode3MaxBaroAlt: number; + + Mode4MaxRAAlt: number; + + Mode2BoundaryLeaveAlt: number; + + Mode2NumTerrain: number; + + Mode2NumFramesInBoundary: number; + + RadioAltRate: number; + + prevRadioAlt: number; + + prevRadioAlt2: number; + + modes: ModesType[]; + + PrevShouldPullUpPlay: boolean; + + AltCallState: LegacyStateMachine; + + RetardState: LegacyStateMachine; + + // eslint-disable-next-line camelcase + constructor(private soundManager: LegacySoundManager) { + this.autoCallOutPins = DEFAULT_RADIO_AUTO_CALL_OUTS; + + this.minimumsState = 0; + + this.Mode3MaxBaroAlt = NaN; + + this.Mode4MaxRAAlt = NaN; + + this.Mode2BoundaryLeaveAlt = NaN; + this.Mode2NumTerrain = 0; + this.Mode2NumFramesInBoundary = 0; + + this.RadioAltRate = NaN; + this.prevRadioAlt = NaN; + this.prevRadioAlt2 = NaN; + + this.modes = [ + // Mode 1 + { + // 0: no warning, 1: "sink rate", 2 "pull up" + current: 0, + previous: 0, + type: [ + {}, + { sound: soundList.sink_rate, soundPeriod: 1.1, gpwsLight: true }, + { gpwsLight: true, pullUp: true }, + ], + }, + // Mode 2 is currently inactive. + { + // 0: no warning, 1: "terrain", 2: "pull up" + current: 0, + previous: 0, + type: [{}, { gpwsLight: true }, { gpwsLight: true, pullUp: true }], + }, + // Mode 3 + { + // 0: no warning, 1: "don't sink" + current: 0, + previous: 0, + type: [{}, { sound: soundList.dont_sink, soundPeriod: 1.1, gpwsLight: true }], + }, + // Mode 4 + { + // 0: no warning, 1: "too low gear", 2: "too low flaps", 3: "too low terrain" + current: 0, + previous: 0, + type: [ + {}, + { sound: soundList.too_low_gear, soundPeriod: 1.1, gpwsLight: true }, + { sound: soundList.too_low_flaps, soundPeriod: 1.1, gpwsLight: true }, + { sound: soundList.too_low_terrain, soundPeriod: 1.1, gpwsLight: true }, + ], + }, + // Mode 5, not all warnings are fully implemented + { + // 0: no warning, 1: "glideslope", 2: "hard glideslope" (louder) + current: 0, + previous: 0, + type: [ + {}, + {}, + {}, + ], + onChange: (current) => { + SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', current >= 1); + }, + }, + ]; + + this.PrevShouldPullUpPlay = false; + + this.AltCallState = createStateMachine(AltCallStateMachine); + this.AltCallState.setState('ground'); + this.RetardState = createStateMachine(RetardStateMachine); + this.RetardState.setState('landed'); + } + + init() { + console.log('A32NX_GPWS init'); + + SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', 0); + SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', 0); + + // eslint-disable-next-line max-len + NXDataStore.getAndSubscribe('CONFIG_A32NX_FWC_RADIO_AUTO_CALL_OUT_PINS', (k, v) => k === 'CONFIG_A32NX_FWC_RADIO_AUTO_CALL_OUT_PINS' && (this.autoCallOutPins = Number(v)), DEFAULT_RADIO_AUTO_CALL_OUTS.toString()); + } + + update(deltaTime) { + this.gpws(deltaTime); + } + + gpws(deltaTime) { + // EGPWS receives ADR1 only + const baroAlt = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_BARO_CORRECTED_ALTITUDE_1'); + 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'); + + 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 ( + radioAltValid && radioAlt.value >= 10 && radioAlt.value <= 2450 + && !SimVar.GetSimVarValue('L:A32NX_GPWS_SYS_OFF', 'Bool') + ) { // Activate between 10 - 2450 radio alt unless SYS is off + const FlapPushButton = SimVar.GetSimVarValue('L:A32NX_GPWS_FLAPS3', 'Bool'); + const FlapPosition = SimVar.GetSimVarValue('L:A32NX_FLAPS_HANDLE_INDEX', 'Number'); + const FlapsInLandingConfig = FlapPushButton ? (FlapPosition === 3) : (FlapPosition === 4); + const vSpeed = Simplane.getVerticalSpeed(); + const Airspeed = SimVar.GetSimVarValue('AIRSPEED INDICATED', 'Knots'); + const gearExtended = SimVar.GetSimVarValue('GEAR TOTAL PCT EXTENDED', 'Percent') > 0.9; + + this.updateMaxRA(radioAlt.value, onGround, phase); + + this.GPWSMode1(this.modes[0], radioAlt.value, vSpeed); + // 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.GPWSMode5(this.modes[4], radioAlt.value); + } else { + this.modes.forEach((mode) => { + mode.current = 0; + }); + + this.Mode3MaxBaroAlt = NaN; + if (onGround || (radioAltValid && radioAlt.value < 10)) { + this.Mode4MaxRAAlt = NaN; + } + + SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', 0); + SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', 0); + } + + this.GPWSComputeLightsAndCallouts(); + + if ((mda !== 0 || (dh !== -1 && dh !== -2) && phase === FmgcFlightPhase.Approach)) { + let minimumsDA; // MDA or DH + let minimumsIA; // radio or baro altitude + if (dh >= 0) { + minimumsDA = dh; + minimumsIA = radioAlt.isNormalOperation() || radioAlt.isFunctionalTest() ? radioAlt.value : NaN; + } else { + minimumsDA = mda; + minimumsIA = baroAlt.isNormalOperation() || baroAlt.isFunctionalTest() ? baroAlt.value : NaN; + } + if (Number.isFinite(minimumsDA) && Number.isFinite(minimumsIA)) { + this.gpwsMinimums(minimumsDA, minimumsIA); + } + } + } + + /** + * Takes the derivative of the radio altimeter. Using central difference, to prevent high frequency noise + * @param radioAlt - in feet + * @param deltaTime - in milliseconds + */ + differentiateRadioalt(radioAlt, deltaTime) { + if (!Number.isNaN(this.prevRadioAlt2) && !Number.isNaN(radioAlt)) { + this.RadioAltRate = (radioAlt - this.prevRadioAlt2) / (deltaTime / 1000 / 60) / 2; + this.prevRadioAlt2 = this.prevRadioAlt; + this.prevRadioAlt = radioAlt; + } else if (!Number.isNaN(this.prevRadioAlt) && !Number.isNaN(radioAlt)) { + this.prevRadioAlt2 = this.prevRadioAlt; + this.prevRadioAlt = radioAlt; + } else { + this.prevRadioAlt2 = radioAlt; + } + } + + updateMaxRA(radioAlt, onGround, phase) { + // 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; + } + } + + gpwsMinimums(minimumsDA, minimumsIA) { + let over100Above = false; + let overMinimums = false; + + if (minimumsDA <= 90) { + overMinimums = minimumsIA >= minimumsDA + 15; + over100Above = minimumsIA >= minimumsDA + 115; + } else { + overMinimums = minimumsIA >= minimumsDA + 5; + over100Above = minimumsIA >= minimumsDA + 105; + } + if (this.minimumsState === 0 && overMinimums) { + this.minimumsState = 1; + } else if (this.minimumsState === 1 && over100Above) { + this.minimumsState = 2; + } else if (this.minimumsState === 2 && !over100Above) { + this.soundManager.tryPlaySound(soundList.hundred_above); + this.minimumsState = 1; + } else if (this.minimumsState === 1 && !overMinimums) { + this.soundManager.tryPlaySound(soundList.minimums); + this.minimumsState = 0; + } + } + + GPWSComputeLightsAndCallouts() { + this.modes.forEach((mode) => { + if (mode.current === mode.previous) { + return; + } + + const previousType = mode.type[mode.previous]; + this.soundManager.removePeriodicSound(previousType.sound); + + const currentType = mode.type[mode.current]; + this.soundManager.addPeriodicSound(currentType.sound, currentType.soundPeriod); + + if (mode.onChange) { + mode.onChange(mode.current, mode.previous); + } + + mode.previous = mode.current; + }); + + const activeTypes = this.modes.map((mode) => mode.type[mode.current]); + + const shouldPullUpPlay = activeTypes.some((type) => type.pullUp); + if (shouldPullUpPlay !== this.PrevShouldPullUpPlay) { + if (shouldPullUpPlay) { + this.soundManager.addPeriodicSound(soundList.pull_up, 1.1); + } else { + this.soundManager.removePeriodicSound(soundList.pull_up); + } + this.PrevShouldPullUpPlay = shouldPullUpPlay; + } + + const illuminateGpwsLight = activeTypes.some((type) => type.gpwsLight); + SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', illuminateGpwsLight); + } + + /** + * 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 + */ + GPWSMode1(mode, radioAlt, vSpeed) { + const sinkrate = -vSpeed; + + if (sinkrate <= 1000) { + mode.current = 0; + return; + } + + const maxSinkrateAlt = 0.61 * sinkrate - 600; + const maxPullUpAlt = sinkrate < 1700 ? 1.3 * sinkrate - 1940 : 0.4 * sinkrate - 410; + + if (radioAlt <= maxPullUpAlt) { + mode.current = 2; + } else if (radioAlt <= maxSinkrateAlt) { + mode.current = 1; + } else { + mode.current = 0; + } + } + + /** + * 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 gearExtended - If the gear is deployed + */ + GPWSMode2(mode, radioAlt, speed, FlapsInLandingConfig, gearExtended) { + 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)); + + if (!FlapsInLandingConfig && -this.RadioAltRate > 2000) { + if (radioAlt < UpperBoundarySpeed && radioAlt < UpperBoundaryRate) { + this.Mode2NumFramesInBoundary += 1; + } else { + this.Mode2NumFramesInBoundary = 0; + } + } else if (FlapsInLandingConfig && -this.RadioAltRate > 2000) { + if (radioAlt < 775 && radioAlt < UpperBoundaryRate && -this.RadioAltRate < 10000) { + this.Mode2NumFramesInBoundary += 1; + } else { + this.Mode2NumFramesInBoundary = 0; + } + } + // This is to prevent very quick changes in radio alt rate triggering the alarm. The derivative is sadly pretty jittery. + if (this.Mode2NumFramesInBoundary > 5) { + IsInBoundary = true; + } + + if (IsInBoundary) { + this.Mode2BoundaryLeaveAlt = -1; + if (this.Mode2NumTerrain < 2 || gearExtended) { + if (this.soundManager.tryPlaySound(soundList.too_low_terrain)) { // too low terrain is not correct, but no "terrain" call yet + this.Mode2NumTerrain += 1; + } + mode.current = 1; + } else if (!gearExtended) { + mode.current = 2; + } + } else if (this.Mode2BoundaryLeaveAlt === -1) { + this.Mode2BoundaryLeaveAlt = radioAlt; + } else if (this.Mode2BoundaryLeaveAlt + 300 > radioAlt) { + mode.current = 1; + this.soundManager.tryPlaySound(soundList.too_low_terrain); + } else if (this.Mode2BoundaryLeaveAlt + 300 <= radioAlt) { + mode.current = 0; + this.Mode2NumTerrain = 0; + this.Mode2BoundaryLeaveAlt = NaN; + } + } + + /** + * 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 + */ + GPWSMode3(mode, radioAlt, phase) { + if (!(phase === FmgcFlightPhase.Takeoff || phase === FmgcFlightPhase.GoAround) || radioAlt > 1500 || radioAlt < 10) { + this.Mode3MaxBaroAlt = NaN; + mode.current = 0; + return; + } + + const baroAlt = SimVar.GetSimVarValue('PLANE ALTITUDE', 'feet'); + + const maxAltLoss = 0.09 * radioAlt + 7.1; + + if (baroAlt > this.Mode3MaxBaroAlt || Number.isNaN(this.Mode3MaxBaroAlt)) { + this.Mode3MaxBaroAlt = baroAlt; + mode.current = 0; + } else if ((this.Mode3MaxBaroAlt - baroAlt) > maxAltLoss) { + mode.current = 1; + } else { + mode.current = 0; + } + } + + /** + * 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 gearExtended - If the gear is extended + * @param phase - Flight phase index + * @constructor + */ + GPWSMode4(mode, radioAlt, speed, FlapsInLandingConfig, gearExtended, phase) { + if (radioAlt < 30 || radioAlt > 1000) { + mode.current = 0; + 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; + } + } 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; + } + } 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; + + if (this.Mode4MaxRAAlt > 100 && radioAlt < maxWarnAltSpeed && radioAlt < maxWarnAlt) { + mode.current = 3; + } + } + } + + /** + * Compute the GPWS Mode 5 state. + * @param mode - The mode object which stores the state. + * @param - radioAlt Radio altitude in feet + * @constructor + */ + GPWSMode5(mode, radioAlt) { + if (radioAlt > 1000 || radioAlt < 30 || SimVar.GetSimVarValue('L:A32NX_GPWS_GS_OFF', 'Bool')) { + mode.current = 0; + return; + } + if (!SimVar.GetSimVarValue('L:A32NX_RADIO_RECEIVER_GS_IS_VALID', 'number')) { + mode.current = 0; + return; + } + const error = SimVar.GetSimVarValue('L:A32NX_RADIO_RECEIVER_GS_DEVIATION', 'number'); + const dots = -error * 2.5; // According to the FCOM, one dot is approx. 0.4 degrees. 1/0.4 = 2.5 + + const minAltForWarning = dots < 2.9 ? -75 * dots + 247.5 : 30; + const minAltForHardWarning = dots < 3.8 ? -66.66 * dots + 283.33 : 30; + + if (dots > 2 && radioAlt > minAltForHardWarning && radioAlt < 350) { + mode.current = 2; + } else if (dots > 1.3 && radioAlt > minAltForWarning) { + mode.current = 1; + } else { + mode.current = 0; + } + } + + UpdateAltState(radioAlt) { + if (Number.isNaN(radioAlt)) { + return; + } + switch (this.AltCallState.value) { + case 'ground': + if (radioAlt > 6) { + this.AltCallState.action('up'); + } + break; + case 'over5': + if (radioAlt > 12) { + this.AltCallState.action('up'); + } else if (radioAlt <= 6) { + if (this.RetardState.value !== 'retardPlaying' && (this.autoCallOutPins & RadioAutoCallOutFlags.Five)) { + this.soundManager.tryPlaySound(soundList.alt_5); + } + this.AltCallState.action('down'); + } + break; + case 'over10': + if (radioAlt > 22) { + this.AltCallState.action('up'); + } else if (radioAlt <= 12) { + if (this.RetardState.value !== 'retardPlaying' && (this.autoCallOutPins & RadioAutoCallOutFlags.Ten)) { + this.soundManager.tryPlaySound(soundList.alt_10); + } + this.AltCallState.action('down'); + } + break; + case 'over20': + if (radioAlt > 32) { + this.AltCallState.action('up'); + } else if (radioAlt <= 22) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.Twenty) { + this.soundManager.tryPlaySound(soundList.alt_20); + } + this.AltCallState.action('down'); + } + break; + case 'over30': + if (radioAlt > 42) { + this.AltCallState.action('up'); + } else if (radioAlt <= 32) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.Thirty) { + this.soundManager.tryPlaySound(soundList.alt_30); + } + this.AltCallState.action('down'); + } + break; + case 'over40': + if (radioAlt > 53) { + this.AltCallState.action('up'); + } else if (radioAlt <= 42) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.Forty) { + this.soundManager.tryPlaySound(soundList.alt_40); + } + this.AltCallState.action('down'); + } + break; + case 'over50': + if (radioAlt > 110) { + this.AltCallState.action('up'); + } else if (radioAlt <= 53) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.Fifty) { + this.soundManager.tryPlaySound(soundList.alt_50); + } + this.AltCallState.action('down'); + } + break; + case 'over100': + if (radioAlt > 210) { + this.AltCallState.action('up'); + } else if (radioAlt <= 110) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.OneHundred) { + this.soundManager.tryPlaySound(soundList.alt_100); + } + this.AltCallState.action('down'); + } + break; + case 'over200': + if (radioAlt > 310) { + this.AltCallState.action('up'); + } else if (radioAlt <= 210) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.TwoHundred) { + this.soundManager.tryPlaySound(soundList.alt_200); + } + this.AltCallState.action('down'); + } + break; + case 'over300': + if (radioAlt > 410) { + this.AltCallState.action('up'); + } else if (radioAlt <= 310) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.ThreeHundred) { + this.soundManager.tryPlaySound(soundList.alt_300); + } + this.AltCallState.action('down'); + } + break; + case 'over400': + if (radioAlt > 513) { + this.AltCallState.action('up'); + } else if (radioAlt <= 410) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.FourHundred) { + this.soundManager.tryPlaySound(soundList.alt_400); + } + this.AltCallState.action('down'); + } + break; + case 'over500': + if (radioAlt > 1020) { + this.AltCallState.action('up'); + } else if (radioAlt <= 513) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.FiveHundred) { + this.soundManager.tryPlaySound(soundList.alt_500); + } + this.AltCallState.action('down'); + } + break; + case 'over1000': + if (radioAlt > 2020) { + this.AltCallState.action('up'); + } else if (radioAlt <= 1020) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.OneThousand) { + this.soundManager.tryPlaySound(soundList.alt_1000); + } + this.AltCallState.action('down'); + } + break; + case 'over2000': + if (radioAlt > 2530) { + this.AltCallState.action('up'); + } else if (radioAlt <= 2020) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.TwoThousand) { + this.soundManager.tryPlaySound(soundList.alt_2000); + } + this.AltCallState.action('down'); + } + break; + case 'over2500': + if (radioAlt <= 2530) { + if (this.autoCallOutPins & RadioAutoCallOutFlags.TwoThousandFiveHundred) { + this.soundManager.tryPlaySound(soundList.alt_2500); + } else if (this.autoCallOutPins & RadioAutoCallOutFlags.TwentyFiveHundred) { + this.soundManager.tryPlaySound(soundList.alt_2500b); + } + this.AltCallState.action('down'); + } + break; + default: + break; + } + + switch (this.RetardState.value) { + case 'overRetard': + if (radioAlt < 20) { + if (!SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_ACTIVE', 'Bool')) { + this.RetardState.action('play'); + this.soundManager.addPeriodicSound(soundList.retard, 1.1); + } else if (radioAlt < 10) { + this.RetardState.action('play'); + this.soundManager.addPeriodicSound(soundList.retard, 1.1); + } + } + break; + case 'retardPlaying': + if (SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:1', 'number') < 2.6 || SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:2', 'number') < 2.6) { + this.RetardState.action('land'); + this.soundManager.removePeriodicSound(soundList.retard); + } else if (SimVar.GetSimVarValue('L:A32NX_FMGC_FLIGHT_PHASE', 'Enum') === FmgcFlightPhase.GoAround || radioAlt > 20) { + this.RetardState.action('go_around'); + this.soundManager.removePeriodicSound(soundList.retard); + } + break; + case 'landed': + if (radioAlt > 20) { + this.RetardState.action('up'); + } + break; + default: + break; + } + } +} + +const RetardStateMachine = { + overRetard: { transitions: { play: { target: 'retardPlaying' } } }, + retardPlaying: { + transitions: { + land: { target: 'landed' }, + go_around: { target: 'overRetard' }, + }, + }, + landed: { transitions: { up: { target: 'overRetard' } } }, +}; + +const AltCallStateMachine = { + init: 'ground', + over2500: { transitions: { down: { target: 'over2000' } } }, + over2000: { + transitions: { + down: { target: 'over1000' }, + up: { target: 'over2500' }, + }, + }, + over1000: { + transitions: { + down: { target: 'over500' }, + up: { target: 'over2000' }, + }, + }, + over500: { + transitions: { + down: { target: 'over400' }, + up: { target: 'over1000' }, + }, + }, + over400: { + transitions: { + down: { target: 'over300' }, + up: { target: 'over500' }, + }, + }, + over300: { + transitions: { + down: { target: 'over200' }, + up: { target: 'over400' }, + }, + }, + over200: { + transitions: { + down: { target: 'over100' }, + up: { target: 'over300' }, + }, + }, + over100: { + transitions: { + down: { target: 'over50' }, + up: { target: 'over200' }, + }, + }, + over50: { + transitions: { + down: { target: 'over40' }, + up: { target: 'over100' }, + }, + }, + over40: { + transitions: { + down: { target: 'over30' }, + up: { target: 'over50' }, + }, + }, + over30: { + transitions: { + down: { target: 'over20' }, + up: { target: 'over40' }, + }, + }, + over20: { + transitions: { + down: { target: 'over10' }, + up: { target: 'over30' }, + }, + }, + over10: { + transitions: { + down: { target: 'over5' }, + up: { target: 'over20' }, + }, + }, + over5: { + transitions: { + down: { target: 'ground' }, + up: { target: 'over10' }, + }, + }, + ground: { transitions: { up: { target: 'over5' } } }, +}; + +type LegacyStateMachine = { + value: string; + action(event: string): void; + setState(newState: string): void; +}; + +const createStateMachine = (machineDef: any): LegacyStateMachine => { + const machine = { + value: machineDef.init, + action(event: string) { + const currStateDef = machineDef[machine.value]; + const destTransition = currStateDef.transitions[event]; + if (!destTransition) { + return; + } + const destState = destTransition.target; + + machine.value = destState; + }, + setState(newState: string) { + const valid = machineDef[newState]; + if (valid) { + machine.value = newState; + } + }, + }; + return machine; +}; diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts b/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts new file mode 100644 index 00000000000..87db4ebc28a --- /dev/null +++ b/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts @@ -0,0 +1,205 @@ +class PeriodicSound { + timeSinceLastPlayed: number; + + constructor(public sound: LegacySound, public period: number) { + this.sound = sound; + this.period = period; + this.timeSinceLastPlayed = NaN; + } +} + +export class LegacySoundManager { + periodicList: PeriodicSound[]; + + soundQueue: LegacySound[]; + + playingSound: LegacySound; + + playingSoundRemaining: number; + + constructor() { + this.periodicList = []; + + this.playingSound = null; + this.playingSoundRemaining = NaN; + } + + addPeriodicSound(sound: LegacySound, period = NaN) { + if (!sound) { + return; + } + + let useLengthForPeriod = false; + if (period < sound.length) { + console.error("A32NXSoundManager ERROR: Sound period can't be smaller than sound length. Using sound length instead."); + useLengthForPeriod = true; + } + + let found = false; + this.periodicList.forEach((element) => { + if (element.sound.name === sound.name) { + found = true; + } + }); + + if (!found) { + this.periodicList.push(new PeriodicSound(sound, useLengthForPeriod ? sound.length : period)); + } + } + + removePeriodicSound(sound: LegacySound) { + if (!sound) { + return; + } + + for (let i = 0; i < this.periodicList.length; i++) { + if (this.periodicList[i].sound.name === sound.name) { + this.periodicList.splice(i, 1); + } + } + } + + tryPlaySound(sound: LegacySound, retry = false, repeatOnce = false) { + if (this.playingSound === null) { + this.playingSound = sound; + this.playingSoundRemaining = sound.length; + + Coherent.call('PLAY_INSTRUMENT_SOUND', sound.name).catch(console.error); + if (repeatOnce) { + this.soundQueue.push(sound); + } + return true; + } if (retry) { + this.soundQueue.push(sound); + if (repeatOnce) { + this.soundQueue.push(sound); + } + return false; + } + return false; + } + + update(deltaTime: number) { + if (this.playingSoundRemaining <= 0) { + this.playingSound = null; + this.playingSoundRemaining = NaN; + } else if (this.playingSoundRemaining > 0) { + this.playingSoundRemaining -= deltaTime / 1000; + } + + this.periodicList.forEach((element) => { + if (Number.isNaN(element.timeSinceLastPlayed) || element.timeSinceLastPlayed >= element.period) { + if (this.tryPlaySound(element.sound)) { + element.timeSinceLastPlayed = 0; + } + } else { + element.timeSinceLastPlayed += deltaTime / 1000; + } + }); + } +} + +// many lengths are approximate until we can get them accuratly (when boris re-makes them and we have the sources) +interface LegacySound { + name: string; + length: number; +} + +export const soundList: Record = { + pull_up: { + name: 'aural_pullup_new', + length: 0.9, + }, + sink_rate: { + name: 'aural_sink_rate_new', + length: 0.9, + }, + dont_sink: { + name: 'aural_dontsink_new', + length: 0.9, + }, + too_low_gear: { + name: 'aural_too_low_gear', + length: 0.8, + }, + too_low_flaps: { + name: 'aural_too_low_flaps', + length: 0.8, + }, + too_low_terrain: { + name: 'aural_too_low_terrain', + length: 0.9, + }, + minimums: { + name: 'aural_minimumnew', + length: 0.67, + }, + hundred_above: { + name: 'aural_100above', + length: 0.72, + }, + retard: { + name: 'new_retard', + length: 0.9, + }, + alt_2500: { + name: 'new_2500', + length: 1.1, + }, + alt_2500b: { + name: 'new_2_500', + length: 1.047, + }, + alt_2000: { + name: 'new_2000', + length: 0.72, + }, + alt_1000: { + name: 'new_1000', + length: 0.9, + }, + alt_500: { + name: 'new_500', + length: 0.6, + }, + alt_400: { + name: 'new_400', + length: 0.6, + }, + alt_300: { + name: 'new_300', + length: 0.6, + }, + alt_200: { + name: 'new_200', + length: 0.6, + }, + alt_100: { + name: 'new_100', + length: 0.6, + }, + alt_50: { + name: 'new_50', + length: 0.4, + }, + alt_40: { + name: 'new_40', + length: 0.4, + }, + alt_30: { + name: 'new_30', + length: 0.4, + }, + alt_20: { + name: 'new_20', + length: 0.4, + }, + alt_10: { + name: 'new_10', + length: 0.3, + }, + alt_5: { + name: 'new_5', + length: 0.3, + }, +}; diff --git a/fbw-a380x/src/systems/systems-host/tsconfig.json b/fbw-a380x/src/systems/systems-host/tsconfig.json index cf30496d09a..72df227efd5 100644 --- a/fbw-a380x/src/systems/systems-host/tsconfig.json +++ b/fbw-a380x/src/systems/systems-host/tsconfig.json @@ -7,7 +7,7 @@ "@datalink/common": ["../../../fbw-common/src/systems/datalink/common/src/index.ts"], "@datalink/router": ["../../../fbw-common/src/systems/datalink/router/src/index.ts"], "@failures": ["./failures/src/index.ts"], - "@fmgc/*": ["./fmgc/src/*"], + "@fmgc/*": ["../../../fbw-a32nx/src/systems/fmgc/src/*"], "@instruments/common/*": ["./instruments/src/Common/*"], "@localization/*": ["../localization/*"], "@sentry/*": ["./sentry-client/src/*"],