diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index c76d696710d..27d3ea96b20 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -73,6 +73,9 @@ 1. [FMS] Fix Pause at T/D not working in selected speed - @BlueberryKing (BlueberryKing) 1. [MISC] Replaced brake temperature simulation with physics based model of brakes - @Gurgel100 (Pascal) 1. [A32NX/MCDU] Suppress TMPY FPLN when no modifications made in airways page - @robertxing2004 (robeet) +1. [A380X/FWS] No "auto brake off" callout when double pressing A/THR instinctive disconnect - @flogross89 (floridude) +1. [A380X/FWS] Improve landing capability downgrade triple click logic (#9008) - @flogross89 (floridude) +1. [A32NX/FWS] Add "auto brake off" callout, improve AP/ATHR disconnect aural alert logic - @flogross89 (floridude) 1. [A32NX/FCU] The FCU LCD is now still visible with ambient light when the backlight is off @tracernz (Mike) 1. [A380X/FMC] Fix reset of perf data on done phase or database swap - @tracernz (Mike) 1. [A32NX/ATC] Fixed ATC/TCAS power supply - @tracernz (Mike) diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/sound/sound.xml b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/sound/sound.xml index 1b5aced6220..cd1656bc82e 100644 --- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/sound/sound.xml +++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/sound/sound.xml @@ -596,10 +596,6 @@ - - - - @@ -2150,7 +2146,7 @@ - + @@ -2274,54 +2270,55 @@ + - + - + - - - - - - - + + + + + + + diff --git a/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx b/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx index 0af0ff516e0..fae8018fc37 100644 --- a/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx +++ b/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx @@ -27,6 +27,7 @@ const EWDMessages = { '000002010': ' \x1b<3mFLAPS FULL', '000002011': ' \x1b<3mFLAPS\x1b<5m.....CONF 3', '000002012': ' \x1b<3mFLAPS CONF 3', + '320000001': '\x1b<4mAUTO BRK OFF', '000002201': '\x1b<3mAUTO BRK LO', '000002202': '\x1b<3mAUTO BRK MED', '000002203': '\x1b<3mAUTO BRK MAX', @@ -192,6 +193,11 @@ const EWDMessages = { '216330503': '\x1b<4m -FWD CAB TRIM VALVE', '216330504': '\x1b<4m -AFT CAB TRIM VALVE', '216330505': '\x1b<4m -TRIM AIR HI PR', + '220000001': '\x1b<2mAP OFF', + '220000002': '\x1b<4mA/THR OFF', + '220800001': '\x1b<2m\x1b4mAUTO FLT\x1bm AP OFF', + '220800004': '\x1b<4m\x1b4mAUTO FLT\x1bm A/THR OFF', + '220800005': '\x1b<5m -THR LEVERS........MOVE', '220020201': '\x1b<4m\x1b4mAUTO FLT\x1bm FCU 1+2 FAULT', '220020202': '\x1b<5m -PFD BARO REF: STD ONLY', '220021001': '\x1b<4m\x1b4mAUTO FLT\x1bm FCU 1 FAULT', diff --git a/fbw-a32nx/src/systems/instruments/src/EWD/instrument.tsx b/fbw-a32nx/src/systems/instruments/src/EWD/instrument.tsx index ae54d48dd36..5fcb9206740 100644 --- a/fbw-a32nx/src/systems/instruments/src/EWD/instrument.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EWD/instrument.tsx @@ -2,20 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0 -import { - AdcPublisher, - Clock, - EventBus, - FSComponent, - InstrumentBackplane, - StallWarningPublisher, -} from '@microsoft/msfs-sdk'; +import { AdcPublisher, Clock, EventBus, FSComponent, InstrumentBackplane } from '@microsoft/msfs-sdk'; import { FuelSystemPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/FuelSystemPublisher'; import { ArincValueProvider } from './shared/ArincValueProvider'; import { EwdComponent } from './EWD'; import { EwdSimvarPublisher } from './shared/EwdSimvarPublisher'; -import { A32NXFcuBusPublisher } from '@shared/publishers/A32NXFcuBusPublisher'; -import { PseudoFWC } from './PseudoFWC'; import './style.scss'; @@ -34,12 +25,6 @@ class A32NX_EWD extends BaseInstrument { private readonly adcPublisher = new AdcPublisher(this.bus); - private readonly stallWarningPublisher = new StallWarningPublisher(this.bus, 0.9); - - private readonly a32nxFcuBusPublisher = new A32NXFcuBusPublisher(this.bus); - - private readonly pseudoFwc = new PseudoFWC(this.bus, this); - constructor() { super(); @@ -47,10 +32,6 @@ class A32NX_EWD extends BaseInstrument { this.backplane.addPublisher('SimVars', this.simVarPublisher); this.backplane.addPublisher('FuelSystem', this.fuelSystemPublisher); this.backplane.addPublisher('adc', this.adcPublisher); - this.backplane.addPublisher('stallWarning', this.stallWarningPublisher); - this.backplane.addPublisher('A32NXFcuBus', this.a32nxFcuBusPublisher); - - this.backplane.addInstrument('Fwc', this.pseudoFwc); } get templateID(): string { diff --git a/fbw-a32nx/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx b/fbw-a32nx/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx new file mode 100644 index 00000000000..2a7c0695682 --- /dev/null +++ b/fbw-a32nx/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx @@ -0,0 +1,33 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, SimVarDefinition, SimVarValueType, SimVarPublisher } from '@microsoft/msfs-sdk'; + +export type PseudoFwcSimvars = { + engine1Master: number; + engine2Master: number; + engine3Master: number; + engine4Master: number; + fmgc1DiscreteWord3: number; + fmgc2DiscreteWord3: number; + fmgc1DiscreteWord4: number; + fmgc2DiscreteWord4: number; +}; + +export class PseudoFwcSimvarPublisher extends SimVarPublisher { + private static simvars = new Map([ + ['engine1Master', { name: 'A:FUELSYSTEM VALVE SWITCH:1', type: SimVarValueType.Bool }], + ['engine2Master', { name: 'A:FUELSYSTEM VALVE SWITCH:2', type: SimVarValueType.Bool }], + ['engine3Master', { name: 'A:FUELSYSTEM VALVE SWITCH:3', type: SimVarValueType.Bool }], + ['engine4Master', { name: 'A:FUELSYSTEM VALVE SWITCH:4', type: SimVarValueType.Bool }], + ['fmgc1DiscreteWord3', { name: 'L:A32NX_FMGC_1_DISCRETE_WORD_3', type: SimVarValueType.Number }], + ['fmgc2DiscreteWord3', { name: 'L:A32NX_FMGC_2_DISCRETE_WORD_3', type: SimVarValueType.Number }], + ['fmgc1DiscreteWord4', { name: 'L:A32NX_FMGC_1_DISCRETE_WORD_4', type: SimVarValueType.Number }], + ['fmgc2DiscreteWord4', { name: 'L:A32NX_FMGC_2_DISCRETE_WORD_4', type: SimVarValueType.Number }], + ]); + + public constructor(bus: EventBus) { + super(PseudoFwcSimvarPublisher.simvars, bus); + } +} diff --git a/fbw-a32nx/src/systems/systems-host/index.ts b/fbw-a32nx/src/systems/systems-host/index.ts index 38afa459c5c..33210e739cf 100644 --- a/fbw-a32nx/src/systems/systems-host/index.ts +++ b/fbw-a32nx/src/systems/systems-host/index.ts @@ -2,18 +2,43 @@ // // SPDX-License-Identifier: GPL-3.0 -import { EventBus, HEventPublisher } from '@microsoft/msfs-sdk'; +import { + Clock, + ClockEvents, + EventBus, + HEventPublisher, + InstrumentBackplane, + StallWarningPublisher, +} from '@microsoft/msfs-sdk'; import { AtsuSystem } from './systems/atsu'; import { PowerSupplyBusses } from './systems/powersupply'; +import { PseudoFWC } from 'systems-host/systems/FWC/PseudoFWC'; +import { FuelSystemPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/FuelSystemPublisher'; +import { A32NXFcuBusPublisher } from '@shared/publishers/A32NXFcuBusPublisher'; +import { PseudoFwcSimvarPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher'; class SystemsHost extends BaseInstrument { - private readonly bus: EventBus; + private readonly bus = new EventBus(); - private readonly hEventPublisher: HEventPublisher; + private readonly backplane = new InstrumentBackplane(); - private readonly powerSupply: PowerSupplyBusses; + private readonly clock = new Clock(this.bus); - private readonly atsu: AtsuSystem; + private readonly hEventPublisher = new HEventPublisher(this.bus); + + private readonly powerSupply = new PowerSupplyBusses(this.bus); + + private readonly atsu = new AtsuSystem(this.bus); + + private readonly fuelSystemPublisher = new FuelSystemPublisher(this.bus); + + private readonly stallWarningPublisher = new StallWarningPublisher(this.bus, 0.9); + + private readonly a32nxFcuBusPublisher = new A32NXFcuBusPublisher(this.bus); + + private readonly pseudoFwcPublisher = new PseudoFwcSimvarPublisher(this.bus); + + private readonly pseudoFwc = new PseudoFWC(this.bus, this); /** * "mainmenu" = 0 @@ -26,10 +51,26 @@ class SystemsHost extends BaseInstrument { constructor() { super(); - this.bus = new EventBus(); - this.hEventPublisher = new HEventPublisher(this.bus); - this.powerSupply = new PowerSupplyBusses(this.bus); - this.atsu = new AtsuSystem(this.bus); + this.backplane.addInstrument('Clock', this.clock); + this.backplane.addInstrument('AtsuSystem', this.atsu); + this.backplane.addPublisher('FuelSystem', this.fuelSystemPublisher); + this.backplane.addPublisher('PowerPublisher', this.powerSupply); + this.backplane.addPublisher('stallWarning', this.stallWarningPublisher); + this.backplane.addPublisher('a32nxFcuBusPublisher', this.a32nxFcuBusPublisher); + this.backplane.addPublisher('PseudoFwcPublisher', this.pseudoFwcPublisher); + + this.pseudoFwc.init(); + let lastUpdateTime: number; + this.bus + .getSubscriber() + .on('simTimeHiFreq') + .atFrequency(50) + .handle((now) => { + const dt = lastUpdateTime === undefined ? 0 : now - lastUpdateTime; + lastUpdateTime = now; + + this.pseudoFwc.update(dt); + }); } get templateID(): string { @@ -47,9 +88,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', @@ -58,6 +96,8 @@ class SystemsHost extends BaseInstrument { }, true, ); + + this.backplane.init(); } public Update(): void { @@ -67,14 +107,11 @@ 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(); + this.backplane.onUpdate(); } } diff --git a/fbw-a32nx/src/systems/systems-host/systems/FWC/FwsSoundManager.ts b/fbw-a32nx/src/systems/systems-host/systems/FWC/FwsSoundManager.ts new file mode 100644 index 00000000000..1010f16b25a --- /dev/null +++ b/fbw-a32nx/src/systems/systems-host/systems/FWC/FwsSoundManager.ts @@ -0,0 +1,428 @@ +// Copyright (c) 2021-2024 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, SimVarValueType, Subscribable } from '@microsoft/msfs-sdk'; + +export interface FwsSoundManagerControlEvents { + enqueueSound: string; + dequeueSound: string; +} + +// Synthetic voice has priority over everything, SC is least important +enum FwsAuralWarningType { + SingleChime, + AuralWarning, + SyntheticVoice, +} + +export enum FwsAuralVolume { + Full, // 0 dB + Attenuated, // -6dB + Silent, // -200 dB +} + +interface FwsAural { + /** The LocalVar which triggers the playback. Not prefixed by L: here. Either localVarName or wwiseEventName has to be defined. */ + localVarName?: string; + /** The Wwise event which triggers the playback. Either localVarName or wwiseEventName has to be defined. */ + wwiseEventName?: string; + /** Sounds are queued based on type and priority (highest priority = gets queued first within same type) */ + priority: number; + type: FwsAuralWarningType; + /** Length of audio in seconds, if non-repetitive */ + length?: number; + /** If this is set, this sound is repeated periodically with the specified pause in seconds */ + periodicWithPause?: number; + continuous?: boolean; +} + +export const FwsAuralsList: Record = { + continuousRepetitiveChime: { + localVarName: 'A32NX_FWC_CRC', + priority: 5, + type: FwsAuralWarningType.AuralWarning, + continuous: true, + }, + singleChime: { + localVarName: 'A32NX_FWC_SC', + length: 0.54, + priority: 0, + type: FwsAuralWarningType.SingleChime, + continuous: false, + }, + cavalryChargeOnce: { + localVarName: 'A32NX_FWC_CAVALRY_CHARGE', + length: 0.9, + priority: 4, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + cavalryChargeCont: { + localVarName: 'A32NX_FWC_CAVALRY_CHARGE', + priority: 4, + type: FwsAuralWarningType.AuralWarning, + continuous: true, + }, + tripleClick: { + localVarName: 'A32NX_FMA_TRIPLE_CLICK', + length: 0.62, + priority: 3, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + pause0p8s: { + localVarName: 'A32NX_AUDIO_PAUSE', + length: 0.8, + priority: 4, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + autoBrakeOff: { + localVarName: 'A32NX_AUDIO_AUTOBRAKE_OFF', + length: 1.5, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + runwayTooShort: { + localVarName: 'A32NX_AUDIO_ROW_RWY_TOO_SHORT', + length: 1.6, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: true, + }, + keepMaxReverse: { + localVarName: 'A32NX_AUDIO_ROP_KEEP_MAX_REVERSE', + length: 1.4, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + setMaxReverse: { + localVarName: 'A32NX_AUDIO_ROW_SET_MAX_REVERSE', + length: 1.62, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + brakeMaxBraking: { + localVarName: 'A32NX_AUDIO_ROP_MAX_BRAKING', + length: 3.1, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: true, + }, + stall: { + localVarName: 'A32NX_AUDIO_ROP_MAX_BRAKING', + length: 3.0, + priority: 5, + type: FwsAuralWarningType.SyntheticVoice, + continuous: true, + }, + cChordOnce: { + localVarName: 'A32NX_ALT_DEVIATION', + length: 1.0, + priority: 3, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + cChordCont: { + localVarName: 'A32NX_ALT_DEVIATION', + priority: 3, + type: FwsAuralWarningType.AuralWarning, + continuous: true, + }, + // Altitude callouts + minimums: { + wwiseEventName: 'aural_minimumnew', + length: 0.67, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + hundred_above: { + wwiseEventName: 'aural_100above', + length: 0.72, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + retard: { + wwiseEventName: 'new_retard', + length: 0.9, + periodicWithPause: 0.2, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_2500: { + wwiseEventName: 'new_2500', + length: 1.1, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_2500b: { + wwiseEventName: 'new_2_500', + length: 1.047, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_2000: { + wwiseEventName: 'new_2000', + length: 0.72, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_1000: { + wwiseEventName: 'new_1000', + length: 0.9, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_500: { + wwiseEventName: 'new_500', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_400: { + wwiseEventName: 'new_400', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_300: { + wwiseEventName: 'new_300', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_200: { + wwiseEventName: 'new_200', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_100: { + wwiseEventName: 'new_100', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_50: { + wwiseEventName: 'new_50', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_40: { + wwiseEventName: 'new_40', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_30: { + wwiseEventName: 'new_30', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_20: { + wwiseEventName: 'new_20', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_10: { + wwiseEventName: 'new_10', + length: 0.3, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_5: { + wwiseEventName: 'new_5', + length: 0.3, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, +}; + +// FIXME Not all sounds are added to this yet (e.g. CIDS chimes), consider adding them in the future +// Also, single chimes are not filtered (in RL only once every two seconds) +export class FwsSoundManager { + private readonly soundQueue = new Set(); + + private singleChimesPending = 0; + + private currentSoundPlaying: keyof typeof FwsAuralsList | null = null; + + /** in seconds */ + private currentSoundPlayTimeRemaining = 0; + + constructor( + private bus: EventBus, + private startupCompleted: Subscribable, + ) { + // Stop all sounds + Object.values(FwsAuralsList).forEach((a) => { + if (a.localVarName) { + SimVar.SetSimVarValue(`L:${a.localVarName}`, SimVarValueType.Bool, false); + } + }); + + const sub = this.bus.getSubscriber(); + sub.on('enqueueSound').handle((s) => this.enqueueSound(s)); + sub.on('dequeueSound').handle((s) => this.dequeueSound(s)); + } + + /** Add sound to queue. Don't add if already playing */ + enqueueSound(soundKey: keyof typeof FwsAuralsList) { + const sound = FwsAuralsList[soundKey]; + if (!sound || this.currentSoundPlaying === soundKey) { + return; + } + + if (sound.type === FwsAuralWarningType.SyntheticVoice || sound.type === FwsAuralWarningType.AuralWarning) { + this.soundQueue.add(soundKey); + } else if (sound.type === FwsAuralWarningType.SingleChime) { + this.singleChimesPending++; + } + } + + /** Remove sound from queue, e.g. when condition doesn't apply anymore. If sound is currently playing, stops sound immediately */ + dequeueSound(soundKey: keyof typeof FwsAuralsList) { + // Check if this sound is currently playing + if (this.currentSoundPlaying === soundKey && FwsAuralsList[this.currentSoundPlaying]?.continuous) { + this.stopCurrentSound(); + } + this.soundQueue.delete(soundKey); + } + + private stopCurrentSound() { + // Only LVar sounds which are continuous can be stopped + if ( + this.currentSoundPlaying && + FwsAuralsList[this.currentSoundPlaying].localVarName && + FwsAuralsList[this.currentSoundPlaying]?.continuous + ) { + SimVar.SetSimVarValue(`L:${FwsAuralsList[this.currentSoundPlaying].localVarName}`, SimVarValueType.Bool, false); + this.currentSoundPlaying = null; + this.currentSoundPlayTimeRemaining = 0; + } + } + + /** + * Convenience function for FWS: If condition true and sound not already playing, add to queue. If not, dequeue sound + * */ + handleSoundCondition(soundKey: keyof typeof FwsAuralsList, condition: boolean) { + if (condition && this.currentSoundPlaying !== soundKey) { + this.enqueueSound(soundKey); + } else if (!condition) { + this.dequeueSound(soundKey); + } + } + + /** This only has an effect on sounds defining WwiseRTPC behavior/var for volume */ + setVolume(volume: FwsAuralVolume) { + SimVar.SetSimVarValue('L:A32NX_FWS_AUDIO_VOLUME', SimVarValueType.Enum, volume); + } + + /** Play now, not to be called from the outside */ + private playSound(soundKey: keyof typeof FwsAuralsList) { + const sound = FwsAuralsList[soundKey]; + if (!sound) { + return; + } + + if (sound.localVarName) { + SimVar.SetSimVarValue(`L:${sound.localVarName}`, SimVarValueType.Bool, true); + } else if (sound.wwiseEventName) { + Coherent.call('PLAY_INSTRUMENT_SOUND', sound.wwiseEventName); + } + this.currentSoundPlaying = soundKey; + this.currentSoundPlayTimeRemaining = sound.continuous ? Infinity : sound.length; + this.soundQueue.delete(soundKey); + } + /** Find most important sound from soundQueue and play */ + private selectAndPlayMostImportantSound(): keyof typeof FwsAuralsList | null { + if (!this.startupCompleted.get()) { + return; + } + + // Logic for scheduling new sounds: Take sound from soundQueue of most important type + // (SyntheticVoice > AuralWarning > SingleChime) with highest priority, and play it + let selectedSoundKey: keyof typeof FwsAuralsList | null = null; + this.soundQueue.forEach((sk) => { + const s = FwsAuralsList[sk]; + if ( + selectedSoundKey === null || + s.type > FwsAuralsList[selectedSoundKey].type || + (s.type === FwsAuralsList[selectedSoundKey].type && s.priority > FwsAuralsList[selectedSoundKey].priority) + ) { + selectedSoundKey = sk; + } + }); + + if (selectedSoundKey) { + this.playSound(selectedSoundKey); + return selectedSoundKey; + } + + // See if single chimes are left + if (this.singleChimesPending) { + this.playSound('singleChime'); + this.singleChimesPending--; + return 'singleChime'; + } + + // Ok, nothing to play + return null; + } + + onUpdate(deltaTime: number) { + // Either wait for the current sound to finish, or schedule the next sound + if (this.currentSoundPlaying && this.currentSoundPlayTimeRemaining > 0) { + if (this.currentSoundPlayTimeRemaining - deltaTime / 1_000 > 0) { + // Wait for sound to be finished + this.currentSoundPlayTimeRemaining -= deltaTime / 1_000; + } else { + // Sound finishes in this cycle + if (FwsAuralsList[this.currentSoundPlaying].localVarName) { + SimVar.SetSimVarValue( + `L:${FwsAuralsList[this.currentSoundPlaying].localVarName}`, + SimVarValueType.Bool, + false, + ); + } + this.currentSoundPlaying = null; + this.currentSoundPlayTimeRemaining = 0; + } + + // Interrupt if sound with higher category is present in queue and current sound is continuous + let shouldInterrupt = false; + let rescheduleSound: keyof typeof FwsAuralsList | null = null; + this.soundQueue.forEach((sk) => { + const s = FwsAuralsList[sk]; + if ( + s && + this.currentSoundPlaying && + FwsAuralsList[this.currentSoundPlaying]?.continuous && + s.type > FwsAuralsList[this.currentSoundPlaying].type + ) { + shouldInterrupt = true; + } + }); + + if (shouldInterrupt) { + if (this.currentSoundPlaying && FwsAuralsList[this.currentSoundPlaying]?.continuous) { + rescheduleSound = this.currentSoundPlaying; + this.stopCurrentSound(); + if (rescheduleSound) { + this.enqueueSound(rescheduleSound); + } + } + } + } else { + // Play next sound + this.selectAndPlayMostImportantSound(); + } + } +} diff --git a/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts b/fbw-a32nx/src/systems/systems-host/systems/FWC/PseudoFWC.ts similarity index 88% rename from fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts rename to fbw-a32nx/src/systems/systems-host/systems/FWC/PseudoFWC.ts index 228279f0ca1..4266ca34ee1 100644 --- a/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts +++ b/fbw-a32nx/src/systems/systems-host/systems/FWC/PseudoFWC.ts @@ -13,6 +13,10 @@ import { SimVarValueType, SubscribableMapFunctions, StallWarningEvents, + KeyEventManager, + GameStateProvider, + Wait, + KeyEvents, } from '@microsoft/msfs-sdk'; import { @@ -21,17 +25,20 @@ import { Arinc429RegisterSubject, Arinc429SignStatusMatrix, Arinc429Word, + Arinc429WordData, NXDataStore, NXLogicClockNode, NXLogicConfirmNode, NXLogicMemoryNode, NXLogicPulseNode, NXLogicTriggeredMonostableNode, + UpdateThrottler, } from '@flybywiresim/fbw-sdk'; import { VerticalMode } from '@shared/autopilot'; -import { EwdSimvars } from 'instruments/src/EWD/shared/EwdSimvarPublisher'; -import { FuelSystemEvents } from '../MsfsAvionicsCommon/providers/FuelSystemPublisher'; +import { FuelSystemEvents } from '../../../instruments/src/MsfsAvionicsCommon/providers/FuelSystemPublisher'; import { A32NXFcuBusEvents } from '../../../shared/src/publishers/A32NXFcuBusPublisher'; +import { FwsSoundManager } from 'systems-host/systems/FWC/FwsSoundManager'; +import { PseudoFwcSimvars } from 'instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher'; export function xor(a: boolean, b: boolean): boolean { return !!((a ? 1 : 0) ^ (b ? 1 : 0)); @@ -70,10 +77,21 @@ enum FwcAuralWarning { None, SingleChime, Crc, + CavalryCharge, } export class PseudoFWC { - private readonly sub = this.bus.getSubscriber(); + private readonly sub = this.bus.getSubscriber< + PseudoFwcSimvars & StallWarningEvents & KeyEvents & A32NXFcuBusEvents + >(); + + private readonly fwsUpdateThrottler = new UpdateThrottler(125); // has to be > 100 due to pulse nodes + + private keyEventManager: KeyEventManager; + + private readonly startupCompleted = Subject.create(false); + + public readonly soundManager = new FwsSoundManager(this.bus, this.startupCompleted); /** Time to inhibit master warnings and cautions during startup in ms */ private static readonly FWC_STARTUP_TIME = 5000; @@ -81,9 +99,6 @@ export class PseudoFWC { /** Time to inhibit SCs after one is trigger in ms */ private static readonly AURAL_SC_INHIBIT_TIME = 2000; - /** The time to play the single chime sound in ms */ - private static readonly AURAL_SC_PLAY_TIME = 500; - private static readonly EWD_MESSAGE_LINES = 7; private static readonly ewdMessageSimVarsLeft = Array.from( @@ -104,11 +119,16 @@ export class PseudoFWC { Subject.create(''), ); + // Input buffering + public readonly toConfigInputBuffer = new NXLogicMemoryNode(false); + public readonly clearButtonInputBuffer = new NXLogicMemoryNode(false); + public readonly recallButtonInputBuffer = new NXLogicMemoryNode(false); + public readonly aThrDiscInputBuffer = new NXLogicMemoryNode(false); + public readonly apDiscInputBuffer = new NXLogicMemoryNode(false); + /* PSEUDO FWC VARIABLES */ private readonly startupTimer = new DebounceTimer(); - private readonly startupCompleted = Subject.create(false); - private readonly allCurrentFailures: string[] = []; private readonly failuresLeft: string[] = []; @@ -117,6 +137,15 @@ export class PseudoFWC { private recallFailures: string[] = []; + private requestMasterCautionFromFaults = false; + private requestMasterCautionFromABrkOff = false; + private requestMasterCautionFromAThrOff = false; + + private requestSingleChimeFromAThrOff = false; + + private requestMasterWarningFromFaults = false; + private requestMasterWarningFromApOff = false; + private auralCrcKeys: string[] = []; private auralScKeys: string[] = []; @@ -127,8 +156,6 @@ export class PseudoFWC { private readonly auralSingleChimeInhibitTimer = new DebounceTimer(); - private readonly auralSingleChimePlayingTimer = new DebounceTimer(); - private readonly masterWarning = Subject.create(false); private readonly masterCaution = Subject.create(false); @@ -285,6 +312,76 @@ export class PseudoFWC { private toSpeedsNotInsertedWarning = Subject.create(false); + /** If multiple AP discs are triggered between FWS cycles, memorize the amount */ + public autoPilotInstinctiveDiscCountSinceLastFwsCycle = 0; + + public readonly autoPilotDisengagedInstantPulse = new NXLogicPulseNode(false); + + /** 1.8s according to references, but was raised to 1.9s to allow for triple click to finish */ + public readonly autoPilotInstinctiveDiscPressedInLast1p5Sec = new NXLogicTriggeredMonostableNode(1.5, true); + + /** 1.8s according to references, but was raised to 1.9s to allow for triple click to finish */ + public readonly autoPilotInstinctiveDiscPressedTwiceInLast1p5Sec = new NXLogicTriggeredMonostableNode(1.5, true); + + public readonly autoPilotInstinctiveDiscPressedPulse = new NXLogicPulseNode(true); + + /** Stay in first warning stage for 1.8s. Raised to 1.9s to allow for triple click to finish */ + public readonly autoPilotOffVoluntaryEndAfter1p5s = new NXLogicTriggeredMonostableNode(1.5, true); + + public readonly autoPilotOffVoluntaryFirstCavalryChargeActive = new NXLogicTriggeredMonostableNode(0.8, true); + + public readonly autoPilotOffVoluntaryFirstCavalryChargeActivePulse = new NXLogicPulseNode(false); + + public readonly autoPilotOffVoluntaryDiscPulse = new NXLogicPulseNode(true); + + public readonly autoPilotOffVoluntaryMemory = new NXLogicMemoryNode(true); + + public readonly autoPilotOffInvoluntaryMemory = new NXLogicMemoryNode(false); + + public readonly autoPilotOffAlertShown = Subject.create(false); + + public readonly autoPilotOffInvoluntary = Subject.create(false); + + public readonly autoPilotOffAlertShownMemory = new NXLogicMemoryNode(false); + + public readonly autoPilotOffUnacknowledged = new NXLogicMemoryNode(false); + + public readonly autoPilotOffShowMemo = Subject.create(false); + + public readonly fmgc1DiscreteWord3 = Arinc429LocalVarConsumerSubject.create(this.sub.on('fmgc1DiscreteWord3')); + + public readonly fmgc2DiscreteWord3 = Arinc429LocalVarConsumerSubject.create(this.sub.on('fmgc2DiscreteWord3')); + + public readonly fmgc1DiscreteWord4 = Arinc429LocalVarConsumerSubject.create(this.sub.on('fmgc1DiscreteWord4')); + + public readonly fmgc2DiscreteWord4 = Arinc429LocalVarConsumerSubject.create(this.sub.on('fmgc2DiscreteWord4')); + + public readonly fmgcApproachCapability = Subject.create(0); + + public readonly approachCapabilityDowngradeDebounce = new NXLogicTriggeredMonostableNode(0.5, true); + + public readonly approachCapabilityDowngradeSuppress = new NXLogicTriggeredMonostableNode(3, true); + + public readonly approachCapabilityDowngradeDebouncePulse = new NXLogicPulseNode(false); + + public readonly autoThrustDisengagedInstantPulse = new NXLogicPulseNode(false); + + public readonly autoThrustInstinctiveDiscPressed = new NXLogicTriggeredMonostableNode(1.5, true); // Save event for 1.5 sec + + public readonly autoThrustOffVoluntaryMemoNode = new NXLogicTriggeredMonostableNode(9, false); // Emit memo for max. 9 sec + + public readonly autoThrustOffVoluntaryCautionNode = new NXLogicTriggeredMonostableNode(3, false); // Emit master caution for max. 3 sec + + public readonly autoThrustOffInvoluntaryNode = new NXLogicMemoryNode(false); + + public autoThrustInhibitCaution = false; // Inhibit for 10 sec + + public readonly autoThrustOffVoluntary = Subject.create(false); + + public readonly autoThrustOffInvoluntary = Subject.create(false); + + public autoThrustOffVoluntaryMemoInhibited = false; + /** TO CONF pressed in phase 2 or 3 SR */ private toConfigCheckedInPhase2Or3 = false; @@ -678,6 +775,14 @@ export class PseudoFWC { private onGroundImmediate = false; + public readonly autoBrakeDeactivatedNode = new NXLogicTriggeredMonostableNode(9, false); // When ABRK deactivated, emit this for 9 sec + + public readonly autoBrakeOffAuralConfirmNode = new NXLogicConfirmNode(1, true); + + public readonly autoBrakeOff = Subject.create(false); + + public autoBrakeOffAuralTriggered = false; + /* NAVIGATION */ private readonly adirsRemainingAlignTime = Subject.create(0); @@ -798,12 +903,22 @@ export class PseudoFWC { private readonly throttle2Position = Subject.create(0); + public readonly allThrottleIdle = Subject.create(false); + private readonly engine1ValueSwitch = ConsumerValue.create(null, false); private readonly engine2ValueSwitch = ConsumerValue.create(null, false); private readonly autoThrustStatus = Subject.create(0); + private readonly atsDiscreteWord = Arinc429Register.empty(); + + private readonly ecu1MaintenanceWord6 = Arinc429Register.empty(); + + private readonly ecu2MaintenanceWord6 = Arinc429Register.empty(); + + private readonly thrLocked = Subject.create(false); + private readonly autothrustLeverWarningFlex = Subject.create(false); private readonly autothrustLeverWarningToga = Subject.create(false); @@ -946,10 +1061,34 @@ export class PseudoFWC { } init(): void { + Promise.all([ + KeyEventManager.getManager(this.bus), + Wait.awaitSubscribable(GameStateProvider.get(), (state) => state === GameState.ingame, true), + ]).then(([keyEventManager]) => { + this.keyEventManager = keyEventManager; + this.registerKeyEvents(); + }); + + this.sub + .on('key_intercept') + .atFrequency(50) + .handle((keyData) => { + switch (keyData.key) { + case 'A32NX.AUTO_THROTTLE_DISCONNECT': + this.autoThrottleInstinctiveDisconnect(); + break; + case 'A32NX.FCU_AP_DISCONNECT_PUSH': + case 'A32NX.AUTOPILOT_DISENGAGE': + case 'AUTOPILOT_OFF': + this.autoPilotInstinctiveDisconnect(); + break; + } + }); + this.toConfigNormal.sub((normal) => SimVar.SetSimVarValue('L:A32NX_TO_CONFIG_NORMAL', 'bool', normal)); this.fwcFlightPhase.sub(() => this.flightPhaseEndedPulseNode.write(true, 0)); - this.auralCrcOutput.sub((crc) => SimVar.SetSimVarValue('L:A32NX_FWC_CRC', 'bool', crc)); + this.auralCrcOutput.sub((crc) => this.soundManager.handleSoundCondition('continuousRepetitiveChime', crc), true); this.masterCaution.sub((caution) => { // Inhibit master warning/cautions until FWC startup has been completed @@ -1017,6 +1156,14 @@ export class PseudoFWC { }); } + private registerKeyEvents() { + this.keyEventManager.interceptKey('A32NX.AUTO_THROTTLE_DISCONNECT', true); + this.keyEventManager.interceptKey('A32NX.FCU_AP_DISCONNECT_PUSH', true); + this.keyEventManager.interceptKey('A32NX.AUTOPILOT_DISENGAGE', false); // internal event, for FWS only + this.keyEventManager.interceptKey('AUTOPILOT_OFF', true); + this.keyEventManager.interceptKey('AUTO_THROTTLE_ARM', true); + } + mapOrder(array, order): [] { array.sort((a, b) => { if (order.indexOf(a) > order.indexOf(b)) { @@ -1094,8 +1241,43 @@ export class PseudoFWC { /** * Periodic update */ - onUpdate() { - const deltaTime = this.instrument.deltaTime; + update(_deltaTime: number) { + const deltaTime = this.fwsUpdateThrottler.canUpdate(_deltaTime); + + // Acquire discrete inputs at a higher frequency, buffer them until the next FWS cycle. + // T.O CONFIG button + if (SimVar.GetSimVarValue('L:A32NX_BTN_TOCONFIG', 'bool')) { + this.toConfigInputBuffer.write(true, false); + } + + // CLR buttons + const clearButtonLeft = SimVar.GetSimVarValue('L:A32NX_BTN_CLR', 'bool'); + const clearButtonRight = SimVar.GetSimVarValue('L:A32NX_BTN_CLR2', 'bool'); + if (clearButtonLeft || clearButtonRight) { + this.clearButtonInputBuffer.write(true, false); + } + + // RCL button + const recallButton = SimVar.GetSimVarValue('L:A32NX_BTN_RCL', 'bool'); + if (recallButton) { + this.recallButtonInputBuffer.write(true, false); + } + + // Enforce cycle time for the logic computation (otherwise pulse nodes would be broken) + if (deltaTime === -1 || _deltaTime === 0) { + return; + } + + // Play sounds + this.soundManager.onUpdate(deltaTime); + + // Write pulse nodes for buffered inputs + this.toConfigPulseNode.write(this.toConfigInputBuffer.read(), deltaTime); + this.clr1PulseNode.write(this.clearButtonInputBuffer.read(), deltaTime); + this.clr2PulseNode.write(this.clearButtonInputBuffer.read(), deltaTime); + this.rclUpPulseNode.write(this.recallButtonInputBuffer.read(), deltaTime); + this.autoThrustInstinctiveDiscPressed.write(this.aThrDiscInputBuffer.read(), deltaTime); + this.autoPilotInstinctiveDiscPressedPulse.write(this.apDiscInputBuffer.read(), deltaTime); // Inputs update this.flightPhaseEndedPulseNode.write(false, deltaTime); @@ -1111,10 +1293,11 @@ export class PseudoFWC { this.flightPhase67.set([6, 7].includes(this.fwcFlightPhase.get())); this.flightPhase78.set([7, 8].includes(this.fwcFlightPhase.get())); const flightPhase567 = [5, 6, 7].includes(this.fwcFlightPhase.get()); + const flightPhase167 = [1, 6, 7].includes(this.fwcFlightPhase.get()); // TO CONFIG button this.toConfigTestRaw = SimVar.GetSimVarValue('L:A32NX_BTN_TOCONFIG', 'bool') > 0; - this.toConfigPulseNode.write(this.toConfigTestRaw, deltaTime); + this.toConfigPulseNode.write(this.toConfigTestRaw, _deltaTime); const toConfigTest = this.toConfigTriggerNode.write(this.toConfigPulseNode.read(), deltaTime); if (toConfigTest !== this.toConfigTest) { // temporary var for the old FWC stuff @@ -1126,17 +1309,11 @@ export class PseudoFWC { ); // CLR buttons - const clearButtonLeft = SimVar.GetSimVarValue('L:A32NX_BTN_CLR', 'bool'); - const clearButtonRight = SimVar.GetSimVarValue('L:A32NX_BTN_CLR2', 'bool'); - this.clr1PulseNode.write(clearButtonLeft, deltaTime); - this.clr2PulseNode.write(clearButtonRight, deltaTime); const previousClrPulseUpTrigger = this.clrPulseUpTrigger.read(); this.clrPulseUpTrigger.write(this.clr1PulseNode.read() || this.clr2PulseNode.read(), deltaTime); this.clrTriggerRisingEdge = !previousClrPulseUpTrigger && this.clrPulseUpTrigger.read(); // RCL button - const recallButton = SimVar.GetSimVarValue('L:A32NX_BTN_RCL', 'bool'); - this.rclUpPulseNode.write(recallButton, deltaTime); const previousRclUpTriggerNode = this.rclUpTriggerNode.read(); this.rclUpTriggerNode.write(recallButton, deltaTime); @@ -1208,8 +1385,17 @@ export class PseudoFWC { this.throttle1Position.set(SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:1', 'number')); this.throttle2Position.set(SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:2', 'number')); this.autoThrustStatus.set(SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_STATUS', 'enum')); + this.atsDiscreteWord.setFromSimVar('L:A32NX_FCU_ATS_DISCRETE_WORD'); + this.ecu1MaintenanceWord6.setFromSimVar('L:A32NX_ECU_1_MAINTENANCE_WORD_6'); + this.ecu2MaintenanceWord6.setFromSimVar('L:A32NX_ECU_2_MAINTENANCE_WORD_6'); this.autothrustLeverWarningFlex.set(SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_THRUST_LEVER_WARNING_FLEX', 'bool')); this.autothrustLeverWarningToga.set(SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_THRUST_LEVER_WARNING_TOGA', 'bool')); + this.allThrottleIdle.set(this.throttle1Position.get() < 1 && this.throttle2Position.get() < 1); + + const masterCautionButtonLeft = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERCAUT_L', 'bool'); + const masterCautionButtonRight = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERCAUT_R', 'bool'); + const masterWarningButtonLeft = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_L', 'bool'); + const masterWarningButtonRight = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_R', 'bool'); /* HYDRAULICS acquisition */ @@ -1341,6 +1527,201 @@ export class PseudoFWC { (onGroundCount > 1 && raInvalid); this.aircraftOnGround.set(this.onGroundConf.write(this.onGroundImmediate, deltaTime)); + // AP OFF + const apEngaged: boolean = + SimVar.GetSimVarValue('L:A32NX_FMGC_1_AP_ENGAGED', SimVarValueType.Bool) || + SimVar.GetSimVarValue('L:A32NX_FMGC_2_AP_ENGAGED', SimVarValueType.Bool); + this.autoPilotDisengagedInstantPulse.write(apEngaged, deltaTime); + + const apDiscPressedInLast1p8SecBeforeThisCycle = this.autoPilotInstinctiveDiscPressedInLast1p5Sec.read(); + this.autoPilotInstinctiveDiscPressedInLast1p5Sec.write(this.autoPilotInstinctiveDiscPressedPulse.read(), deltaTime); + + const voluntaryApDisc = + this.autoPilotDisengagedInstantPulse.read() && this.autoPilotInstinctiveDiscPressedInLast1p5Sec.read(); + this.autoPilotOffVoluntaryEndAfter1p5s.write(voluntaryApDisc, deltaTime); + this.autoPilotOffVoluntaryDiscPulse.write(voluntaryApDisc, deltaTime); + + this.autoPilotOffVoluntaryFirstCavalryChargeActive.write(this.autoPilotOffVoluntaryDiscPulse.read(), deltaTime); + this.autoPilotOffVoluntaryFirstCavalryChargeActivePulse.write( + this.autoPilotOffVoluntaryFirstCavalryChargeActive.read(), + deltaTime, + ); + + this.autoPilotInstinctiveDiscPressedTwiceInLast1p5Sec.write( + this.autoPilotInstinctiveDiscPressedPulse.read() && + (this.autoPilotInstinctiveDiscCountSinceLastFwsCycle > 1 || apDiscPressedInLast1p8SecBeforeThisCycle), + deltaTime, + ); + + this.autoPilotOffVoluntaryMemory.write( + this.autoPilotOffVoluntaryDiscPulse.read(), + apEngaged || + this.autoPilotInstinctiveDiscPressedTwiceInLast1p5Sec.read() || + !this.autoPilotOffVoluntaryEndAfter1p5s.read(), + ); + + const discPbPressedAfterDisconnection = + !this.autoPilotDisengagedInstantPulse.read() && + (this.autoPilotInstinctiveDiscPressedPulse.read() || masterWarningButtonLeft || masterWarningButtonRight); + + this.autoPilotOffUnacknowledged.write( + this.autoPilotDisengagedInstantPulse.read(), + apEngaged || this.autoPilotInstinctiveDiscPressedTwiceInLast1p5Sec.read() || discPbPressedAfterDisconnection, + ); + + this.autoPilotOffInvoluntaryMemory.write( + !apEngaged && !this.autoPilotOffVoluntaryMemory.read() && this.autoPilotOffUnacknowledged.read(), + !this.autoPilotOffUnacknowledged.read(), + ); + + this.autoPilotOffAlertShownMemory.write( + this.autoPilotOffInvoluntaryMemory.read(), + this.fwcFlightPhase.get() === 1 || apEngaged, + ); + this.autoPilotOffAlertShown.set(this.autoPilotOffAlertShownMemory.read()); + this.autoPilotOffInvoluntary.set(this.autoPilotOffInvoluntaryMemory.read()); + this.autoPilotOffShowMemo.set(this.autoPilotOffVoluntaryMemory.read() || this.autoPilotOffInvoluntaryMemory.read()); + + if (this.autoPilotDisengagedInstantPulse.read()) { + // Request quiet CRC one time + this.requestMasterWarningFromApOff = true; + this.soundManager.enqueueSound('cavalryChargeOnce'); // On the A320, play first cav charge completely no matter what + } + if (this.autoPilotOffVoluntaryFirstCavalryChargeActivePulse.read()) { + this.soundManager.dequeueSound('cavalryChargeOnce'); + } + if (!this.autoPilotOffVoluntaryMemory.read() && !this.autoPilotOffInvoluntaryMemory.read()) { + this.requestMasterWarningFromApOff = false; + this.soundManager.dequeueSound('cavalryChargeOnce'); + this.soundManager.dequeueSound('cavalryChargeCont'); + } + + this.autoPilotInstinctiveDiscPressedPulse.write(false, deltaTime); + + // approach capability downgrade. Debounce first, then suppress for a certain amount of time + // (to avoid multiple triple clicks, and a delay which is too long) + let fmgcApproachCapability = 0; + const getApproachCapability = (dw3: Arinc429WordData, dw4: Arinc429WordData): number => { + let appCap = 0; + const landModeActive = dw4.bitValueOr(14, false); + const landModeArmed = dw3.bitValueOr(20, false); + const land2Capacity = dw4.bitValueOr(23, false); + const land3FailPassiveCapacity = dw4.bitValueOr(24, false); + const land3FailOperationalCapacity = dw4.bitValueOr(25, false); + + if (land2Capacity) { + appCap = 2; + } else if (land3FailPassiveCapacity) { + appCap = 3; + } else if (land3FailOperationalCapacity) { + appCap = 4; + } else if (landModeActive || landModeArmed) { + appCap = 1; + } else { + appCap = 0; + } + return appCap; + }; + + if (this.fmgc1DiscreteWord3.get().isNormalOperation() && this.fmgc1DiscreteWord4.get().isNormalOperation()) { + fmgcApproachCapability = getApproachCapability(this.fmgc1DiscreteWord3.get(), this.fmgc1DiscreteWord4.get()); + } else if (this.fmgc2DiscreteWord3.get().isNormalOperation() && this.fmgc2DiscreteWord4.get().isNormalOperation()) { + fmgcApproachCapability = getApproachCapability(this.fmgc2DiscreteWord3.get(), this.fmgc2DiscreteWord4.get()); + } + + const capabilityDowngrade = + fmgcApproachCapability < this.fmgcApproachCapability.get() && fmgcApproachCapability > 0; + this.approachCapabilityDowngradeDebounce.write( + capabilityDowngrade && flightPhase167 && !this.approachCapabilityDowngradeSuppress.read(), + deltaTime, + ); + this.approachCapabilityDowngradeDebouncePulse.write(this.approachCapabilityDowngradeDebounce.read(), deltaTime); + this.approachCapabilityDowngradeSuppress.write(this.approachCapabilityDowngradeDebouncePulse.read(), deltaTime); + // Capability downgrade after debounce --> triple click + if (this.approachCapabilityDowngradeDebouncePulse.read()) { + this.soundManager.enqueueSound('pause0p8s'); + this.soundManager.enqueueSound('tripleClick'); + } + this.fmgcApproachCapability.set(fmgcApproachCapability); + + // A/THR OFF + const aThrEngaged = this.atsDiscreteWord.bitValueOr(13, false); + this.autoThrustDisengagedInstantPulse.write(aThrEngaged, deltaTime); + this.autoThrustInstinctiveDiscPressed.write(false, deltaTime); + + const below50ft = this.radioHeight1.valueOr(2500) < 50 && this.radioHeight2.valueOr(2500) < 50; + + if (below50ft && this.allThrottleIdle.get()) { + this.autoThrustInhibitCaution = true; + } + + const voluntaryAThrDisc = + !this.aircraftOnGround.get() && + this.autoThrustDisengagedInstantPulse.read() && + (this.autoThrustInstinctiveDiscPressed.read() || this.allThrottleIdle.get()) && + !this.autoThrustInhibitCaution; + + // Voluntary A/THR disconnect + this.autoThrustOffVoluntaryMemoNode.write(voluntaryAThrDisc && !aThrEngaged, deltaTime); + this.autoThrustOffVoluntaryCautionNode.write(voluntaryAThrDisc && !aThrEngaged, deltaTime); + this.thrLocked.set( + this.ecu1MaintenanceWord6.bitValueOr(12, false) || this.ecu2MaintenanceWord6.bitValueOr(12, false), + ); + + if (!this.autoThrustOffVoluntaryMemoNode.read()) { + this.autoThrustInhibitCaution = false; + } + + if ( + this.autoThrustOffVoluntaryCautionNode.read() && + !this.autoThrustOffVoluntary.get() && + !this.autoThrustInhibitCaution + ) { + // First triggered in this cycle, request master caution + this.requestMasterCautionFromAThrOff = true; + this.requestSingleChimeFromAThrOff = true; + } else if (!this.autoThrustOffVoluntaryCautionNode.read() || this.autoThrustInhibitCaution) { + this.requestMasterCautionFromAThrOff = false; + this.requestSingleChimeFromAThrOff = false; + } + this.autoThrustOffVoluntary.set( + this.autoThrustOffVoluntaryMemoNode.read() && !this.autoThrustInhibitCaution && !aThrEngaged, + ); + + // Involuntary A/THR disconnect + const involuntaryAThrDisc = + !this.aircraftOnGround.get() && + this.autoThrustDisengagedInstantPulse.read() && + !(this.autoThrustInstinctiveDiscPressed.read() || (below50ft && this.allThrottleIdle.get())); + + this.autoThrustOffInvoluntaryNode.write(involuntaryAThrDisc, aThrEngaged || voluntaryAThrDisc); + this.autoThrustOffInvoluntary.set(this.autoThrustOffInvoluntaryNode.read()); + + // AUTO BRAKE OFF + this.autoBrakeDeactivatedNode.write(!!SimVar.GetSimVarValue('L:A32NX_AUTOBRAKES_ACTIVE', 'boolean'), deltaTime); + + if (!this.autoBrakeDeactivatedNode.read()) { + this.requestMasterCautionFromABrkOff = false; + this.autoBrakeOffAuralTriggered = false; + } + + this.autoBrakeOffAuralConfirmNode.write(this.autoBrakeDeactivatedNode.read(), deltaTime); + + const autoBrakeOffShouldTrigger = + this.aircraftOnGround.get() && this.computedAirSpeedToNearest2.get() > 33 && this.autoBrakeDeactivatedNode.read(); + + if (autoBrakeOffShouldTrigger && !this.autoBrakeOff.get()) { + // Triggered in this cycle -> request master caution + this.requestMasterCautionFromABrkOff = true; + } + + // FIXME double callout if ABRK fails + this.autoBrakeOff.set(autoBrakeOffShouldTrigger); + if (autoBrakeOffShouldTrigger && this.autoBrakeOffAuralConfirmNode.read() && !this.autoBrakeOffAuralTriggered) { + this.soundManager.enqueueSound('autoBrakeOff'); + this.autoBrakeOffAuralTriggered = true; + } + // Engine Logic this.thrustLeverNotSet.set(this.autothrustLeverWarningFlex.get() || this.autothrustLeverWarningToga.get()); // FIXME ECU doesn't have the necessary output words so we go purely on TLA @@ -2178,18 +2559,17 @@ export class PseudoFWC { } /* MASTER CAUT/WARN BUTTONS */ - - const masterCautionButtonLeft = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERCAUT_L', 'bool'); - const masterCautionButtonRight = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERCAUT_R', 'bool'); - const masterWarningButtonLeft = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_L', 'bool'); - const masterWarningButtonRight = SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_R', 'bool'); if (masterCautionButtonLeft || masterCautionButtonRight) { - this.masterCaution.set(false); this.auralSingleChimePending = false; + this.requestMasterCautionFromFaults = false; + this.requestMasterCautionFromABrkOff = false; + this.requestMasterCautionFromAThrOff = false; + this.autoThrustInhibitCaution = true; } if ((masterWarningButtonLeft || masterWarningButtonRight) && this.nonCancellableWarningCount === 0) { - this.masterWarning.set(false); - this.auralCrcActive.set(false); + this.requestMasterWarningFromFaults = this.nonCancellableWarningCount > 0; + this.requestMasterWarningFromApOff = false; + this.auralCrcActive.set(this.nonCancellableWarningCount > 0); } /* T.O. CONFIG CHECK */ @@ -2309,10 +2689,10 @@ export class PseudoFWC { } if (value.failure === 3) { - this.masterWarning.set(true); + this.requestMasterWarningFromFaults = true; } if (value.failure === 2) { - this.masterCaution.set(true); + this.requestMasterCautionFromFaults = true; } } @@ -2378,6 +2758,10 @@ export class PseudoFWC { } auralScKeys.push(key); } + + if (value.auralWarning?.get() === FwcAuralWarning.CavalryCharge) { + this.soundManager.enqueueSound('cavalryChargeCont'); + } } this.auralCrcKeys = auralCrcKeys; @@ -2465,13 +2849,21 @@ export class PseudoFWC { this.ewdMessageLinesLeft.forEach((l, i) => l.set(orderedMemoArrayLeft[i])); if (orderedFailureArrayRight.length === 0) { - this.masterCaution.set(false); + this.requestMasterCautionFromFaults = false; if (this.nonCancellableWarningCount === 0) { - this.masterWarning.set(false); + this.requestMasterWarningFromFaults = false; } } } + this.masterCaution.set( + this.requestMasterCautionFromFaults || + this.requestMasterCautionFromABrkOff || + this.requestMasterCautionFromAThrOff, + ); + + this.masterWarning.set(this.requestMasterWarningFromFaults || this.requestMasterWarningFromApOff); + if (leftFailureSystemCount + rightFailureSystemCount === 0) { SimVar.SetSimVarValue('L:A32NX_ECAM_SFAIL', 'number', -1); } @@ -2491,30 +2883,35 @@ export class PseudoFWC { this.ewdMessageLinesRight.forEach((l, i) => l.set(orderedMemoArrayRight[i])); - // This does not consider interrupting c-chord, priority of synthetic voice etc. - // We shall wait for the rust FWC for those nice things! - if (this.auralSingleChimePending && !this.auralCrcActive.get() && !this.auralSingleChimeInhibitTimer.isPending()) { + const chimeRequested = + (this.auralSingleChimePending || this.requestSingleChimeFromAThrOff) && !this.auralCrcActive.get(); + if (chimeRequested && !this.auralSingleChimeInhibitTimer.isPending()) { this.auralSingleChimePending = false; - SimVar.SetSimVarValue('L:A32NX_FWC_SC', 'bool', true); + this.requestSingleChimeFromAThrOff = false; + this.soundManager.enqueueSound('singleChime'); // there can only be one SC per 2 seconds, non-cumulative, so clear any pending ones at the end of that inhibit period this.auralSingleChimeInhibitTimer.schedule( () => (this.auralSingleChimePending = false), PseudoFWC.AURAL_SC_INHIBIT_TIME, ); - this.auralSingleChimePlayingTimer.schedule( - () => SimVar.SetSimVarValue('L:A32NX_FWC_SC', 'bool', false), - PseudoFWC.AURAL_SC_PLAY_TIME, - ); } this.updateRowRopWarnings(); + + // Reset all buffered inputs + this.toConfigInputBuffer.write(false, true); + this.clearButtonInputBuffer.write(false, true); + this.recallButtonInputBuffer.write(false, true); + this.aThrDiscInputBuffer.write(false, true); + this.apDiscInputBuffer.write(false, true); + this.autoPilotInstinctiveDiscCountSinceLastFwsCycle = 0; } updateRowRopWarnings() { const w = Arinc429Word.fromSimVarValue('L:A32NX_ROW_ROP_WORD_1'); // ROW - SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_RWY_TOO_SHORT', 'bool', w.bitValueOr(15, false)); + this.soundManager.handleSoundCondition('runwayTooShort', w.bitValueOr(15, false)); // ROP // MAX BRAKING, only for manual braking, if maximum pedal braking is not applied @@ -2522,25 +2919,71 @@ export class PseudoFWC { SimVar.GetSimVarValue('L:A32NX_LEFT_BRAKE_PEDAL_INPUT', 'number') > 90 || SimVar.GetSimVarValue('L:A32NX_RIGHT_BRAKE_PEDAL_INPUT', 'number') > 90; const maxBraking = w.bitValueOr(13, false) && !maxBrakingSet; - SimVar.SetSimVarValue('L:A32NX_AUDIO_ROP_MAX_BRAKING', 'bool', maxBraking); + this.soundManager.handleSoundCondition('brakeMaxBraking', maxBraking); // SET MAX REVERSE, if not already max. reverse set and !MAX_BRAKING const maxReverseSet = SimVar.GetSimVarValue('L:XMLVAR_Throttle1Position', 'number') < 0.1 && SimVar.GetSimVarValue('L:XMLVAR_Throttle2Position', 'number') < 0.1; const maxReverse = (w.bitValueOr(12, false) || w.bitValueOr(13, false)) && !maxReverseSet; - SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_SET_MAX_REVERSE', 'bool', !maxBraking && maxReverse); + this.soundManager.handleSoundCondition('setMaxReverse', !maxBraking && maxReverse); // At 80kt, KEEP MAX REVERSE once, if max. reversers deployed const ias = SimVar.GetSimVarValue('AIRSPEED INDICATED', 'knots'); - SimVar.SetSimVarValue( - 'L:A32NX_AUDIO_ROP_KEEP_MAX_REVERSE', - 'bool', + this.soundManager.handleSoundCondition( + 'keepMaxReverse', ias <= 80 && ias > 4 && (w.bitValueOr(12, false) || w.bitValueOr(13, false)), ); } + autoThrottleInstinctiveDisconnect() { + // When instinctive A/THR disc. p/b is pressed after ABRK deactivation, inhibit audio+memo, don't request master caution + // Unclear refs, whether this has to happen within the audio confirm node time (1s) + if (this.autoBrakeDeactivatedNode.read()) { + this.requestMasterCautionFromABrkOff = false; + } + + this.aThrDiscInputBuffer.write(true, false); + + if (this.autoThrustOffVoluntary.get()) { + // Pressed a second time -> silence + this.autoThrustInhibitCaution = true; + this.requestMasterCautionFromAThrOff = false; + } + } + + autoPilotInstinctiveDisconnect() { + this.apDiscInputBuffer.write(true, false); + if (this.apDiscInputBuffer.read()) { + this.autoPilotInstinctiveDiscCountSinceLastFwsCycle++; + } + } + ewdMessageFailures: EWDMessageDict = { + // 22 - FLIGHT GUIDANCE + 220800001: { + // AP OFF involuntary + flightPhaseInhib: [], + simVarIsActive: this.autoPilotOffAlertShown, + auralWarning: this.autoPilotOffInvoluntary.map((a) => (a ? FwcAuralWarning.CavalryCharge : FwcAuralWarning.None)), + whichCodeToReturn: () => [0], + codesToReturn: ['220800001'], + memoInhibit: () => false, + failure: 3, + sysPage: -1, + side: 'LEFT', + }, + 220800004: { + // A/THR OFF involuntary + flightPhaseInhib: [3, 4, 8], + simVarIsActive: this.autoThrustOffInvoluntary, + whichCodeToReturn: () => [0, this.thrLocked.get() ? 1 : null], + codesToReturn: ['220800004', '220800005'], + memoInhibit: () => false, + failure: 2, + sysPage: -1, + side: 'LEFT', + }, // 22 - AUTOFLIGHT 2200202: { // FCU 1+2 FAULT @@ -4332,6 +4775,18 @@ export class PseudoFWC { sysPage: -1, side: 'RIGHT', }, + // 32 LANDING GEAR + 320000001: { + // AUTO BRK OFF + flightPhaseInhib: [1, 2, 3, 4, 5, 6, 7, 10], + simVarIsActive: this.autoBrakeOff, + whichCodeToReturn: () => [0], + codesToReturn: ['320000001'], + memoInhibit: () => false, + failure: 0, + sysPage: -1, + side: 'RIGHT', + }, '0000040': { // NW STRG DISC flightPhaseInhib: [], @@ -4630,5 +5085,28 @@ export class PseudoFWC { sysPage: -1, side: 'RIGHT', }, + // 22 - Flight guidance + 220000001: { + // A/THR OFF + flightPhaseInhib: [], + simVarIsActive: this.autoPilotOffShowMemo, + whichCodeToReturn: () => [0], + codesToReturn: ['220000001'], + memoInhibit: () => false, + failure: 0, + sysPage: -1, + side: 'RIGHT', + }, + 220000002: { + // A/THR OFF + flightPhaseInhib: [], + simVarIsActive: this.autoThrustOffVoluntary, + whichCodeToReturn: () => [0], + codesToReturn: ['220000002'], + memoInhibit: () => false, + failure: 0, + sysPage: -1, + side: 'RIGHT', + }, }; } diff --git a/fbw-a32nx/src/systems/systems-host/systems/atsu.ts b/fbw-a32nx/src/systems/systems-host/systems/atsu.ts index 256c359a796..f129b08fe98 100644 --- a/fbw-a32nx/src/systems/systems-host/systems/atsu.ts +++ b/fbw-a32nx/src/systems/systems-host/systems/atsu.ts @@ -6,10 +6,10 @@ import { Atc } from '@datalink/atc'; import { Aoc } from '@datalink/aoc'; import { SimVarHandling } from '@datalink/common'; import { Router } from '@datalink/router'; -import { EventBus, EventSubscriber } from '@microsoft/msfs-sdk'; +import { EventBus, EventSubscriber, Instrument } from '@microsoft/msfs-sdk'; import { PowerSupplyBusTypes } from 'systems-host/systems/powersupply'; -export class AtsuSystem { +export class AtsuSystem implements Instrument { private readonly simVarHandling: SimVarHandling; private readonly powerSupply: EventSubscriber; @@ -40,18 +40,15 @@ export class AtsuSystem { }); } - public connectedCallback(): void { + public init(): void { this.simVarHandling.initialize(); + this.simVarHandling.startPublish(); this.router.initialize(); this.atc.initialize(); this.aoc.initialize(); } - public startPublish(): void { - this.simVarHandling.startPublish(); - } - - public update(): void { + public onUpdate(): void { this.simVarHandling.update(); this.router.update(); } diff --git a/fbw-a32nx/src/systems/systems-host/systems/powersupply.ts b/fbw-a32nx/src/systems/systems-host/systems/powersupply.ts index 0fc4ca65137..796f174d292 100644 --- a/fbw-a32nx/src/systems/systems-host/systems/powersupply.ts +++ b/fbw-a32nx/src/systems/systems-host/systems/powersupply.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0 import { + BackplanePublisher, EventBus, EventSubscriber, Publisher, @@ -53,7 +54,7 @@ export interface PowerSupplyBusTypes { dcBusEss: boolean; } -export class PowerSupplyBusses { +export class PowerSupplyBusses implements BackplanePublisher { private simVarPublisher: PowerSupplySimvarPublisher = null; private subscriber: EventSubscriber = null; @@ -94,22 +95,12 @@ export class PowerSupplyBusses { .handle((powered: number) => this.publisher.pub('dcBusEss', powered !== 0, false, false)); } - public connectedCallback(): void { - this.initialize(); - - this.simVarPublisher.subscribe('msfsAcBus1'); - this.simVarPublisher.subscribe('msfsAcBus2'); - this.simVarPublisher.subscribe('msfsAcBusEss'); - this.simVarPublisher.subscribe('msfsDcBus1'); - this.simVarPublisher.subscribe('msfsDcBus2'); - this.simVarPublisher.subscribe('msfsDcBusEss'); - } - public startPublish(): void { + this.initialize(); this.simVarPublisher.startPublish(); } - public update(): void { + public onUpdate(): void { this.simVarPublisher.onUpdate(); } } diff --git a/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.cpp b/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.cpp index 68c39bf1722..6789785aa13 100644 --- a/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.cpp +++ b/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.cpp @@ -291,6 +291,7 @@ bool SimConnectInterface::prepareSimInputSimConnectDataDefinitions() { result &= addInputDataDefinition(hSimConnect, 0, Events::AUTOPILOT_DISENGAGE_SET, "AUTOPILOT_DISENGAGE_SET", true); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTOPILOT_DISENGAGE_TOGGLE, "AUTOPILOT_DISENGAGE_TOGGLE", true); result &= addInputDataDefinition(hSimConnect, 0, Events::TOGGLE_FLIGHT_DIRECTOR, "TOGGLE_FLIGHT_DIRECTOR", true); + result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_AUTOPILOT_DISENGAGE, "A32NX.AUTOPILOT_DISENGAGE", false); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_FCU_AP_1_PUSH, "A32NX.FCU_AP_1_PUSH", false); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_FCU_AP_2_PUSH, "A32NX.FCU_AP_2_PUSH", false); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_FCU_AP_DISCONNECT_PUSH, "A32NX.FCU_AP_DISCONNECT_PUSH", false); @@ -397,6 +398,7 @@ bool SimConnectInterface::prepareSimInputSimConnectDataDefinitions() { result &= addInputDataDefinition(hSimConnect, 0, Events::AUTO_THROTTLE_ARM, "AUTO_THROTTLE_ARM", true); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTO_THROTTLE_DISCONNECT, "AUTO_THROTTLE_DISCONNECT", true); + result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_AUTO_THROTTLE_DISCONNECT, "A32NX.AUTO_THROTTLE_DISCONNECT", false); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTO_THROTTLE_TO_GA, "AUTO_THROTTLE_TO_GA", true); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_ATHR_RESET_DISABLE, "A32NX.ATHR_RESET_DISABLE", false); @@ -1810,6 +1812,9 @@ void SimConnectInterface::processEvent(const DWORD eventId, const DWORD data0, c if (static_cast(data0) == 1) { simInputAutopilot.AP_disconnect = 1; std::cout << "WASM: event triggered: AUTOPILOT_DISENGAGE_SET" << std::endl; + + // Re emitting masked event for autopilot disconnection + sendEvent(SimConnectInterface::Events::A32NX_AUTOPILOT_DISENGAGE, 0, SIMCONNECT_GROUP_PRIORITY_STANDARD); } break; } @@ -2436,6 +2441,9 @@ void SimConnectInterface::processEvent(const DWORD eventId, const DWORD data0, c case Events::AUTO_THROTTLE_DISCONNECT: { simInputThrottles.ATHR_disconnect = 1; std::cout << "WASM: event triggered: AUTO_THROTTLE_DISCONNECT" << std::endl; + + // Re emitting masked event + sendEvent(Events::A32NX_AUTO_THROTTLE_DISCONNECT, 0, SIMCONNECT_GROUP_PRIORITY_STANDARD); break; } diff --git a/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.h b/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.h index fe4f980c487..f644c9cfef3 100644 --- a/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.h +++ b/fbw-a32nx/src/wasm/fbw_a320/src/interface/SimConnectInterface.h @@ -47,6 +47,7 @@ class SimConnectInterface { AUTOPILOT_DISENGAGE_SET, AUTOPILOT_DISENGAGE_TOGGLE, TOGGLE_FLIGHT_DIRECTOR, + A32NX_AUTOPILOT_DISENGAGE, A32NX_FCU_AP_1_PUSH, A32NX_FCU_AP_2_PUSH, A32NX_FCU_AP_DISCONNECT_PUSH, @@ -148,6 +149,7 @@ class SimConnectInterface { BAROMETRIC, AUTO_THROTTLE_ARM, AUTO_THROTTLE_DISCONNECT, + A32NX_AUTO_THROTTLE_DISCONNECT, AUTO_THROTTLE_TO_GA, A32NX_ATHR_RESET_DISABLE, A32NX_THROTTLE_MAPPING_SET_DEFAULTS, diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx index 6d001810ac1..47527dc1641 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher.tsx @@ -5,201 +5,18 @@ import { EventBus, SimVarDefinition, SimVarValueType, SimVarPublisher } from '@microsoft/msfs-sdk'; export type PseudoFwcSimvars = { - acEssBus: boolean; - ewdPotentiometer: number; - autoThrustCommand1: number; - autoThrustCommand2: number; - autoThrustLimit: number; - autoThrustLimitToga: number; - thrustLimitType: number; - autoThrustMode: number; - autoThrustStatus: number; - autoThrustTLA1: number; - autoThrustTLA2: number; - autoThrustWarningToga: boolean; - packs1Supplying: boolean; - packs2Supplying: boolean; - engine1AntiIce: boolean; - engine1EGT: number; - engine1Fadec: boolean; - engine1FF: number; engine1Master: number; - engine1N1: number; - engine1N2: number; - engine1ReverserTransit: boolean; - engine1ReverserDeployed: boolean; - engine1State: number; - engine2AntiIce: boolean; - engine2EGT: number; - engine2Fadec: boolean; - engine2FF: number; engine2Master: number; - engine2N1: number; - engine2N2: number; - engine2ReverserTransit: boolean; - engine2ReverserDeployed: boolean; - engine2State: number; engine3Master: number; engine4Master: number; - wingAntiIce: boolean; - apuBleedPressure: number; - left1LandingGear: boolean; - right1LandingGear: boolean; - throttle1Position: number; - throttle2Position: number; - fwcFlightPhase: number; - idleN1: number; - flexTemp: number; - satRaw: number; - totalFuel: number; - slatsFlapsStatusRaw: number; - slatsPositionRaw: number; - flapsPositionRaw: number; - ewdLowerLeft1: number; - ewdLowerLeft2: number; - ewdLowerLeft3: number; - ewdLowerLeft4: number; - ewdLowerLeft5: number; - ewdLowerLeft6: number; - ewdLowerLeft7: number; - ewdLowerRight1: number; - ewdLowerRight2: number; - ewdLowerRight3: number; - ewdLowerRight4: number; - ewdLowerRight5: number; - ewdLowerRight6: number; - ewdLowerRight7: number; }; -export enum PseudoFwcVars { - acEssBus = 'L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED', - ewdPotentiometer = 'LIGHT POTENTIOMETER:92', - autoThrustCommand1 = 'L:A32NX_AUTOTHRUST_N1_COMMANDED:1', - autoThrustCommand2 = 'L:A32NX_AUTOTHRUST_N1_COMMANDED:2', - autoThrustLimit = 'L:A32NX_AUTOTHRUST_THRUST_LIMIT', - autoThrustLimitToga = 'L:A32NX_AUTOTHRUST_THRUST_LIMIT_TOGA', - thrustLimitType = 'L:A32NX_AUTOTHRUST_THRUST_LIMIT_TYPE', - autoThrustMode = 'L:A32NX_AUTOTHRUST_MODE', - autoThrustStatus = 'L:A32NX_AUTOTHRUST_STATUS', - autoThrustTLA1 = 'L:A32NX_AUTOTHRUST_TLA_N1:1', - autoThrustTLA2 = 'L:A32NX_AUTOTHRUST_TLA_N1:2', - autoThrustWarningToga = 'L:A32NX_AUTOTHRUST_THRUST_LEVER_WARNING_TOGA', - packs1Supplying = 'L:A32NX_COND_PACK_FLOW_VALVE_1_IS_OPEN', - packs2Supplying = 'L:A32NX_COND_PACK_FLOW_VALVE_2_IS_OPEN', - engine1AntiIce = 'L:XMLVAR_Momentary_PUSH_OVHD_ANTIICE_ENG1_Pressed', - engine1EGT = 'L:A32NX_ENGINE_EGT:1', - engine1Fadec = 'L:A32NX_FADEC_POWERED_ENG1', - engine1FF = 'L:A32NX_ENGINE_FF:1', - engine1N1 = 'L:A32NX_ENGINE_N1:1', - engine1N2 = 'L:A32NX_ENGINE_N2:1', - engine1ReverserTransit = 'L:A32NX_REVERSER_1_DEPLOYING', - engine1ReverserDeployed = 'L:A32NX_REVERSER_1_DEPLOYED', - engine1State = 'L:A32NX_ENGINE_STATE:1', - engine2AntiIce = 'L:XMLVAR_Momentary_PUSH_OVHD_ANTIICE_ENG2_Pressed', - engine2EGT = 'L:A32NX_ENGINE_EGT:2', - engine2Fadec = 'L:A32NX_FADEC_POWERED_ENG2', - engine2FF = 'L:A32NX_ENGINE_FF:2', - engine2N1 = 'L:A32NX_ENGINE_N1:2', - engine2N2 = 'L:A32NX_ENGINE_N2:2', - engine2ReverserTransit = 'L:A32NX_REVERSER_2_DEPLOYING', - engine2ReverserDeployed = 'L:A32NX_REVERSER_2_DEPLOYED', - engine2State = 'L:A32NX_ENGINE_STATE:2', - wingAntiIce = 'L:A32NX_PNEU_WING_ANTI_ICE_SYSTEM_SELECTED', - apuBleedPressure = 'L:APU_BLEED_PRESSURE', - left1LandingGear = 'L:A32NX_LGCIU_1_LEFT_GEAR_COMPRESSED', - right1LandingGear = 'L:A32NX_LGCIU_1_RIGHT_GEAR_COMPRESSED', - throttle1Position = 'L:XMLVAR_Throttle1Position', - throttle2Position = 'L:XMLVAR_Throttle2Position', - fwcFlightPhase = 'L:A32NX_FWC_FLIGHT_PHASE', - idleN1 = 'L:A32NX_ENGINE_IDLE_N1', - flexTemp = 'L:AIRLINER_TO_FLEX_TEMP', - satRaw = 'L:A32NX_ADIRS_ADR_1_STATIC_AIR_TEMPERATURE', - totalFuel = 'FUEL TOTAL QUANTITY WEIGHT', - slatsFlapsStatusRaw = 'L:A32NX_SFCC_SLAT_FLAP_SYSTEM_STATUS_WORD', - slatsPositionRaw = 'L:A32NX_SFCC_SLAT_ACTUAL_POSITION_WORD', - flapsPositionRaw = 'L:A32NX_SFCC_FLAP_ACTUAL_POSITION_WORD', - ewdLowerLeft1 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_1', - ewdLowerLeft2 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_2', - ewdLowerLeft3 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_3', - ewdLowerLeft4 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_4', - ewdLowerLeft5 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_5', - ewdLowerLeft6 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_6', - ewdLowerLeft7 = 'L:A32NX_Ewd_LOWER_LEFT_LINE_7', - ewdLowerRight1 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_1', - ewdLowerRight2 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_2', - ewdLowerRight3 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_3', - ewdLowerRight4 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_4', - ewdLowerRight5 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_5', - ewdLowerRight6 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_6', - ewdLowerRight7 = 'L:A32NX_Ewd_LOWER_RIGHT_LINE_7', -} - export class PseudoFwcSimvarPublisher extends SimVarPublisher { private static simvars = new Map([ - ['acEssBus', { name: PseudoFwcVars.acEssBus, type: SimVarValueType.Bool }], - ['ewdPotentiometer', { name: PseudoFwcVars.ewdPotentiometer, type: SimVarValueType.Number }], - ['autoThrustCommand1', { name: PseudoFwcVars.autoThrustCommand1, type: SimVarValueType.Number }], - ['autoThrustCommand2', { name: PseudoFwcVars.autoThrustCommand2, type: SimVarValueType.Number }], - ['autoThrustLimit', { name: PseudoFwcVars.autoThrustLimit, type: SimVarValueType.Number }], - ['autoThrustLimitToga', { name: PseudoFwcVars.autoThrustLimitToga, type: SimVarValueType.Number }], - ['thrustLimitType', { name: PseudoFwcVars.thrustLimitType, type: SimVarValueType.Enum }], - ['autoThrustMode', { name: PseudoFwcVars.autoThrustMode, type: SimVarValueType.Enum }], - ['autoThrustStatus', { name: PseudoFwcVars.autoThrustStatus, type: SimVarValueType.Enum }], - ['autoThrustTLA1', { name: PseudoFwcVars.autoThrustTLA1, type: SimVarValueType.Number }], - ['autoThrustTLA2', { name: PseudoFwcVars.autoThrustTLA2, type: SimVarValueType.Number }], - ['autoThrustWarningToga', { name: PseudoFwcVars.autoThrustWarningToga, type: SimVarValueType.Bool }], - ['packs1Supplying', { name: PseudoFwcVars.packs1Supplying, type: SimVarValueType.Bool }], - ['packs2Supplying', { name: PseudoFwcVars.packs2Supplying, type: SimVarValueType.Bool }], - ['engine1AntiIce', { name: PseudoFwcVars.engine1AntiIce, type: SimVarValueType.Bool }], - ['engine1EGT', { name: PseudoFwcVars.engine1EGT, type: SimVarValueType.Number }], - ['engine1Fadec', { name: PseudoFwcVars.engine1Fadec, type: SimVarValueType.Bool }], ['engine1Master', { name: 'A:FUELSYSTEM VALVE SWITCH:1', type: SimVarValueType.Bool }], - ['engine1FF', { name: PseudoFwcVars.engine1FF, type: SimVarValueType.Number }], - ['engine1N1', { name: PseudoFwcVars.engine1N1, type: SimVarValueType.Number }], - ['engine1N2', { name: PseudoFwcVars.engine1N2, type: SimVarValueType.Number }], - ['engine1ReverserTransit', { name: PseudoFwcVars.engine1ReverserTransit, type: SimVarValueType.Bool }], - ['engine1ReverserDeployed', { name: PseudoFwcVars.engine1ReverserDeployed, type: SimVarValueType.Bool }], - ['engine1State', { name: PseudoFwcVars.engine1State, type: SimVarValueType.Enum }], - ['engine2AntiIce', { name: PseudoFwcVars.engine2AntiIce, type: SimVarValueType.Bool }], - ['engine2EGT', { name: PseudoFwcVars.engine2EGT, type: SimVarValueType.Number }], - ['engine2Fadec', { name: PseudoFwcVars.engine2Fadec, type: SimVarValueType.Bool }], - ['engine2FF', { name: PseudoFwcVars.engine2FF, type: SimVarValueType.Number }], ['engine2Master', { name: 'A:FUELSYSTEM VALVE SWITCH:2', type: SimVarValueType.Bool }], - ['engine2N1', { name: PseudoFwcVars.engine2N1, type: SimVarValueType.Number }], - ['engine2N2', { name: PseudoFwcVars.engine2N2, type: SimVarValueType.Number }], - ['engine2ReverserTransit', { name: PseudoFwcVars.engine2ReverserTransit, type: SimVarValueType.Bool }], - ['engine2ReverserDeployed', { name: PseudoFwcVars.engine2ReverserDeployed, type: SimVarValueType.Bool }], - ['engine2State', { name: PseudoFwcVars.engine2State, type: SimVarValueType.Enum }], ['engine3Master', { name: 'A:FUELSYSTEM VALVE SWITCH:3', type: SimVarValueType.Bool }], ['engine4Master', { name: 'A:FUELSYSTEM VALVE SWITCH:4', type: SimVarValueType.Bool }], - ['wingAntiIce', { name: PseudoFwcVars.wingAntiIce, type: SimVarValueType.Bool }], - ['apuBleedPressure', { name: PseudoFwcVars.apuBleedPressure, type: SimVarValueType.PSI }], - ['left1LandingGear', { name: PseudoFwcVars.left1LandingGear, type: SimVarValueType.Bool }], - ['right1LandingGear', { name: PseudoFwcVars.right1LandingGear, type: SimVarValueType.Bool }], - ['throttle1Position', { name: PseudoFwcVars.throttle1Position, type: SimVarValueType.Number }], - ['throttle2Position', { name: PseudoFwcVars.throttle2Position, type: SimVarValueType.Number }], - ['fwcFlightPhase', { name: PseudoFwcVars.fwcFlightPhase, type: SimVarValueType.Enum }], - ['idleN1', { name: PseudoFwcVars.idleN1, type: SimVarValueType.Number }], - ['flexTemp', { name: PseudoFwcVars.flexTemp, type: SimVarValueType.Number }], - ['satRaw', { name: PseudoFwcVars.satRaw, type: SimVarValueType.Number }], - ['totalFuel', { name: PseudoFwcVars.totalFuel, type: SimVarValueType.Number }], - ['slatsFlapsStatusRaw', { name: PseudoFwcVars.slatsFlapsStatusRaw, type: SimVarValueType.Number }], - ['slatsPositionRaw', { name: PseudoFwcVars.slatsPositionRaw, type: SimVarValueType.Number }], - ['flapsPositionRaw', { name: PseudoFwcVars.flapsPositionRaw, type: SimVarValueType.Number }], - ['ewdLowerLeft1', { name: PseudoFwcVars.ewdLowerLeft1, type: SimVarValueType.Number }], - ['ewdLowerLeft2', { name: PseudoFwcVars.ewdLowerLeft2, type: SimVarValueType.Number }], - ['ewdLowerLeft3', { name: PseudoFwcVars.ewdLowerLeft3, type: SimVarValueType.Number }], - ['ewdLowerLeft4', { name: PseudoFwcVars.ewdLowerLeft4, type: SimVarValueType.Number }], - ['ewdLowerLeft5', { name: PseudoFwcVars.ewdLowerLeft5, type: SimVarValueType.Number }], - ['ewdLowerLeft6', { name: PseudoFwcVars.ewdLowerLeft6, type: SimVarValueType.Number }], - ['ewdLowerLeft7', { name: PseudoFwcVars.ewdLowerLeft7, type: SimVarValueType.Number }], - ['ewdLowerRight1', { name: PseudoFwcVars.ewdLowerRight1, type: SimVarValueType.Number }], - ['ewdLowerRight2', { name: PseudoFwcVars.ewdLowerRight2, type: SimVarValueType.Number }], - ['ewdLowerRight3', { name: PseudoFwcVars.ewdLowerRight3, type: SimVarValueType.Number }], - ['ewdLowerRight4', { name: PseudoFwcVars.ewdLowerRight4, type: SimVarValueType.Number }], - ['ewdLowerRight5', { name: PseudoFwcVars.ewdLowerRight5, type: SimVarValueType.Number }], - ['ewdLowerRight6', { name: PseudoFwcVars.ewdLowerRight6, type: SimVarValueType.Number }], - ['ewdLowerRight7', { name: PseudoFwcVars.ewdLowerRight7, type: SimVarValueType.Number }], ]); public constructor(bus: EventBus) { diff --git a/fbw-a380x/src/systems/instruments/src/PFD/shared/PFDSimvarPublisher.tsx b/fbw-a380x/src/systems/instruments/src/PFD/shared/PFDSimvarPublisher.tsx index b289f68de72..de69cc28db8 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/shared/PFDSimvarPublisher.tsx +++ b/fbw-a380x/src/systems/instruments/src/PFD/shared/PFDSimvarPublisher.tsx @@ -194,7 +194,7 @@ export enum PFDVars { fmaSpeedProtection = 'L:A32NX_FMA_SPEED_PROTECTION_MODE', AThrMode = 'L:A32NX_AUTOTHRUST_MODE', apVsSelected = 'L:A32NX_AUTOPILOT_VS_SELECTED', - approachCapability = 'L:A32NX_ApproachCapability', + approachCapability = 'L:A32NX_APPROACH_CAPABILITY', ap1Active = 'L:A32NX_AUTOPILOT_1_ACTIVE', ap2Active = 'L:A32NX_AUTOPILOT_2_ACTIVE', fmaVerticalArmed = 'L:A32NX_FMA_VERTICAL_ARMED', diff --git a/fbw-a380x/src/systems/systems-host/index.ts b/fbw-a380x/src/systems/systems-host/index.ts index 1d93a2a0f3d..b8ebac5a4de 100644 --- a/fbw-a380x/src/systems/systems-host/index.ts +++ b/fbw-a380x/src/systems/systems-host/index.ts @@ -11,9 +11,9 @@ import { MappedSubject, SubscribableMapFunctions, WeightBalanceSimvarPublisher, + StallWarningPublisher, } from '@microsoft/msfs-sdk'; import { LegacyGpws } from 'systems-host/systems/LegacyGpws'; -import { LegacyFwc } from 'systems-host/systems/LegacyFwc'; import { LegacyFuel } from 'systems-host/systems/LegacyFuel'; import { LegacySoundManager } from 'systems-host/systems/LegacySoundManager'; import { LegacyTcasComputer } from 'systems-host/systems/tcas/components/LegacyTcasComputer'; @@ -34,6 +34,7 @@ import { AtsuSystem } from 'systems-host/systems/atsu'; import { FwsCore } from 'systems-host/systems/FlightWarningSystem/FwsCore'; import { FuelSystemPublisher } from 'systems-host/systems/FuelSystemPublisher'; import { BrakeToVacateDistanceUpdater } from 'systems-host/systems/BrakeToVacateDistanceUpdater'; +import { PseudoFwcSimvarPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher'; class SystemsHost extends BaseInstrument { private readonly bus = new ArincEventBus(); @@ -49,9 +50,6 @@ class SystemsHost extends BaseInstrument { private readonly failuresConsumer = new FailuresConsumer('A32NX'); // TODO: Migrate PowerSupplyBusses, if needed - - private fwc: LegacyFwc; - private gpws: LegacyGpws; private soundManager: LegacySoundManager; @@ -97,6 +95,10 @@ class SystemsHost extends BaseInstrument { private readonly fuelSystemPublisher = new FuelSystemPublisher(this.bus); + private readonly stallWarningPublisher = new StallWarningPublisher(this.bus, 0.9); + + private readonly pseudoFwcPublisher = new PseudoFwcSimvarPublisher(this.bus); + private readonly fwsCore = new FwsCore(1, this.bus); //FIXME add some deltatime functionality to backplane instruments so we dont have to pass SystemHost @@ -129,12 +131,13 @@ class SystemsHost extends BaseInstrument { this.backplane.addPublisher('BtvPublisher', this.btvPublisher); this.backplane.addPublisher('Weightpublisher', this.weightAndBalancePublisher); this.backplane.addPublisher('FuelPublisher', this.fuelSystemPublisher); + this.backplane.addPublisher('StallWarning', this.stallWarningPublisher); + this.backplane.addPublisher('PseudoFwc', this.pseudoFwcPublisher); this.backplane.addInstrument('LegacyFuel', this.legacyFuel); this.hEventPublisher = new HEventPublisher(this.bus); - this.fwc = new LegacyFwc(); this.soundManager = new LegacySoundManager(); - this.gpws = new LegacyGpws(this.soundManager); + this.gpws = new LegacyGpws(this.bus, this.soundManager); this.gpws.init(); this.fwsCore.init(); @@ -149,7 +152,6 @@ class SystemsHost extends BaseInstrument { const dt = lastUpdateTime === undefined ? 0 : now - lastUpdateTime; lastUpdateTime = now; - this.fwc.update(dt); this.soundManager.update(dt); this.gpws.update(dt); this.fwsCore.update(dt); diff --git a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsAbnormalSensed.ts b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsAbnormalSensed.ts index 9e3583db849..f1b7f4b83ab 100644 --- a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsAbnormalSensed.ts +++ b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsAbnormalSensed.ts @@ -1456,7 +1456,7 @@ export class FwsAbnormalSensed { flightPhaseInhib: [3, 4, 5, 10], simVarIsActive: this.fws.autoThrustOffInvoluntary, notActiveWhenFaults: [], - whichItemsToShow: () => [true], + whichItemsToShow: () => [false], whichItemsChecked: () => [SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_MODE_MESSAGE', 'number') !== 1], failure: 2, sysPage: -1, diff --git a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsCore.ts b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsCore.ts index 276c4089590..80e943068d8 100644 --- a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsCore.ts +++ b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsCore.ts @@ -49,6 +49,8 @@ import { EwdAbnormalItem, FwsAbnormalSensed } from 'systems-host/systems/FlightW import { FwsAbnormalNonSensed } from 'systems-host/systems/FlightWarningSystem/FwsAbnormalNonSensed'; import { MfdSurvEvents } from 'instruments/src/MsfsAvionicsCommon/providers/MfdSurvPublisher'; import { Mle, Mmo, VfeF1, VfeF1F, VfeF2, VfeF3, VfeFF, Vle, Vmo } from '@shared/PerformanceConstants'; +import { FwsAuralVolume, FwsSoundManager } from 'systems-host/systems/FlightWarningSystem/FwsSoundManager'; +import { FwcFlightPhase, FwsFlightPhases } from 'systems-host/systems/FlightWarningSystem/FwsFlightPhases'; export function xor(a: boolean, b: boolean): boolean { return !!((a ? 1 : 0) ^ (b ? 1 : 0)); @@ -70,30 +72,29 @@ export enum FwcAuralWarning { CavalryCharge, } -export enum FwcAuralVolume { - Full, // 0 dB - Attenuated, // -6dB - Silent, // -200 dB -} - export class FwsCore { public readonly sub = this.bus.getSubscriber< PseudoFwcSimvars & StallWarningEvents & MfdSurvEvents & FuelSystemEvents & KeyEvents >(); public readonly vhfSub = this.bus.getSubscriber(); - private fwsUpdateThrottler = new UpdateThrottler(125); // has to be > 100 due to pulse nodes + private readonly fwsUpdateThrottler = new UpdateThrottler(125); // has to be > 100 due to pulse nodes private keyEventManager: KeyEventManager; + private readonly startupTimer = new DebounceTimer(); + + private readonly startupCompleted = Subject.create(false); + + public readonly soundManager = new FwsSoundManager(this.bus, this.startupCompleted); + + private readonly flightPhases = new FwsFlightPhases(this); + /** Time to inhibit master warnings and cautions during startup in ms */ private static readonly FWC_STARTUP_TIME = 5000; /** Time to inhibit SCs after one is trigger in ms */ - private static readonly AURAL_SC_INHIBIT_TIME = 5000; - - /** The time to play the single chime sound in ms */ - private static readonly AURAL_SC_PLAY_TIME = 500; + private static readonly AURAL_SC_INHIBIT_TIME = 2000; private static readonly EWD_MESSAGE_LINES = 10; @@ -207,9 +208,6 @@ export class FwsCore { public readonly apDiscInputBuffer = new NXLogicMemoryNode(false); /* PSEUDO FWC VARIABLES */ - private readonly startupTimer = new DebounceTimer(); - - private readonly startupCompleted = Subject.create(false); /** Keys/IDs of all failures currently active, irrespective they are already cleared or not */ public readonly allCurrentFailures: string[] = []; @@ -243,8 +241,6 @@ export class FwsCore { public readonly auralSingleChimePlayingTimer = new DebounceTimer(); - public readonly auralCavalryChargeActive = Subject.create(false); - public readonly masterWarning = Subject.create(false); public readonly masterCaution = Subject.create(false); @@ -266,21 +262,6 @@ export class FwsCore { this.startupCompleted, ); - public readonly auralCrcOutput = MappedSubject.create( - ([auralCrc, startup]) => auralCrc && startup, - this.auralCrcActive, - this.startupCompleted, - ); - - public readonly auralCavalryChargeOutput = MappedSubject.create( - ([auralCavCharge, auralCrc, startup]) => auralCavCharge && !auralCrc && startup, - this.auralCavalryChargeActive, - this.auralCrcOutput, - this.startupCompleted, - ); - - public readonly fwsAuralVolume = Subject.create(FwcAuralVolume.Full); - public readonly ecamStsNormal = Subject.create(true); public readonly fwcOut126 = Arinc429RegisterSubject.createEmpty(); @@ -486,14 +467,17 @@ export class FwsCore { /** 1.8s according to references, but was raised to 1.9s to allow for triple click to finish */ public readonly autoPilotInstinctiveDiscPressedTwiceInLast1p9Sec = new NXLogicTriggeredMonostableNode(1.9, true); + /** Prohibits that CRC can be cancelled before even hearing it */ + public readonly autoPilotFirstCavalryStillWithinFirst0p3s = new NXLogicTriggeredMonostableNode(0.3, true); + public readonly autoPilotInstinctiveDiscPressedPulse = new NXLogicPulseNode(true); /** Stay in first warning stage for 1.8s. Raised to 1.9s to allow for triple click to finish */ public readonly autoPilotOffVoluntaryEndAfter1p9s = new NXLogicTriggeredMonostableNode(1.9, true); - public readonly autoPilotOffVoluntaryFirstCavalryChargeActive = new NXLogicTriggeredMonostableNode(0.8, true); + public readonly autoPilotOffVoluntaryFirstCavalryChargeActive = new NXLogicTriggeredMonostableNode(0.9, true); - public readonly autoPilotOffSendTripleClickAfterFirstCavalryCharge = new NXLogicPulseNode(false); + public readonly autoPilotOffVoluntaryFirstCavalryChargeActivePulse = new NXLogicPulseNode(false); public readonly autoPilotOffVoluntaryDiscPulse = new NXLogicPulseNode(true); @@ -507,6 +491,14 @@ export class FwsCore { public readonly autoPilotOffShowMemo = Subject.create(false); + public readonly approachCapability = Subject.create(0); + + public readonly approachCapabilityDowngradeDebounce = new NXLogicTriggeredMonostableNode(1, true); + + public readonly approachCapabilityDowngradeSuppress = new NXLogicTriggeredMonostableNode(3, true); + + public readonly approachCapabilityDowngradeDebouncePulse = new NXLogicPulseNode(false); + public readonly autoThrustDisengagedInstantPulse = new NXLogicPulseNode(false); public readonly autoThrustInstinctiveDiscPressed = new NXLogicTriggeredMonostableNode(1.5, true); // Save event for 1.5 sec @@ -1035,7 +1027,7 @@ export class FwsCore { /* 31 - FWS */ - public readonly fwcFlightPhase = Subject.create(-1); + public readonly flightPhase = Subject.create(null); public readonly flightPhase128 = Subject.create(false); @@ -1093,8 +1085,6 @@ export class FwsCore { /** If one of the ADR's CAS is above V1 - 4kts, confirm for 0.3s */ public readonly v1SpeedConfirmNode = new NXLogicConfirmNode(0.3); - public readonly v1CalloutOutput = Subject.create(false); - /* LANDING GEAR AND LIGHTS */ public readonly aircraftOnGround = Subject.create(false); @@ -1175,6 +1165,8 @@ export class FwsCore { public readonly autoBrakeOff = Subject.create(false); + public autoBrakeOffAuralTriggered = false; + public autoBrakeOffMemoInhibited = false; /* NAVIGATION */ @@ -1223,6 +1215,8 @@ export class FwsCore { this.fwsNumber === 2 ? this.airDataFoOn3 : this.airDataCaptOn3, ); + public readonly adrPressureAltitude = Subject.create(0); + public readonly ir1MaintWord = Arinc429Register.empty(); public readonly ir2MaintWord = Arinc429Register.empty(); public readonly ir3MaintWord = Arinc429Register.empty(); @@ -1579,22 +1573,14 @@ export class FwsCore { }); this.toConfigNormal.sub((normal) => SimVar.SetSimVarValue('L:A32NX_TO_CONFIG_NORMAL', 'bool', normal)); - this.fwcFlightPhase.sub(() => this.flightPhaseEndedPulseNode.write(true, 0)); + this.flightPhase.sub((fp) => { + SimVar.SetSimVarValue('L:A32NX_FWC_FLIGHT_PHASE', 'Enum', fp || 0); + if (fp !== null) { + this.flightPhaseEndedPulseNode.write(true, 0); + } + }); - this.auralCrcOutput.sub( - (crc) => SimVar.SetSimVarValue('L:A32NX_FWC_CRC', 'bool', this.startupCompleted.get() ? crc : false), - true, - ); - - this.auralCavalryChargeOutput.sub( - (cc) => - SimVar.SetSimVarValue( - 'L:A32NX_FWC_CAVALRY_CHARGE', - SimVarValueType.Bool, - this.startupCompleted.get() ? cc : false, - ), - true, - ); + this.auralCrcActive.sub((crc) => this.soundManager.handleSoundCondition('continuousRepetitiveChime', crc), true); this.masterCautionOutput.sub((caution) => { // Inhibit master warning/cautions until FWC startup has been completed @@ -1606,13 +1592,6 @@ export class FwsCore { SimVar.SetSimVarValue('L:A32NX_MASTER_WARNING', 'bool', warning); }, true); - this.fwsAuralVolume.sub((volume) => { - // Inhibit master warning/cautions until FWC startup has been completed - SimVar.SetSimVarValue('L:A32NX_FWS_AUDIO_VOLUME', SimVarValueType.Enum, volume); - }, true); - - this.v1CalloutOutput.sub((c) => SimVar.SetSimVarValue('L:A32NX_AUDIO_V1_CALLOUT', SimVarValueType.Bool, c), true); - // L/G lever red arrow sinking outputs this.lgLeverRedArrow.sub((on) => { // TODO FWCs need to be powered... @@ -1623,7 +1602,7 @@ export class FwsCore { this.stallWarning.sub((v) => { this.fwcOut126.setBitValue(17, v); // set the sound on/off - SimVar.SetSimVarValue('L:A32NX_AUDIO_STALL_WARNING', 'bool', v); + this.soundManager.handleSoundCondition('stall', v); }, true); this.aircraftOnGround.sub((v) => this.fwcOut126.setBitValue(28, v)); @@ -1832,6 +1811,12 @@ export class FwsCore { // A380X hack: Inject healthy messages for some systems which are not yet implemented this.healthInjector(); + // Update flight phases + this.flightPhases.update(deltaTime); + + // Play sounds + this.soundManager.onUpdate(deltaTime); + // Write pulse nodes for buffered inputs this.toConfigPulseNode.write(this.toConfigInputBuffer.read(), deltaTime); this.clrPulseNode.write(this.clearButtonInputBuffer.read(), deltaTime); @@ -1845,24 +1830,24 @@ export class FwsCore { // Inputs update this.flightPhaseEndedPulseNode.write(false, deltaTime); - this.fwcFlightPhase.set(SimVar.GetSimVarValue('L:A32NX_FWC_FLIGHT_PHASE', 'Enum')); - const phase3 = this.fwcFlightPhase.get() === 3; - const phase6 = this.fwcFlightPhase.get() === 6; + const phase3 = this.flightPhase.get() === 3; + const phase6 = this.flightPhase.get() === 6; this.flightPhase3PulseNode.write(phase3, deltaTime); // flight phase convenience vars - this.flightPhase128.set([1, 2, 8].includes(this.fwcFlightPhase.get())); - this.flightPhase23.set([2, 3].includes(this.fwcFlightPhase.get())); - this.flightPhase345.set([3, 4, 5].includes(this.fwcFlightPhase.get())); + this.flightPhase128.set([1, 2, 8].includes(this.flightPhase.get())); + this.flightPhase23.set([2, 3].includes(this.flightPhase.get())); + this.flightPhase345.set([3, 4, 5].includes(this.flightPhase.get())); this.flightPhase34567.set( - this.flightPhase345.get() || this.fwcFlightPhase.get() === 6 || this.fwcFlightPhase.get() === 7, + this.flightPhase345.get() || this.flightPhase.get() === 6 || this.flightPhase.get() === 7, ); - this.flightPhase1211.set([1, 2, 11].includes(this.fwcFlightPhase.get())); - this.flightPhase89.set([8, 9].includes(this.fwcFlightPhase.get())); - this.flightPhase910.set([9, 10].includes(this.fwcFlightPhase.get())); - const flightPhase6789 = [6, 7, 8, 9].includes(this.fwcFlightPhase.get()); - const flightPhase112 = [1, 12].includes(this.fwcFlightPhase.get()); + this.flightPhase1211.set([1, 2, 11].includes(this.flightPhase.get())); + this.flightPhase89.set([8, 9].includes(this.flightPhase.get())); + this.flightPhase910.set([9, 10].includes(this.flightPhase.get())); + const flightPhase6789 = [6, 7, 8, 9].includes(this.flightPhase.get()); + const flightPhase112 = [1, 12].includes(this.flightPhase.get()); + const flightPhase189 = [1, 8, 9].includes(this.flightPhase.get()); - this.phase815MinConfNode.write(this.fwcFlightPhase.get() === 8, deltaTime); + this.phase815MinConfNode.write(this.flightPhase.get() === 8, deltaTime); this.phase112.set(flightPhase112); // TO CONFIG button @@ -2011,12 +1996,12 @@ export class FwsCore { const yLoPressure = !yellowSysPressurised; this.eng1Or2RunningAndPhaseConfirmationNode.write( - this.engine1Running.get() || this.engine2Running.get() || ![1, 2, 11, 12].includes(this.fwcFlightPhase.get()), + this.engine1Running.get() || this.engine2Running.get() || ![1, 2, 11, 12].includes(this.flightPhase.get()), deltaTime, ); this.eng3Or4RunningAndPhaseConfirmationNode.write( - this.engine3Running.get() || this.engine4Running.get() || ![1, 2, 11, 12].includes(this.fwcFlightPhase.get()), + this.engine3Running.get() || this.engine4Running.get() || ![1, 2, 11, 12].includes(this.flightPhase.get()), deltaTime, ); @@ -2097,7 +2082,7 @@ export class FwsCore { !this.greenRsvLoAirPressure.get() && !this.greenRsvOverheat.get() && !this.greenAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng1APumpAuto.get(), deltaTime, ); @@ -2114,7 +2099,7 @@ export class FwsCore { !this.greenRsvLoAirPressure.get() && !this.greenRsvOverheat.get() && !this.greenAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng1BPumpAuto.get(), deltaTime, ); @@ -2135,7 +2120,7 @@ export class FwsCore { !this.greenRsvLoAirPressure.get() && !this.greenRsvOverheat.get() && !this.greenAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng2APumpAuto.get(), deltaTime, ); @@ -2153,7 +2138,7 @@ export class FwsCore { !this.greenRsvLoAirPressure.get() && !this.greenRsvOverheat.get() && !this.greenAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng2BPumpAuto.get(), deltaTime, ); @@ -2173,7 +2158,7 @@ export class FwsCore { !this.yellowRsvLoAirPressure.get() && !this.yellowRsvOverheat.get() && !this.yellowAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng3APumpAuto.get(), deltaTime, ); @@ -2194,7 +2179,7 @@ export class FwsCore { !this.yellowRsvLoAirPressure.get() && !this.yellowRsvOverheat.get() && !this.yellowAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng3BPumpAuto.get(), deltaTime, ); @@ -2216,7 +2201,7 @@ export class FwsCore { !this.yellowRsvLoAirPressure.get() && !this.yellowRsvOverheat.get() && !this.yellowAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng4APumpAuto, deltaTime, ); @@ -2235,7 +2220,7 @@ export class FwsCore { !this.yellowRsvLoAirPressure.get() && !this.yellowRsvOverheat.get() && !this.yellowAbnormLoPressure.get() && - this.fwcFlightPhase.get() === 2 && + this.flightPhase.get() === 2 && !this.eng4BPumpAuto.get(), deltaTime, ); @@ -2284,8 +2269,9 @@ export class FwsCore { this.adirsRemainingAlignTime.set(SimVar.GetSimVarValue('L:A32NX_ADIRS_REMAINING_IR_ALIGNMENT_TIME', 'Seconds')); // TODO use GPS alt if ADRs not available - const pressureAltitude = - adr1PressureAltitude.valueOr(null) ?? adr2PressureAltitude.valueOr(null) ?? adr3PressureAltitude.valueOr(null); + this.adrPressureAltitude.set( + adr1PressureAltitude.valueOr(null) ?? adr2PressureAltitude.valueOr(null) ?? adr3PressureAltitude.valueOr(null), + ); this.ir1Align.set( this.ir1MaintWord.bitValueOr(16, false) || this.ir1MaintWord.bitValueOr(17, false) || @@ -2319,6 +2305,7 @@ export class FwsCore { /* V1 callout */ const v1 = SimVar.GetSimVarValue('L:AIRLINER_V1_SPEED', SimVarValueType.Knots); const v1Threshold = v1 - 4; + const v1ConfirmNodeStatus = this.v1SpeedConfirmNode.read(); this.v1SpeedConfirmNode.write( v1 && (this.adr1Cas.get().valueOr(0) > v1Threshold || @@ -2326,7 +2313,14 @@ export class FwsCore { this.adr3Cas.get().valueOr(0) > v1Threshold), deltaTime, ); - this.v1CalloutOutput.set(this.fwcFlightPhase.get() === 4 && this.v1SpeedConfirmNode.read()); + if ( + this.flightPhase.get() === 4 && + this.v1SpeedConfirmNode.read() && + !v1ConfirmNodeStatus && + this.v1SpeedConfirmNode.read() + ) { + this.soundManager.enqueueSound('v1'); + } /* LANDING GEAR AND LIGHTS acquisition */ @@ -2447,14 +2441,14 @@ export class FwsCore { this.autoPilotOffVoluntaryDiscPulse.write(voluntaryApDisc, deltaTime); this.autoPilotOffVoluntaryFirstCavalryChargeActive.write(this.autoPilotOffVoluntaryDiscPulse.read(), deltaTime); - this.autoPilotOffSendTripleClickAfterFirstCavalryCharge.write( + this.autoPilotOffVoluntaryFirstCavalryChargeActivePulse.write( this.autoPilotOffVoluntaryFirstCavalryChargeActive.read(), deltaTime, ); - SimVar.SetSimVarValue( - 'L:A32NX_FMA_TRIPLE_CLICK', - 'Bool', - this.autoPilotOffSendTripleClickAfterFirstCavalryCharge.read(), + + this.autoPilotFirstCavalryStillWithinFirst0p3s.write( + this.autoPilotOffVoluntaryFirstCavalryChargeActive.read(), + deltaTime, ); this.autoPilotInstinctiveDiscPressedTwiceInLast1p9Sec.write( @@ -2466,13 +2460,14 @@ export class FwsCore { this.autoPilotOffVoluntaryMemory.write( this.autoPilotOffVoluntaryDiscPulse.read(), apEngaged || - this.autoPilotInstinctiveDiscPressedTwiceInLast1p9Sec.read() || + (this.autoPilotInstinctiveDiscPressedTwiceInLast1p9Sec.read() && + !this.autoPilotFirstCavalryStillWithinFirst0p3s.read()) || !this.autoPilotOffVoluntaryEndAfter1p9s.read(), ); const discPbPressedAfterDisconnection = !this.autoPilotDisengagedInstantPulse.read() && - (this.autoPilotInstinctiveDiscPressedPulse.read() || masterWarningButtonLeft || masterCautionButtonRight); + (this.autoPilotInstinctiveDiscPressedPulse.read() || masterWarningButtonLeft || masterWarningButtonRight); this.autoPilotOffUnacknowledged.write( this.autoPilotDisengagedInstantPulse.read(), @@ -2489,21 +2484,37 @@ export class FwsCore { if (this.autoPilotDisengagedInstantPulse.read()) { // Request quiet CRC one time this.requestMasterWarningFromApOff = true; - this.auralCavalryChargeActive.set(true); - this.fwsAuralVolume.set(FwcAuralVolume.Attenuated); + this.soundManager.setVolume(FwsAuralVolume.Attenuated); + this.soundManager.enqueueSound('cavalryChargeCont'); // On the A380, first cav charge can be cancelled early } - if (!this.autoPilotOffVoluntaryFirstCavalryChargeActive.read()) { - this.auralCavalryChargeActive.set(false); - this.fwsAuralVolume.set(FwcAuralVolume.Full); + if (this.autoPilotOffVoluntaryFirstCavalryChargeActivePulse.read()) { + this.soundManager.dequeueSound('cavalryChargeCont'); + this.soundManager.setVolume(FwsAuralVolume.Full); } if (!this.autoPilotOffVoluntaryMemory.read() && !this.autoPilotOffInvoluntaryMemory.read()) { this.requestMasterWarningFromApOff = false; - this.auralCavalryChargeActive.set(false); - this.fwsAuralVolume.set(FwcAuralVolume.Full); + this.soundManager.dequeueSound('cavalryChargeCont'); + this.soundManager.setVolume(FwsAuralVolume.Full); } this.autoPilotInstinctiveDiscPressedPulse.write(false, deltaTime); + // approach capability downgrade. Debounce first, then suppress for a certain amount of time + // (to avoid multiple triple clicks, and a delay which is too long) + const newCapability = SimVar.GetSimVarValue('L:A32NX_APPROACH_CAPABILITY', SimVarValueType.Number); + const capabilityDowngrade = newCapability < this.approachCapability.get() && newCapability > 0; + this.approachCapabilityDowngradeDebounce.write( + capabilityDowngrade && flightPhase189 && !this.approachCapabilityDowngradeSuppress.read(), + deltaTime, + ); + this.approachCapabilityDowngradeDebouncePulse.write(this.approachCapabilityDowngradeDebounce.read(), deltaTime); + this.approachCapabilityDowngradeSuppress.write(this.approachCapabilityDowngradeDebouncePulse.read(), deltaTime); + // Capability downgrade after debounce --> triple click + if (this.approachCapabilityDowngradeDebouncePulse.read()) { + this.soundManager.enqueueSound('tripleClick'); + } + this.approachCapability.set(newCapability); + // A/THR OFF const aThrEngaged = this.autoThrustStatus.get() === 2 || this.autoThrustMode.get() !== 0; this.autoThrustDisengagedInstantPulse.write(aThrEngaged, deltaTime); @@ -2563,6 +2574,7 @@ export class FwsCore { if (!this.autoBrakeDeactivatedNode.read()) { this.autoBrakeOffMemoInhibited = false; this.requestMasterCautionFromABrkOff = false; + this.autoBrakeOffAuralTriggered = false; } this.autoBrakeOffAuralConfirmNode.write( @@ -2583,11 +2595,10 @@ export class FwsCore { // FIXME double callout if ABRK fails this.autoBrakeOff.set(autoBrakeOffShouldTrigger); - SimVar.SetSimVarValue( - 'L:A32NX_AUDIO_AUTOBRAKE_OFF', - SimVarValueType.Bool, - autoBrakeOffShouldTrigger && this.autoBrakeOffAuralConfirmNode.read(), - ); + if (autoBrakeOffShouldTrigger && this.autoBrakeOffAuralConfirmNode.read() && !this.autoBrakeOffAuralTriggered) { + this.soundManager.enqueueSound('autoBrakeOff'); + this.autoBrakeOffAuralTriggered = true; + } // Engine Logic this.thrustLeverNotSet.set(this.autothrustLeverWarningFlex.get() || this.autothrustLeverWarningToga.get()); @@ -2717,11 +2728,11 @@ export class FwsCore { /* 21 - AIR CONDITIONING AND PRESSURIZATION */ - this.flightLevel.set(Math.round(pressureAltitude / 100)); + this.flightLevel.set(Math.round(this.adrPressureAltitude.get() / 100)); - this.phase8ConfirmationNode60.write(this.fwcFlightPhase.get() === 8, deltaTime); + this.phase8ConfirmationNode60.write(this.flightPhase.get() === 8, deltaTime); - this.phase8ConfirmationNode180.write(this.fwcFlightPhase.get() === 8, deltaTime); + this.phase8ConfirmationNode180.write(this.flightPhase.get() === 8, deltaTime); this.fdac1Channel1Failure.set(SimVar.GetSimVarValue('L:A32NX_COND_FDAC_1_CHANNEL_1_FAILURE', 'bool')); this.fdac1Channel2Failure.set(SimVar.GetSimVarValue('L:A32NX_COND_FDAC_1_CHANNEL_2_FAILURE', 'bool')); @@ -3225,7 +3236,7 @@ export class FwsCore { const flapsNotInToPos = this.flapsSuperiorTo26Deg.get() || this.flapsInferiorTo8Deg.get(); this.flapConfigSr.write( this.flightPhase345.get() && flapsNotInToPos, - !flapsNotInToPos || phase6 || this.fwcFlightPhase.get() === 7, + !flapsNotInToPos || phase6 || this.flightPhase.get() === 7, ); this.flapsNotTo.set(this.flightPhase1211.get() && flapsNotInToPos); this.flapsNotToMemo.set(this.flapConfigSr.read() || this.flapsNotTo.get()); @@ -3240,7 +3251,7 @@ export class FwsCore { const slatsNotInToPos = this.slatsInferiorTo20Deg.get(); this.slatConfigSr.write( this.flightPhase345.get() && slatsNotInToPos, - !slatsNotInToPos || phase6 || this.fwcFlightPhase.get() === 7, + !slatsNotInToPos || phase6 || this.flightPhase.get() === 7, ); this.slatsNotTo.set(this.flightPhase1211.get() && slatsNotInToPos); this.slatConfigAural.set( @@ -3254,7 +3265,7 @@ export class FwsCore { const speedbrakesNotInToPos = fcdc1DiscreteWord4.bitValueOr(28, false) || fcdc2DiscreteWord4.bitValueOr(28, false); this.speedbrakesConfigSr.write( this.flightPhase345.get() && speedbrakesNotInToPos, - !speedbrakesNotInToPos || phase6 || this.fwcFlightPhase.get() === 7, + !speedbrakesNotInToPos || phase6 || this.flightPhase.get() === 7, ); this.speedbrakesNotTo.set(this.flightPhase1211.get() && speedbrakesNotInToPos); this.speedbrakesConfigAural.set( @@ -3283,7 +3294,7 @@ export class FwsCore { ); // taxi in flap 0 one minute check - this.taxiInFlap0Check.write(this.slatFlapSelectionS0F0 && this.fwcFlightPhase.get() == 11, deltaTime); + this.taxiInFlap0Check.write(this.slatFlapSelectionS0F0 && this.flightPhase.get() == 11, deltaTime); this.flapsMcduDisagree.set( (flapsMcduPos1Disagree || flapsMcduPos2Disagree || flapsMcduPos3Disagree) && @@ -3303,7 +3314,7 @@ export class FwsCore { const pitchConfigInPhase3or4or5 = this.flightPhase345.get() && pitchConfig; this.pitchConfigInPhase3or4or5Sr.write( pitchConfigInPhase3or4or5, - phase6 || this.fwcFlightPhase.get() === 7 || !pitchConfig, + phase6 || this.flightPhase.get() === 7 || !pitchConfig, ); this.pitchTrimNotToAudio.set(pitchConfigTestInPhase1211 || pitchConfigInPhase3or4or5); this.pitchTrimNotToWarning.set(pitchConfigTestInPhase1211 || this.pitchConfigInPhase3or4or5Sr.read()); @@ -3349,7 +3360,7 @@ export class FwsCore { (adr1PressureAltitude.valueOr(0) >= 22000 || adr2PressureAltitude.valueOr(0) >= 22000 || adr3PressureAltitude.valueOr(0) >= 22000) && - this.fwcFlightPhase.get() === 8 && + this.flightPhase.get() === 8 && !this.slatFlapSelectionS0F0, ); @@ -3361,13 +3372,13 @@ export class FwsCore { deltaTime, ); this.speedBrakeCaution1Confirm.write( - this.fwcFlightPhase.get() === 8 && + this.flightPhase.get() === 8 && this.speedBrakeCommand50sConfirm.read() && !this.engAboveIdleWithSpeedBrakeConfirm.read(), deltaTime, ); const speedBrakeCaution1 = this.speedBrakeCaution1Confirm.read(); - const speedBrakeCaution2 = this.fwcFlightPhase.get() === 9 && this.speedBrakeCommand5sConfirm.read(); + const speedBrakeCaution2 = this.flightPhase.get() === 9 && this.speedBrakeCommand5sConfirm.read(); // FIXME FCU does not provide the bit, so we synthesize it const apVerticalMode = SimVar.GetSimVarValue('L:A32NX_FMA_VERTICAL_MODE', 'number'); const apTcasRaNoseUp = @@ -3377,7 +3388,7 @@ export class FwsCore { this.apTcasRaNoseUpConfirm.write(apTcasRaNoseUp, deltaTime); this.speedBrakeCaution3Confirm.write( this.speedBrakeCommand.get() && - this.fwcFlightPhase.get() === 8 && + this.flightPhase.get() === 8 && oneEngineAboveMinPower && this.apTcasRaNoseUpConfirm.read(), deltaTime, @@ -3410,7 +3421,7 @@ export class FwsCore { const lgDown = this.lgciu1DiscreteWord1.bitValueOr(29, false) || (this.lgciu2DiscreteWord1.bitValueOr(29, false) && mainGearDownlocked); - this.phase104s5Trigger.write(this.fwcFlightPhase.get() === 10, deltaTime); + this.phase104s5Trigger.write(this.flightPhase.get() === 10, deltaTime); this.groundSpoiler5sDelayed.write( fcdc1DiscreteWord4.bitValueOr(27, false) || fcdc2DiscreteWord4.bitValueOr(27, false), deltaTime, @@ -3433,7 +3444,7 @@ export class FwsCore { ); // l/g gear not down - const fwcFlightPhase = this.fwcFlightPhase.get(); + const fwcFlightPhase = this.flightPhase.get(); const flightPhase4567 = fwcFlightPhase === 4 || fwcFlightPhase === 5 || fwcFlightPhase === 6 || fwcFlightPhase === 7; const flightPhase8 = fwcFlightPhase === 8; @@ -3444,7 +3455,7 @@ export class FwsCore { this.radioHeight3.valueOr(Infinity), ) < 750; const altInhibit = - (pressureAltitude ?? 0) > 18500 && + (this.adrPressureAltitude.get() ?? 0) > 18500 && !this.radioHeight1.isNoComputedData() && !this.radioHeight1.isNormalOperation() && !this.radioHeight2.isNoComputedData() && @@ -3753,7 +3764,7 @@ export class FwsCore { (this.engine1State.get() === 0 && this.engine2State.get() === 0)), ); - const flightPhase = this.fwcFlightPhase.get(); + const flightPhase = this.flightPhase.get(); let tempMemoArrayLeft: string[] = []; let tempMemoArrayRight: string[] = []; const allFailureKeys: string[] = []; @@ -3938,7 +3949,7 @@ export class FwsCore { } if (value.auralWarning?.get() === FwcAuralWarning.CavalryCharge) { - this.auralCavalryChargeActive.set(true); + this.soundManager.enqueueSound('cavalryChargeCont'); } } @@ -4083,26 +4094,17 @@ export class FwsCore { !ewdLimitationsApprLdgKeys.length, ); - // This does not consider interrupting c-chord, priority of synthetic voice etc. - const chimeRequested = this.auralSingleChimePending || this.requestSingleChimeFromAThrOff; - if ( - chimeRequested && - !this.auralCrcActive.get() && - !this.auralCavalryChargeActive.get() && - !this.auralSingleChimeInhibitTimer.isPending() - ) { + const chimeRequested = + (this.auralSingleChimePending || this.requestSingleChimeFromAThrOff) && !this.auralCrcActive.get(); + if (chimeRequested && !this.auralSingleChimeInhibitTimer.isPending()) { this.auralSingleChimePending = false; this.requestSingleChimeFromAThrOff = false; - SimVar.SetSimVarValue('L:A32NX_FWC_SC', 'bool', true); + this.soundManager.enqueueSound('singleChime'); // there can only be one SC per 2 seconds, non-cumulative, so clear any pending ones at the end of that inhibit period this.auralSingleChimeInhibitTimer.schedule( () => (this.auralSingleChimePending = false), FwsCore.AURAL_SC_INHIBIT_TIME, ); - this.auralSingleChimePlayingTimer.schedule( - () => SimVar.SetSimVarValue('L:A32NX_FWC_SC', 'bool', false), - FwsCore.AURAL_SC_PLAY_TIME, - ); } this.normalChecklists.update(); @@ -4126,7 +4128,7 @@ export class FwsCore { const w = Arinc429Word.fromSimVarValue('L:A32NX_ROW_ROP_WORD_1'); // ROW - SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_RWY_TOO_SHORT', 'bool', w.bitValueOr(15, false)); + this.soundManager.handleSoundCondition('runwayTooShort', w.bitValueOr(15, false)); // ROP // MAX BRAKING, only for manual braking, if maximum pedal braking is not applied @@ -4134,20 +4136,19 @@ export class FwsCore { SimVar.GetSimVarValue('L:A32NX_LEFT_BRAKE_PEDAL_INPUT', 'number') > 90 || SimVar.GetSimVarValue('L:A32NX_RIGHT_BRAKE_PEDAL_INPUT', 'number') > 90; const maxBraking = w.bitValueOr(13, false) && !maxBrakingSet; - SimVar.SetSimVarValue('L:A32NX_AUDIO_ROP_MAX_BRAKING', 'bool', maxBraking); + this.soundManager.handleSoundCondition('brakeMaxBraking', maxBraking); // SET MAX REVERSE, if not already max. reverse set and !MAX_BRAKING const maxReverseSet = SimVar.GetSimVarValue('L:XMLVAR_Throttle1Position', 'number') < 0.1 && SimVar.GetSimVarValue('L:XMLVAR_Throttle2Position', 'number') < 0.1; const maxReverse = (w.bitValueOr(12, false) || w.bitValueOr(13, false)) && !maxReverseSet; - SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_SET_MAX_REVERSE', 'bool', !maxBraking && maxReverse); + this.soundManager.handleSoundCondition('setMaxReverse', !maxBraking && maxReverse); // At 80kt, KEEP MAX REVERSE once, if max. reversers deployed const ias = SimVar.GetSimVarValue('AIRSPEED INDICATED', 'knots'); - SimVar.SetSimVarValue( - 'L:A32NX_AUDIO_ROP_KEEP_MAX_REVERSE', - 'bool', + this.soundManager.handleSoundCondition( + 'keepMaxReverse', ias <= 80 && ias > 4 && (w.bitValueOr(12, false) || w.bitValueOr(13, false)), ); } diff --git a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsFlightPhases.ts b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsFlightPhases.ts new file mode 100644 index 00000000000..ef8066cbcd8 --- /dev/null +++ b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsFlightPhases.ts @@ -0,0 +1,534 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable camelcase */ +import { + Arinc429Register, + NXLogicConfirmNode, + NXLogicMemoryNode, + NXLogicTriggeredMonostableNode, +} from '@flybywiresim/fbw-sdk'; +import { FwsCore } from 'systems-host/systems/FlightWarningSystem/FwsCore'; + +export enum FwcFlightPhase { + ElecPwr = 1, + FirstEngineStarted = 2, + SecondEngineTakeOffPower = 3, + AtOrAboveEightyKnots = 4, + AtOrAboveV1 = 5, + LiftOff = 6, + AtOrAbove400Feet = 7, + AtOrAbove1500FeetTo800Feet = 8, + AtOrBelow800Feet = 9, + TouchDown = 10, + AtOrBelowEightyKnots = 11, + EnginesShutdown = 12, +} + +// FIXME use subjects from FwsCore +/** + * This nearly a 1:1 port from the A32NX's FWC serves as temporary replacement, until a more sophisticated system simulation is in place. + */ +export class FwsFlightPhases { + toConfigTest: boolean; + + ldgMemo: boolean; + + toMemo: boolean; + + gndMemo: NXLogicConfirmNode; + + oneEngineRunningConf: NXLogicConfirmNode; + + speedAbove80KtsMemo: NXLogicMemoryNode; + + speedAboveV1Memo: NXLogicMemoryNode; + + mctMemo: NXLogicConfirmNode; + + firePBOutConf: NXLogicConfirmNode; + + firePBOutMemo: NXLogicTriggeredMonostableNode; + + firePBClear12: NXLogicMemoryNode; + + phase112Memo: NXLogicTriggeredMonostableNode; + + phase10GroundMemo: NXLogicTriggeredMonostableNode; + + ac80KtsMemo: NXLogicTriggeredMonostableNode; + + prevPhase11InvertMemo: NXLogicTriggeredMonostableNode; + + twoEnginesTOPowerInvertMemo: NXLogicTriggeredMonostableNode; + + phase9Nvm: NXLogicMemoryNode; + + prevPhase11: boolean; + + groundImmediateMemo: NXLogicTriggeredMonostableNode; + + phase6Memo: NXLogicTriggeredMonostableNode; + + phase7Memo: NXLogicTriggeredMonostableNode; + + phase89Memo: NXLogicTriggeredMonostableNode; + + memoTo_conf01: NXLogicConfirmNode; + + memoTo_memo: NXLogicMemoryNode; + + memoLdgMemo_conf01: NXLogicConfirmNode; + + memoLdgMemo_inhibit: NXLogicMemoryNode; + + memoLdgMemo_conf02: NXLogicConfirmNode; + + memoLdgMemo_below2000ft: NXLogicMemoryNode; + + memoToInhibit_conf01: NXLogicConfirmNode; + + memoLdgInhibit_conf01: NXLogicConfirmNode; + + previousTargetAltitude: number; + + _wasBelowThreshold: boolean; + + _wasAboveThreshold: boolean; + + _wasInRange: boolean; + + _wasReach200ft: boolean; + + _cChordShortWasTriggered: boolean; + + aircraft: Aircraft; + + private readonly adrAltitude = Arinc429Register.empty(); + + constructor(private fws: FwsCore) { + // momentary + this.toConfigTest = null; + + // persistent + this.ldgMemo = null; + this.toMemo = null; + + this.gndMemo = new NXLogicConfirmNode(1); + + this.oneEngineRunningConf = new NXLogicConfirmNode(30); + + this.speedAbove80KtsMemo = new NXLogicMemoryNode(true); + + this.speedAboveV1Memo = new NXLogicMemoryNode(); + + this.mctMemo = new NXLogicConfirmNode(60, false); + + this.firePBOutConf = new NXLogicConfirmNode(0.2); + this.firePBOutMemo = new NXLogicTriggeredMonostableNode(2); + this.firePBClear12 = new NXLogicMemoryNode(false); + this.phase112Memo = new NXLogicTriggeredMonostableNode(300); + this.phase10GroundMemo = new NXLogicTriggeredMonostableNode(2); + this.ac80KtsMemo = new NXLogicTriggeredMonostableNode(2); + this.prevPhase11InvertMemo = new NXLogicTriggeredMonostableNode(3, false); + this.twoEnginesTOPowerInvertMemo = new NXLogicTriggeredMonostableNode(1, false); + this.phase9Nvm = new NXLogicMemoryNode(true, true); + this.prevPhase11 = false; + + this.groundImmediateMemo = new NXLogicTriggeredMonostableNode(2); + this.phase6Memo = new NXLogicTriggeredMonostableNode(15); + this.phase7Memo = new NXLogicTriggeredMonostableNode(120); + this.phase89Memo = new NXLogicTriggeredMonostableNode(180); + + this.memoTo_conf01 = new NXLogicConfirmNode(120, true); + this.memoTo_memo = new NXLogicMemoryNode(false); + + this.memoLdgMemo_conf01 = new NXLogicConfirmNode(1, true); + this.memoLdgMemo_inhibit = new NXLogicMemoryNode(false); + this.memoLdgMemo_conf02 = new NXLogicConfirmNode(10, true); + this.memoLdgMemo_below2000ft = new NXLogicMemoryNode(true); + + this.memoToInhibit_conf01 = new NXLogicConfirmNode(3, true); + + this.memoLdgInhibit_conf01 = new NXLogicConfirmNode(3, true); + + // altitude warning + this.previousTargetAltitude = NaN; + this._wasBelowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._wasReach200ft = false; + } + + update(deltaTime: number) { + this._updateFlightPhase(deltaTime); + this._updateTakeoffMemo(deltaTime); + this._updateLandingMemo(deltaTime); + this._updateAltitudeWarning(); + } + + _updateFlightPhase(_deltaTime: number) { + const raHeight1Invalid = this.fws.radioHeight1.isFailureWarning() || this.fws.radioHeight1.isNoComputedData(); + const raHeight2Invalid = this.fws.radioHeight2.isFailureWarning() || this.fws.radioHeight2.isNoComputedData(); + let radioHeight; + if (raHeight1Invalid) { + if (raHeight2Invalid) { + radioHeight = this.fws.radioHeight3; + } else { + radioHeight = this.fws.radioHeight2; + } + } else { + radioHeight = this.fws.radioHeight1; + } + // 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 = + this.fws.N1Eng1.get() > 15 || + this.fws.N1Eng2.get() > 15 || + this.fws.N1Eng3.get() > 15 || + this.fws.N1Eng4.get() > 15; + const oneEngineRunning = this.oneEngineRunningConf.write(oneEngRunning, _deltaTime); + const noEngineRunning = !oneEngineRunning; + const hFail = + this.fws.radioHeight1.isFailureWarning() && + this.fws.radioHeight2.isFailureWarning() && + this.fws.radioHeight3.isFailureWarning(); + const adcTestInhib = false; + + const groundImmediate = Simplane.getIsGrounded(); + const ground = this.gndMemo.write(groundImmediate, _deltaTime); + + const ias = this.fws.computedAirSpeedToNearest2.get(); + const acSpeedAbove80kts = this.speedAbove80KtsMemo.write(ias > 83, ias < 77); + + const v1 = SimVar.GetSimVarValue('L:AIRLINER_V1_SPEED', 'knots'); + let acAboveV1: boolean; + if (v1) { + acAboveV1 = this.speedAboveV1Memo.write(ias > v1 + 3, ias < v1 - 3); + } else { + acAboveV1 = false; + } + + const hAbv1500 = radioHeight.isNoComputedData() || radioHeight.value > 1500; + const hAbv800 = radioHeight.isNoComputedData() || radioHeight.value > 800; + const hAbv400 = radioHeight.isNoComputedData() || radioHeight.value > 400; + + const eng1TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:1', 'number'); + const eng1TLAFTO = SimVar.GetSimVarValue('L:A32NX_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 eng1MCL = eng1TLA > 22.9; + const eng1SupMCT = !(eng1TLA < 36.7); + + 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 eng2MCL = eng2TLA > 22.9; + const eng2SupMCT = !(eng2TLA < 36.7); + + const eng3TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:3', 'number'); + const eng3TLAFTO = eng1TLAFTO; // until we have proper FADECs + const eng3MCT = eng3TLA > 33.3 && eng3TLA < 36.7; + const eng3TLAFullPwr = eng3TLA > 43.3; + const eng3MCL = eng3TLA > 22.9; + const eng3SupMCT = !(eng3TLA < 36.7); + + const eng4TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:4', 'number'); + const eng4TLAFTO = eng1TLAFTO; // until we have proper FADECs + const eng4MCT = eng4TLA > 33.3 && eng4TLA < 36.7; + const eng4TLAFullPwr = eng3TLA > 43.3; + const eng4MCL = eng4TLA > 22.9; + const eng4SupMCT = !(eng4TLA < 36.7); + + const twoEnginesMcl = [eng1MCL, eng2MCL, eng3MCL, eng4MCL].filter(Boolean).length >= 2; + const eng1TOPowerSignal = (eng1TLAFTO && eng1MCT) || eng1TLAFullPwr || eng1SupMCT; + const eng2TOPowerSignal = (eng2TLAFTO && eng2MCT) || eng2TLAFullPwr || eng2SupMCT; + const eng3TOPowerSignal = (eng3TLAFTO && eng3MCT) || eng3TLAFullPwr || eng3SupMCT; + const eng4TOPowerSignal = (eng4TLAFTO && eng4MCT) || eng4TLAFullPwr || eng4SupMCT; + + const twoEnginesTOPowerSignal = + [eng1TOPowerSignal, eng2TOPowerSignal, eng3TOPowerSignal, eng4TOPowerSignal].filter(Boolean).length >= 2; + + const twoEnginesTOPower = + twoEnginesTOPowerSignal || + (this.mctMemo.write(twoEnginesTOPowerSignal, _deltaTime) && !hAbv1500 && twoEnginesMcl); + + const eng1FirePbMemo = this.firePBOutMemo.write( + this.firePBOutConf.write(this.fws.fireButtonEng1.get(), _deltaTime), + _deltaTime, + ); + const resetFirePbClear12 = eng1FirePbMemo && ground; + + const phase10 = + (this.phase10GroundMemo.write(groundImmediate, _deltaTime) || groundImmediate) && + !twoEnginesTOPower && + acSpeedAbove80kts; + + const phase345Cond = ground && twoEnginesTOPower; + const phase3 = !acSpeedAbove80kts && oneEngRunning && phase345Cond; + const phase4 = acSpeedAbove80kts && phase345Cond && !acAboveV1; + const phase5 = acSpeedAbove80kts && phase345Cond && acAboveV1; + + const setPhase11Nvm = phase3 || phase10; + const resetPhase11Nvm = + (!this.ac80KtsMemo.write(!acSpeedAbove80kts, _deltaTime) && + ((ground && this.prevPhase11InvertMemo.write(this.prevPhase11, _deltaTime)) || + resetFirePbClear12 || + (ground && this.twoEnginesTOPowerInvertMemo.write(twoEnginesTOPower, _deltaTime))) && + !this.prevPhase11) || + adcTestInhib; + const phase11Nvm = this.phase9Nvm.write(setPhase11Nvm, resetPhase11Nvm); // S* / R (NVM) + const phase211Cond = ground && !twoEnginesTOPower && !acSpeedAbove80kts; + const phase11 = oneEngRunning && phase11Nvm && phase211Cond; + const phase2 = phase211Cond && !phase11Nvm && oneEngRunning; + + const phase112MemoA = this.firePBClear12.write(phase11, resetFirePbClear12); // S / R* + const phase112Cond = !phase11 && noEngineRunning && groundImmediate; + const phase112Memo = this.phase112Memo.write(phase112MemoA && phase112Cond, _deltaTime); + const phase1 = phase112Cond && !phase112Memo; + const phase12 = phase112Cond && phase112Memo; + + this.prevPhase11 = phase11; + + const ground2sMemorized = this.groundImmediateMemo.write(groundImmediate, _deltaTime) || groundImmediate; + + const phase6Cond = !hAbv400 && twoEnginesTOPower && !hFail && !ground2sMemorized; + const phase6 = this.phase6Memo.write(phase6Cond, _deltaTime) && phase6Cond; + + const phase7Cond = !phase6 && !hAbv1500 && twoEnginesTOPower && !hFail && !ground2sMemorized; + const phase7 = this.phase7Memo.write(phase7Cond, _deltaTime) && phase7Cond; + + const phase89Cond = !ground2sMemorized && !hFail && !twoEnginesTOPower && !hAbv1500 && !hAbv800; + const phase89Memo = this.phase89Memo.write(phase89Cond, _deltaTime) && phase89Cond; + + const phase8 = !phase7 && !ground2sMemorized && !phase89Memo; + const phase9 = phase89Memo && !phase10; + + // consolidate into single variable (just to be safe) + const phases = [phase1, phase2, phase3, phase4, phase5, phase6, phase7, phase8, phase9, phase10, phase11, phase12]; + + if (this.fws.flightPhase.get() === null && phases.indexOf(true) !== -1) { + // if we aren't initialized, just grab the first one that is valid + this.fws.flightPhase.set(phases.indexOf(true) + 1); + console.log(`FWC flight phase: ${this.fws.flightPhase.get()}`); + 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.fws.flightPhase.get()) { + console.log(`FWC flight phase: ${this.fws.flightPhase.get()} => ${activePhases[0]}`); + this.fws.flightPhase.set(activePhases[0]); + } + return; + } + + // the mixed case => warn + if (activePhases.length > 1) { + if (activePhases.indexOf(this.fws.flightPhase.get()) !== -1) { + // if the currently active one is present, keep it + return; + } + // pick the earliest one + this.fws.flightPhase.set(activePhases[0]); + return; + } + + // otherwise, no flight phase is valid => warn + if (this.fws.flightPhase.get() === null) { + this.fws.flightPhase.set(null); + } + } + + _updateTakeoffMemo(_deltaTime: number) { + const setFlightPhaseMemo = this.fws.flightPhase.get() === FwcFlightPhase.FirstEngineStarted && this.toConfigTest; + const resetFlightPhaseMemo = + this.fws.flightPhase.get() === FwcFlightPhase.EnginesShutdown || + this.fws.flightPhase.get() === FwcFlightPhase.SecondEngineTakeOffPower || + this.fws.flightPhase.get() === FwcFlightPhase.ElecPwr || + this.fws.flightPhase.get() === FwcFlightPhase.AtOrAbove1500FeetTo800Feet; + const flightPhaseMemo = this.memoTo_memo.write(setFlightPhaseMemo, resetFlightPhaseMemo); + + this.fws.engine1Running.get(); + const toTimerElapsed = this.memoTo_conf01.write( + this.fws.engine1Running.get() && + this.fws.engine2Running.get() && + this.fws.engine3Running.get() && + this.fws.engine4Running.get(), + _deltaTime, + ); + + this.toMemo = + flightPhaseMemo || (this.fws.flightPhase.get() === FwcFlightPhase.FirstEngineStarted && toTimerElapsed); + SimVar.SetSimVarValue('L:A32NX_FWC_TOMEMO', 'Bool', this.toMemo); + } + + _updateLandingMemo(_deltaTime: number) { + const radioHeight1Invalid = this.fws.radioHeight1.isFailureWarning() || this.fws.radioHeight1.isNoComputedData(); + const radioHeight2Invalid = this.fws.radioHeight2.isFailureWarning() || this.fws.radioHeight2.isNoComputedData(); + const radioHeight3Invalid = this.fws.radioHeight3.isFailureWarning() || this.fws.radioHeight3.isNoComputedData(); + const gearDownlocked = SimVar.GetSimVarValue('GEAR TOTAL PCT EXTENDED', 'percent') > 0.95; + + const setBelow2000ft = + (this.fws.radioHeight1.value < 2000 && !radioHeight1Invalid) || + (this.fws.radioHeight2.value < 2000 && !radioHeight2Invalid) || + (this.fws.radioHeight3.value < 2000 && !radioHeight3Invalid); + const resetBelow2000ft = + (this.fws.radioHeight1.value > 2200 || radioHeight1Invalid) && + (this.fws.radioHeight2.value > 2200 || radioHeight2Invalid) && + (this.fws.radioHeight3.value > 2200 || radioHeight3Invalid); + const memo2 = this.memoLdgMemo_below2000ft.write(setBelow2000ft, resetBelow2000ft); + + const setInhibitMemo = this.memoLdgMemo_conf01.write( + resetBelow2000ft && !radioHeight1Invalid && !radioHeight2Invalid && !radioHeight3Invalid, + _deltaTime, + ); + const resetInhibitMemo = !( + this.fws.flightPhase.get() === FwcFlightPhase.AtOrBelow800Feet || + this.fws.flightPhase.get() === FwcFlightPhase.TouchDown || + this.fws.flightPhase.get() === FwcFlightPhase.AtOrAbove1500FeetTo800Feet + ); + const memo1 = this.memoLdgMemo_inhibit.write(setInhibitMemo, resetInhibitMemo); + + const showInApproach = memo1 && memo2 && this.fws.flightPhase.get() === FwcFlightPhase.AtOrAbove1500FeetTo800Feet; + + const invalidRadioMemo = this.memoLdgMemo_conf02.write( + radioHeight1Invalid && + radioHeight2Invalid && + radioHeight3Invalid && + gearDownlocked && + this.fws.flightPhase.get() === FwcFlightPhase.AtOrAbove1500FeetTo800Feet, + _deltaTime, + ); + + this.ldgMemo = + showInApproach || + invalidRadioMemo || + this.fws.flightPhase.get() === FwcFlightPhase.TouchDown || + this.fws.flightPhase.get() === FwcFlightPhase.AtOrBelow800Feet; + SimVar.SetSimVarValue('L:A32NX_FWC_LDGMEMO', 'Bool', this.ldgMemo); + } + + _updateAltitudeWarning() { + const warningPressed = + !!SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_L', 'Bool') || + !!SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_R', 'Bool'); + if (warningPressed === true) { + this._wasBelowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._cChordShortWasTriggered = false; + this.fws.soundManager.dequeueSound('cChordOnce'); + this.fws.soundManager.dequeueSound('cChordCont'); + return; + } + + if (Simplane.getIsGrounded()) { + this.fws.soundManager.dequeueSound('cChordCont'); + } + + // 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._wasBelowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._wasReach200ft = false; + this._cChordShortWasTriggered = false; + this.fws.soundManager.dequeueSound('cChordOnce'); + this.fws.soundManager.dequeueSound('cChordCont'); + 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._wasBelowThreshold = false; + this._wasAboveThreshold = false; + this._wasInRange = false; + this._wasReach200ft = false; + this._cChordShortWasTriggered = false; + this.fws.soundManager.dequeueSound('cChordOnce'); + this.fws.soundManager.dequeueSound('cChordCont'); + return; + } + + // FIXME better altitude selection + this.adrAltitude.setFromSimVar(`L:A32NX_ADIRS_ADR_1_BARO_CORRECTED_ALTITUDE_${this.fws.fwsNumber}`); + if (!this.adrAltitude.isNormalOperation()) { + return; + } + const delta = Math.abs(this.adrAltitude.value - targetAltitude); + + if (delta < 200) { + this._wasBelowThreshold = true; + this._wasAboveThreshold = false; + this._wasReach200ft = true; + } + if (delta > 750) { + this._wasAboveThreshold = true; + this._wasBelowThreshold = false; + this._cChordShortWasTriggered = false; + } + if (delta >= 200 && delta <= 750) { + this._wasInRange = true; + } + + const apEngaged = + SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_1_ACTIVE', 'Bool') || + SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_2_ACTIVE', 'Bool'); + if (this._wasBelowThreshold && this._wasReach200ft) { + if (delta >= 200) { + this.fws.soundManager.enqueueSound('cChordCont'); + } else { + this.fws.soundManager.dequeueSound('cChordCont'); + } + } else if ( + this._wasAboveThreshold && + delta <= 750 && + !this._wasReach200ft && + !this._cChordShortWasTriggered && + !apEngaged + ) { + this._cChordShortWasTriggered = true; + this.fws.soundManager.dequeueSound('cChordCont'); + this.fws.soundManager.enqueueSound('cChordOnce'); + } else if (delta > 750 && this._wasInRange && !this._wasReach200ft) { + if (delta > 750) { + this.fws.soundManager.enqueueSound('cChordCont'); + } else { + this.fws.soundManager.dequeueSound('cChordCont'); + } + } + } + + hasAltitudeConstraint() { + if ( + Simplane.getAutoPilotAltitudeManaged() && + SimVar.GetSimVarValue('L:AP_CURRENT_TARGET_ALTITUDE_IS_CONSTRAINT', 'number') !== 0 + ) { + return false; + } + return true; + } +} diff --git a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsMemos.ts b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsMemos.ts index c4e104543f1..815d4d2bd8e 100644 --- a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsMemos.ts +++ b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsMemos.ts @@ -55,7 +55,7 @@ export class FwsMemos { simVarIsActive: MappedSubject.create( ([speedBrakeCommand, fwcFlightPhase]) => speedBrakeCommand && ![1, 8, 9, 10].includes(fwcFlightPhase), this.fws.speedBrakeCommand, - this.fws.fwcFlightPhase, + this.fws.flightPhase, ), whichCodeToReturn: () => [this.fws.amberSpeedBrake.get() ? 1 : 0], codesToReturn: ['000006001', '000006002'], @@ -353,7 +353,7 @@ export class FwsMemos { // RAT OUT flightPhaseInhib: [], simVarIsActive: this.fws.ratDeployed.map((v) => v > 0), - whichCodeToReturn: () => [[1, 2].includes(this.fws.fwcFlightPhase.get()) ? 0 : 1], + whichCodeToReturn: () => [[1, 2].includes(this.fws.flightPhase.get()) ? 0 : 1], codesToReturn: ['242000001', '242000002'], memoInhibit: () => false, failure: 0, diff --git a/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsSoundManager.ts b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsSoundManager.ts new file mode 100644 index 00000000000..36696baec36 --- /dev/null +++ b/fbw-a380x/src/systems/systems-host/systems/FlightWarningSystem/FwsSoundManager.ts @@ -0,0 +1,453 @@ +// Copyright (c) 2021-2024 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, SimVarValueType, Subscribable } from '@microsoft/msfs-sdk'; + +export interface FwsSoundManagerControlEvents { + enqueueSound: string; + dequeueSound: string; +} + +// Synthetic voice has priority over everything, SC is least important +enum FwsAuralWarningType { + SingleChime, + AuralWarning, + SyntheticVoice, +} + +export enum FwsAuralVolume { + Full, // 0 dB + Attenuated, // -6dB + Silent, // -200 dB +} + +interface FwsAural { + /** The LocalVar which triggers the playback. Not prefixed by L: here. Either localVarName or wwiseEventName has to be defined. */ + localVarName?: string; + /** The Wwise event which triggers the playback. Either localVarName or wwiseEventName has to be defined. */ + wwiseEventName?: string; + /** Sounds are queued based on type and priority (highest priority = gets queued first within same type) */ + priority: number; + type: FwsAuralWarningType; + /** Length of audio in seconds, if non-repetitive */ + length?: number; + /** If this is set, this sound is repeated periodically with the specified pause in seconds */ + periodicWithPause?: number; + continuous?: boolean; +} + +export const FwsAuralsList: Record = { + continuousRepetitiveChime: { + localVarName: 'A32NX_FWC_CRC', + priority: 5, + type: FwsAuralWarningType.AuralWarning, + continuous: true, + }, + singleChime: { + localVarName: 'A32NX_FWC_SC', + length: 0.54, + priority: 0, + type: FwsAuralWarningType.SingleChime, + continuous: false, + }, + cavalryChargeOnce: { + localVarName: 'A32NX_FWC_CAVALRY_CHARGE', + length: 0.9, + priority: 4, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + cavalryChargeCont: { + localVarName: 'A32NX_FWC_CAVALRY_CHARGE', + priority: 4, + type: FwsAuralWarningType.AuralWarning, + continuous: true, + }, + tripleClick: { + localVarName: 'A32NX_FMA_TRIPLE_CLICK', + length: 0.62, + priority: 3, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + v1: { + localVarName: 'A32NX_AUDIO_V1_CALLOUT', + length: 1.3, + priority: 1, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + autoBrakeOff: { + localVarName: 'A32NX_AUDIO_AUTOBRAKE_OFF', + length: 1.5, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + runwayTooShort: { + localVarName: 'A32NX_AUDIO_ROW_RWY_TOO_SHORT', + length: 1.6, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: true, + }, + keepMaxReverse: { + localVarName: 'A32NX_AUDIO_ROP_KEEP_MAX_REVERSE', + length: 1.4, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + setMaxReverse: { + localVarName: 'A32NX_AUDIO_ROW_SET_MAX_REVERSE', + length: 1.62, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: false, + }, + brakeMaxBraking: { + localVarName: 'A32NX_AUDIO_ROP_MAX_BRAKING', + length: 3.1, + priority: 4, + type: FwsAuralWarningType.SyntheticVoice, + continuous: true, + }, + stall: { + localVarName: 'A32NX_AUDIO_ROP_MAX_BRAKING', + length: 3.0, + priority: 5, + type: FwsAuralWarningType.SyntheticVoice, + continuous: true, + }, + cChordOnce: { + localVarName: 'A32NX_ALT_DEVIATION', + length: 1.0, + priority: 3, + type: FwsAuralWarningType.AuralWarning, + continuous: false, + }, + cChordCont: { + localVarName: 'A32NX_ALT_DEVIATION', + priority: 3, + type: FwsAuralWarningType.AuralWarning, + continuous: true, + }, + // Altitude callouts + minimums: { + wwiseEventName: 'aural_minimumnew', + length: 0.67, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + hundred_above: { + wwiseEventName: 'aural_100above', + length: 0.72, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + retard: { + wwiseEventName: 'new_retard', + length: 0.9, + periodicWithPause: 0.2, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_2500: { + wwiseEventName: 'new_2500', + length: 1.1, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_2500b: { + wwiseEventName: 'new_2_500', + length: 1.047, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_2000: { + wwiseEventName: 'new_2000', + length: 0.72, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_1000: { + wwiseEventName: 'new_1000', + length: 0.9, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_500: { + wwiseEventName: 'new_500', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_400: { + wwiseEventName: 'new_400', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_300: { + wwiseEventName: 'new_300', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_200: { + wwiseEventName: 'new_200', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_100: { + wwiseEventName: 'new_100', + length: 0.6, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_90: { + wwiseEventName: '90_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_80: { + wwiseEventName: '80_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_70: { + wwiseEventName: '70_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_60: { + wwiseEventName: '60_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_50: { + wwiseEventName: '50_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_40: { + wwiseEventName: '40_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_30: { + wwiseEventName: '30_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_20: { + wwiseEventName: '20_380', + length: 0.4, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_10: { + wwiseEventName: '10_380', + length: 0.3, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, + alt_5: { + wwiseEventName: '5_380', + length: 0.3, + priority: 2, + type: FwsAuralWarningType.SyntheticVoice, + }, +}; + +// FIXME Not all sounds are added to this yet (e.g. CIDS chimes), consider adding them in the future +// Also, single chimes are not filtered (in RL only once every two seconds) +export class FwsSoundManager { + private readonly soundQueue = new Set(); + + private singleChimesPending = 0; + + private currentSoundPlaying: keyof typeof FwsAuralsList | null = null; + + /** in seconds */ + private currentSoundPlayTimeRemaining = 0; + + constructor( + private bus: EventBus, + private startupCompleted: Subscribable, + ) { + // Stop all sounds + Object.values(FwsAuralsList).forEach((a) => { + if (a.localVarName) { + SimVar.SetSimVarValue(`L:${a.localVarName}`, SimVarValueType.Bool, false); + } + }); + + const sub = this.bus.getSubscriber(); + sub.on('enqueueSound').handle((s) => this.enqueueSound(s)); + sub.on('dequeueSound').handle((s) => this.dequeueSound(s)); + } + + /** Add sound to queue. Don't add if already playing */ + enqueueSound(soundKey: keyof typeof FwsAuralsList) { + const sound = FwsAuralsList[soundKey]; + if (!sound || this.currentSoundPlaying === soundKey) { + return; + } + + if (sound.type === FwsAuralWarningType.SyntheticVoice || sound.type === FwsAuralWarningType.AuralWarning) { + this.soundQueue.add(soundKey); + } else if (sound.type === FwsAuralWarningType.SingleChime) { + this.singleChimesPending++; + } + } + + /** Remove sound from queue, e.g. when condition doesn't apply anymore. If sound is currently playing, stops sound immediately */ + dequeueSound(soundKey: keyof typeof FwsAuralsList) { + // Check if this sound is currently playing + if (this.currentSoundPlaying === soundKey && FwsAuralsList[this.currentSoundPlaying]?.continuous) { + this.stopCurrentSound(); + } + this.soundQueue.delete(soundKey); + } + + private stopCurrentSound() { + // Only LVar sounds which are continuous can be stopped + if ( + this.currentSoundPlaying && + FwsAuralsList[this.currentSoundPlaying].localVarName && + FwsAuralsList[this.currentSoundPlaying]?.continuous + ) { + SimVar.SetSimVarValue(`L:${FwsAuralsList[this.currentSoundPlaying].localVarName}`, SimVarValueType.Bool, false); + this.currentSoundPlaying = null; + this.currentSoundPlayTimeRemaining = 0; + } + } + + /** + * Convenience function for FWS: If condition true and sound not already playing, add to queue. If not, dequeue sound + * */ + handleSoundCondition(soundKey: keyof typeof FwsAuralsList, condition: boolean) { + if (condition && this.currentSoundPlaying !== soundKey) { + this.enqueueSound(soundKey); + } else if (!condition) { + this.dequeueSound(soundKey); + } + } + + /** This only has an effect on sounds defining WwiseRTPC behavior/var for volume */ + setVolume(volume: FwsAuralVolume) { + SimVar.SetSimVarValue('L:A32NX_FWS_AUDIO_VOLUME', SimVarValueType.Enum, volume); + } + + /** Play now, not to be called from the outside */ + private playSound(soundKey: keyof typeof FwsAuralsList) { + const sound = FwsAuralsList[soundKey]; + if (!sound) { + return; + } + + if (sound.localVarName) { + SimVar.SetSimVarValue(`L:${sound.localVarName}`, SimVarValueType.Bool, true); + } else if (sound.wwiseEventName) { + Coherent.call('PLAY_INSTRUMENT_SOUND', sound.wwiseEventName); + } + this.currentSoundPlaying = soundKey; + this.currentSoundPlayTimeRemaining = sound.continuous ? Infinity : sound.length; + this.soundQueue.delete(soundKey); + } + + /** Find most important sound from soundQueue and play */ + private selectAndPlayMostImportantSound(): keyof typeof FwsAuralsList | null { + if (!this.startupCompleted.get()) { + return; + } + + // Logic for scheduling new sounds: Take sound from soundQueue of most important type + // (SyntheticVoice > AuralWarning > SingleChime) with highest priority, and play it + let selectedSoundKey: keyof typeof FwsAuralsList | null = null; + this.soundQueue.forEach((sk) => { + const s = FwsAuralsList[sk]; + if ( + selectedSoundKey === null || + s.type > FwsAuralsList[selectedSoundKey].type || + (s.type === FwsAuralsList[selectedSoundKey].type && s.priority > FwsAuralsList[selectedSoundKey].priority) + ) { + selectedSoundKey = sk; + } + }); + + if (selectedSoundKey) { + this.playSound(selectedSoundKey); + return selectedSoundKey; + } + + // See if single chimes are left + if (this.singleChimesPending) { + this.playSound('singleChime'); + this.singleChimesPending--; + return 'singleChime'; + } + + // Ok, nothing to play + return null; + } + + onUpdate(deltaTime: number) { + // Either wait for the current sound to finish, or schedule the next sound + if (this.currentSoundPlaying && this.currentSoundPlayTimeRemaining > 0) { + if (this.currentSoundPlayTimeRemaining - deltaTime / 1_000 > 0) { + // Wait for sound to be finished + this.currentSoundPlayTimeRemaining -= deltaTime / 1_000; + } else { + // Sound finishes in this cycle + if (FwsAuralsList[this.currentSoundPlaying].localVarName) { + SimVar.SetSimVarValue( + `L:${FwsAuralsList[this.currentSoundPlaying].localVarName}`, + SimVarValueType.Bool, + false, + ); + } + this.currentSoundPlaying = null; + this.currentSoundPlayTimeRemaining = 0; + } + + // Interrupt if sound with higher category is present in queue and current sound is continuous + let shouldInterrupt = false; + let rescheduleSound: keyof typeof FwsAuralsList | null = null; + this.soundQueue.forEach((sk) => { + const s = FwsAuralsList[sk]; + if ( + s && + this.currentSoundPlaying && + FwsAuralsList[this.currentSoundPlaying]?.continuous && + s.type > FwsAuralsList[this.currentSoundPlaying].type + ) { + shouldInterrupt = true; + } + }); + + if (shouldInterrupt) { + if (this.currentSoundPlaying && FwsAuralsList[this.currentSoundPlaying]?.continuous) { + rescheduleSound = this.currentSoundPlaying; + this.stopCurrentSound(); + if (rescheduleSound) { + this.enqueueSound(rescheduleSound); + } + } + } + } else { + // Play next sound + this.selectAndPlayMostImportantSound(); + } + } +} diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts b/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts deleted file mode 100644 index 9ee2596a8f5..00000000000 --- a/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts +++ /dev/null @@ -1,727 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -/* eslint-disable camelcase */ -// TODO remove this once Rust implementation is up and running -import { Arinc429Word, UpdateThrottler } from '@flybywiresim/fbw-sdk'; - -enum FwcFlightPhase { - ElecPwr = 1, - FirstEngineStarted = 2, - SecondEngineTakeOffPower = 3, - AtOrAboveEightyKnots = 4, - AtOrAboveV1 = 5, - LiftOff = 6, - AtOrAbove400Feet = 7, - AtOrAbove1500FeetTo800Feet = 8, - AtOrBelow800Feet = 9, - TouchDown = 10, - AtOrBelowEightyKnots = 11, - EnginesShutdown = 12, -} - -/** - * 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 { - private updateThrottler = new UpdateThrottler(125); // has to be > 100 due to pulse nodes - - toConfigTest: boolean; - - flightPhase: FwcFlightPhase; - - ldgMemo: boolean; - - toMemo: boolean; - - gndMemo: NXLogic_ConfirmNode; - - oneEngineRunningConf: NXLogic_ConfirmNode; - - speedAbove80KtsMemo: NXLogic_MemoryNode; - - speedAboveV1Memo: NXLogic_MemoryNode; - - mctMemo: NXLogic_ConfirmNode; - - firePBOutConf: NXLogic_ConfirmNode; - - firePBOutMemo: NXLogic_TriggeredMonostableNode; - - firePBClear12: NXLogic_MemoryNode; - - phase112Memo: NXLogic_TriggeredMonostableNode; - - phase10GroundMemo: NXLogic_TriggeredMonostableNode; - - ac80KtsMemo: NXLogic_TriggeredMonostableNode; - - prevPhase11InvertMemo: NXLogic_TriggeredMonostableNode; - - twoEnginesTOPowerInvertMemo: NXLogic_TriggeredMonostableNode; - - phase9Nvm: NXLogic_MemoryNode; - - prevPhase11: boolean; - - groundImmediateMemo: NXLogic_TriggeredMonostableNode; - - phase6Memo: NXLogic_TriggeredMonostableNode; - - phase7Memo: NXLogic_TriggeredMonostableNode; - - phase89Memo: 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; - - 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.oneEngineRunningConf = new NXLogic_ConfirmNode(30); - - // ESDL 1. 0. 73 - this.speedAbove80KtsMemo = new NXLogic_MemoryNode(true); - - this.speedAboveV1Memo = new NXLogic_MemoryNode(); - - // 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.firePBClear12 = new NXLogic_MemoryNode(false); - this.phase112Memo = new NXLogic_TriggeredMonostableNode(300); // MTRIG 03 - this.phase10GroundMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 06 - this.ac80KtsMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 04 - this.prevPhase11InvertMemo = new NXLogic_TriggeredMonostableNode(3, false); // MTRIG 02 - this.twoEnginesTOPowerInvertMemo = new NXLogic_TriggeredMonostableNode(1, false); // MTRIG 01 - this.phase9Nvm = new NXLogic_MemoryNode(true, true); - this.prevPhase11 = false; - - // ESDL 1. 0.110 - this.groundImmediateMemo = new NXLogic_TriggeredMonostableNode(2); // MTRIG 03 - this.phase6Memo = new NXLogic_TriggeredMonostableNode(15); - this.phase7Memo = new NXLogic_TriggeredMonostableNode(120); - this.phase89Memo = 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 - - // altitude warning - this.previousTargetAltitude = NaN; - this._wasBellowThreshold = false; - this._wasAboveThreshold = false; - this._wasInRange = false; - this._wasReach200ft = false; - } - - update(_deltaTime: number) { - const throttledT = this.updateThrottler.canUpdate(_deltaTime); - - if (throttledT > 0) { - this._updateFlightPhase(throttledT); - this._updateTakeoffMemo(throttledT); - this._updateLandingMemo(throttledT); - this._updateAltitudeWarning(); - } - } - - _updateFlightPhase(_deltaTime: number) { - const radioHeight1 = Arinc429Word.fromSimVarValue('L:A32NX_RA_1_RADIO_ALTITUDE'); - const radioHeight2 = Arinc429Word.fromSimVarValue('L:A32NX_RA_2_RADIO_ALTITUDE'); - const radioHeight3 = Arinc429Word.fromSimVarValue('L:A32NX_RA_3_RADIO_ALTITUDE'); - - const raHeight1InValid = radioHeight1.isFailureWarning() || radioHeight1.isNoComputedData(); - const raHeight2InValid = radioHeight2.isFailureWarning() || radioHeight2.isNoComputedData(); - let radioHeight; - if (raHeight1InValid) { - if (raHeight2InValid) { - radioHeight = radioHeight3; - } else { - radioHeight = radioHeight2; - } - } else { - radioHeight = radioHeight1; - } - const eng1N1 = SimVar.GetSimVarValue('ENG N1 RPM:1', 'Percent'); - const eng2N1 = SimVar.GetSimVarValue('ENG N1 RPM:2', 'Percent'); - const eng3N1 = SimVar.GetSimVarValue('ENG N1 RPM:3', 'Percent'); - const eng4N1 = SimVar.GetSimVarValue('ENG N1 RPM:4', '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 || eng3N1 > 15 || eng4N1 > 15; - const oneEngineRunning = this.oneEngineRunningConf.write(oneEngRunning, _deltaTime); - const noEngineRunning = !oneEngineRunning; - const hFail = radioHeight1.isFailureWarning() && radioHeight2.isFailureWarning() && radioHeight3.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); - - const v1 = SimVar.GetSimVarValue('L:AIRLINER_V1_SPEED', 'knots'); - let acAboveV1; - if (v1) { - acAboveV1 = this.speedAboveV1Memo.write(ias > v1 + 3, ias < v1 - 3); - } else { - acAboveV1 = false; - } - - // ESLD 1.0.90 - const hAbv1500 = radioHeight.isNoComputedData() || radioHeight.value > 1500; - const hAbv800 = radioHeight.isNoComputedData() || radioHeight.value > 800; - const hAbv400 = radioHeight.isNoComputedData() || radioHeight.value > 400; - - // ESLD 1.0.79 + 1.0.80 - const eng1TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:1', 'number'); - const eng1TLAFTO = SimVar.GetSimVarValue('L:A32NX_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 eng1MCL = eng1TLA > 22.9; - const eng1SupMCT = !(eng1TLA < 36.7); - - 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 eng2MCL = eng2TLA > 22.9; - const eng2SupMCT = !(eng2TLA < 36.7); - - const eng3TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:3', 'number'); - const eng3TLAFTO = eng1TLAFTO; // until we have proper FADECs - const eng3MCT = eng3TLA > 33.3 && eng3TLA < 36.7; - const eng3TLAFullPwr = eng3TLA > 43.3; - const eng3MCL = eng3TLA > 22.9; - const eng3SupMCT = !(eng3TLA < 36.7); - - const eng4TLA = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:4', 'number'); - const eng4TLAFTO = eng1TLAFTO; // until we have proper FADECs - const eng4MCT = eng3TLA > 33.3 && eng3TLA < 36.7; - const eng4TLAFullPwr = eng3TLA > 43.3; - const eng4MCL = eng3TLA > 22.9; - const eng4SupMCT = !(eng4TLA < 36.7); - - const twoEnginesMcl = [eng1MCL, eng2MCL, eng3MCL, eng4MCL].filter(Boolean).length >= 2; - const eng1TOPowerSignal = (eng1TLAFTO && eng1MCT) || eng1TLAFullPwr || eng1SupMCT; - const eng2TOPowerSignal = (eng2TLAFTO && eng2MCT) || eng2TLAFullPwr || eng2SupMCT; - const eng3TOPowerSignal = (eng3TLAFTO && eng3MCT) || eng3TLAFullPwr || eng3SupMCT; - const eng4TOPowerSignal = (eng4TLAFTO && eng4MCT) || eng4TLAFullPwr || eng4SupMCT; - - const twoEnginesTOPowerSignal = - [eng1TOPowerSignal, eng2TOPowerSignal, eng3TOPowerSignal, eng4TOPowerSignal].filter(Boolean).length >= 2; - - const twoEnginesTOPower = - twoEnginesTOPowerSignal || - (this.mctMemo.write(twoEnginesTOPowerSignal, _deltaTime) && !hAbv1500 && twoEnginesMcl); - - // 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 resetFirePbClear12 = eng1FirePbMemo && ground; - - const phase10 = - (this.phase10GroundMemo.write(groundImmediate, _deltaTime) || groundImmediate) && - !twoEnginesTOPower && - acSpeedAbove80kts; - - const phase345Cond = ground && twoEnginesTOPower; - const phase3 = !acSpeedAbove80kts && oneEngRunning && phase345Cond; - const phase4 = acSpeedAbove80kts && phase345Cond && !acAboveV1; - const phase5 = acSpeedAbove80kts && phase345Cond && acAboveV1; - - const setPhase11Nvm = phase3 || phase10; - const resetPhase11Nvm = - (!this.ac80KtsMemo.write(!acSpeedAbove80kts, _deltaTime) && - ((ground && this.prevPhase11InvertMemo.write(this.prevPhase11, _deltaTime)) || - resetFirePbClear12 || - (ground && this.twoEnginesTOPowerInvertMemo.write(twoEnginesTOPower, _deltaTime))) && - !this.prevPhase11) || - adcTestInhib; - const phase11Nvm = this.phase9Nvm.write(setPhase11Nvm, resetPhase11Nvm); // S* / R (NVM) - const phase211Cond = ground && !twoEnginesTOPower && !acSpeedAbove80kts; - const phase11 = oneEngRunning && phase11Nvm && phase211Cond; - const phase2 = phase211Cond && !phase11Nvm && oneEngRunning; - - const phase112MemoA = this.firePBClear12.write(phase11, resetFirePbClear12); // S / R* - const phase112Cond = !phase11 && noEngineRunning && groundImmediate; - const phase112Memo = this.phase112Memo.write(phase112MemoA && phase112Cond, _deltaTime); // MTRIG 03 - const phase1 = phase112Cond && !phase112Memo; - const phase12 = phase112Cond && phase112Memo; - - this.prevPhase11 = phase11; - - // ESLD 1.0.110 - const ground2sMemorized = this.groundImmediateMemo.write(groundImmediate, _deltaTime) || groundImmediate; - - const phase6Cond = !hAbv400 && twoEnginesTOPower && !hFail && !ground2sMemorized; - const phase6 = this.phase6Memo.write(phase6Cond, _deltaTime) && phase6Cond; - - const phase7Cond = !phase6 && !hAbv1500 && twoEnginesTOPower && !hFail && !ground2sMemorized; - const phase7 = this.phase7Memo.write(phase7Cond, _deltaTime) && phase7Cond; - - const phase89Cond = !ground2sMemorized && !hFail && !twoEnginesTOPower && !hAbv1500 && !hAbv800; - const phase89Memo = this.phase89Memo.write(phase89Cond, _deltaTime) && phase89Cond; - - const phase8 = !phase7 && !ground2sMemorized && !phase89Memo; - const phase9 = phase89Memo && !phase10; - - /** * End of ESLD logic ** */ - - // consolidate into single variable (just to be safe) - const phases = [phase1, phase2, phase3, phase4, phase5, phase6, phase7, phase8, phase9, phase10, phase11, phase12]; - - 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) { - if (activePhases.indexOf(this.flightPhase) !== -1) { - // if the currently active one is present, keep it - return; - } - // pick the earliest one - this._setFlightPhase(activePhases[0]); - return; - } - - // otherwise, no flight phase is valid => warn - 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 === FwcFlightPhase.FirstEngineStarted && this.toConfigTest; - const resetFlightPhaseMemo = - this.flightPhase === FwcFlightPhase.EnginesShutdown || - this.flightPhase === FwcFlightPhase.SecondEngineTakeOffPower || - this.flightPhase === FwcFlightPhase.ElecPwr || - this.flightPhase === FwcFlightPhase.AtOrAbove1500FeetTo800Feet; - 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 eng3NotRunning = SimVar.GetSimVarValue('ENG N1 RPM:3', 'Percent') < 15; - const eng4NotRunning = SimVar.GetSimVarValue('ENG N1 RPM:4', 'Percent') < 15; - const toTimerElapsed = this.memoTo_conf01.write( - !eng1NotRunning && !eng2NotRunning && !eng3NotRunning && !eng4NotRunning, - _deltaTime, - ); - - this.toMemo = flightPhaseMemo || (this.flightPhase === FwcFlightPhase.FirstEngineStarted && 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 radioHeight3 = Arinc429Word.fromSimVarValue('L:A32NX_RA_3_RADIO_ALTITUDE'); - const radioHeight1Invalid = radioHeight1.isFailureWarning() || radioHeight1.isNoComputedData(); - const radioHeight2Invalid = radioHeight2.isFailureWarning() || radioHeight2.isNoComputedData(); - const radioHeight3Invalid = radioHeight3.isFailureWarning() || radioHeight3.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) || - (radioHeight3.value < 2000 && !radioHeight3Invalid); - const resetBelow2000ft = - (radioHeight1.value > 2200 || radioHeight1Invalid) && - (radioHeight2.value > 2200 || radioHeight2Invalid) && - (radioHeight3.value > 2200 || radioHeight3Invalid); - const memo2 = this.memoLdgMemo_below2000ft.write(setBelow2000ft, resetBelow2000ft); - - const setInhibitMemo = this.memoLdgMemo_conf01.write( - resetBelow2000ft && !radioHeight1Invalid && !radioHeight2Invalid && !radioHeight3Invalid, - _deltaTime, - ); - const resetInhibitMemo = !( - this.flightPhase === FwcFlightPhase.AtOrBelow800Feet || - this.flightPhase === FwcFlightPhase.TouchDown || - this.flightPhase === FwcFlightPhase.AtOrAbove1500FeetTo800Feet - ); - const memo1 = this.memoLdgMemo_inhibit.write(setInhibitMemo, resetInhibitMemo); - - const showInApproach = memo1 && memo2 && this.flightPhase === FwcFlightPhase.AtOrAbove1500FeetTo800Feet; - - const invalidRadioMemo = this.memoLdgMemo_conf02.write( - radioHeight1Invalid && - radioHeight2Invalid && - radioHeight3Invalid && - gearDownlocked && - this.flightPhase === FwcFlightPhase.AtOrAbove1500FeetTo800Feet, - _deltaTime, - ); - - this.ldgMemo = - showInApproach || - invalidRadioMemo || - this.flightPhase === FwcFlightPhase.TouchDown || - this.flightPhase === FwcFlightPhase.AtOrBelow800Feet; - 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); - } - - const warningPressed = - !!SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_L', 'Bool') || - !!SimVar.GetSimVarValue('L:PUSH_AUTOPILOT_MASTERAWARN_R', 'Bool'); - if (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 index 88b61713dbc..becf5e19fc1 100644 --- a/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts +++ b/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts @@ -2,6 +2,8 @@ import { Arinc429SignStatusMatrix, Arinc429Word, NXDataStore, UpdateThrottler } import { FmgcFlightPhase } from '@shared/flightphase'; import { LegacySoundManager, soundList } from 'systems-host/systems/LegacySoundManager'; import { A380X_DEFAULT_RADIO_AUTO_CALL_OUTS, A380XRadioAutoCallOutFlags } from '../../shared/src/AutoCallOuts'; +import { EventBus } from '@microsoft/msfs-sdk'; +import { FwsSoundManagerControlEvents } from 'systems-host/systems/FlightWarningSystem/FwsSoundManager'; type ModesType = { current: number; @@ -12,12 +14,12 @@ type ModesType = { /** * 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 { private updateThrottler = new UpdateThrottler(125); // has to be > 100 due to pulse nodes + private pub = this.bus.getPublisher(); + autoCallOutPins: number; minimumsState = 0; @@ -51,7 +53,10 @@ export class LegacyGpws { egpwsAlertDiscreteWord2 = Arinc429Word.empty(); // eslint-disable-next-line camelcase - constructor(private soundManager: LegacySoundManager) { + constructor( + private bus: EventBus, + private soundManager: LegacySoundManager, + ) { this.autoCallOutPins = A380X_DEFAULT_RADIO_AUTO_CALL_OUTS; this.minimumsState = 0; @@ -331,10 +336,10 @@ export class LegacyGpws { } else if (this.minimumsState === 1 && over100Above) { this.minimumsState = 2; } else if (this.minimumsState === 2 && !over100Above) { - this.soundManager.tryPlaySound(soundList.hundred_above); + this.pub.pub('enqueueSound', 'hundred_above'); this.minimumsState = 1; } else if (this.minimumsState === 1 && !overMinimums) { - this.soundManager.tryPlaySound(soundList.minimums); + this.pub.pub('enqueueSound', 'minimums'); this.minimumsState = 0; } } @@ -578,7 +583,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 6) { if (this.RetardState.value !== 'retardPlaying' && this.autoCallOutPins & A380XRadioAutoCallOutFlags.Five) { - this.soundManager.tryPlaySound(soundList.alt_5); + this.pub.pub('enqueueSound', 'alt_5'); } this.AltCallState.action('down'); } @@ -588,7 +593,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 12) { if (this.RetardState.value !== 'retardPlaying' && this.autoCallOutPins & A380XRadioAutoCallOutFlags.Ten) { - this.soundManager.tryPlaySound(soundList.alt_10); + this.pub.pub('enqueueSound', 'alt_10'); } this.AltCallState.action('down'); } @@ -598,7 +603,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 22) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Twenty) { - this.soundManager.tryPlaySound(soundList.alt_20); + this.pub.pub('enqueueSound', 'alt_20'); } this.AltCallState.action('down'); } @@ -608,7 +613,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 32) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Thirty) { - this.soundManager.tryPlaySound(soundList.alt_30); + this.pub.pub('enqueueSound', 'alt_30'); } this.AltCallState.action('down'); } @@ -618,7 +623,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 42) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Forty) { - this.soundManager.tryPlaySound(soundList.alt_40); + this.pub.pub('enqueueSound', 'alt_40'); } this.AltCallState.action('down'); } @@ -628,7 +633,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 53) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Fifty) { - this.soundManager.tryPlaySound(soundList.alt_50); + this.pub.pub('enqueueSound', 'alt_50'); } this.AltCallState.action('down'); } @@ -638,7 +643,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 63) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Sixty) { - this.soundManager.tryPlaySound(soundList.alt_60); + this.pub.pub('enqueueSound', 'alt_60'); } this.AltCallState.action('down'); } @@ -648,7 +653,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 73) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Seventy) { - this.soundManager.tryPlaySound(soundList.alt_70); + this.pub.pub('enqueueSound', 'alt_70'); } this.AltCallState.action('down'); } @@ -658,7 +663,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 83) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Eighty) { - this.soundManager.tryPlaySound(soundList.alt_80); + this.pub.pub('enqueueSound', 'alt_80'); } this.AltCallState.action('down'); } @@ -668,7 +673,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 93) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.Ninety) { - this.soundManager.tryPlaySound(soundList.alt_90); + this.pub.pub('enqueueSound', 'alt_90'); } this.AltCallState.action('down'); } @@ -678,7 +683,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 110) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.OneHundred) { - this.soundManager.tryPlaySound(soundList.alt_100); + this.pub.pub('enqueueSound', 'alt_100'); } this.AltCallState.action('down'); } @@ -688,7 +693,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 210) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.TwoHundred) { - this.soundManager.tryPlaySound(soundList.alt_200); + this.pub.pub('enqueueSound', 'alt_200'); } this.AltCallState.action('down'); } @@ -698,7 +703,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 310) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.ThreeHundred) { - this.soundManager.tryPlaySound(soundList.alt_300); + this.pub.pub('enqueueSound', 'alt_300'); } this.AltCallState.action('down'); } @@ -708,7 +713,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 410) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.FourHundred) { - this.soundManager.tryPlaySound(soundList.alt_400); + this.pub.pub('enqueueSound', 'alt_400'); } this.AltCallState.action('down'); } @@ -718,7 +723,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 513) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.FiveHundred) { - this.soundManager.tryPlaySound(soundList.alt_500); + this.pub.pub('enqueueSound', 'alt_500'); } this.AltCallState.action('down'); } @@ -728,7 +733,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 1020) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.OneThousand) { - this.soundManager.tryPlaySound(soundList.alt_1000); + this.pub.pub('enqueueSound', 'alt_1000'); } this.AltCallState.action('down'); } @@ -738,7 +743,7 @@ export class LegacyGpws { this.AltCallState.action('up'); } else if (radioAlt <= 2020) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.TwoThousand) { - this.soundManager.tryPlaySound(soundList.alt_2000); + this.pub.pub('enqueueSound', 'alt_2000'); } this.AltCallState.action('down'); } @@ -746,9 +751,9 @@ export class LegacyGpws { case 'over2500': if (radioAlt <= 2530) { if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.TwoThousandFiveHundred) { - this.soundManager.tryPlaySound(soundList.alt_2500); + this.pub.pub('enqueueSound', 'alt_2500'); } else if (this.autoCallOutPins & A380XRadioAutoCallOutFlags.TwentyFiveHundred) { - this.soundManager.tryPlaySound(soundList.alt_2500b); + this.pub.pub('enqueueSound', 'alt_2500b'); } this.AltCallState.action('down'); } @@ -762,26 +767,28 @@ export class LegacyGpws { if (radioAlt < 20) { if (!SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_ACTIVE', 'Bool')) { this.RetardState.action('play'); - this.soundManager.addPeriodicSound(soundList.retard, 1.1); + this.pub.pub('enqueueSound', 'retard'); } else if (radioAlt < 10) { this.RetardState.action('play'); - this.soundManager.addPeriodicSound(soundList.retard, 1.1); + this.pub.pub('enqueueSound', 'retard'); } } break; case 'retardPlaying': if ( SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:1', 'number') < 2.6 || - SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:2', 'number') < 2.6 + SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:2', 'number') < 2.6 || + SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:3', 'number') < 2.6 || + SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_TLA:4', 'number') < 2.6 ) { this.RetardState.action('land'); - this.soundManager.removePeriodicSound(soundList.retard); + this.pub.pub('dequeueSound', '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); + this.pub.pub('dequeueSound', 'retard'); } break; case 'landed': diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts b/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts index 8512cb125ad..25516bb3c9a 100644 --- a/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts +++ b/fbw-a380x/src/systems/systems-host/systems/LegacySoundManager.ts @@ -146,92 +146,4 @@ export const soundList: Record = { 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_90: { - name: '90_380', - length: 0.4, - }, - alt_80: { - name: '80_380', - length: 0.4, - }, - alt_70: { - name: '70_380', - length: 0.4, - }, - alt_60: { - name: '60_380', - length: 0.4, - }, - alt_50: { - name: '50_380', - length: 0.4, - }, - alt_40: { - name: '40_380', - length: 0.4, - }, - alt_30: { - name: '30_380', - length: 0.4, - }, - alt_20: { - name: '20_380', - length: 0.4, - }, - alt_10: { - name: '10_380', - length: 0.3, - }, - alt_5: { - name: '5_380', - length: 0.3, - }, }; diff --git a/fbw-a380x/src/wasm/fbw_a380/src/FlyByWireInterface.cpp b/fbw-a380x/src/wasm/fbw_a380/src/FlyByWireInterface.cpp index 10c137cb3ce..5850fa32186 100644 --- a/fbw-a380x/src/wasm/fbw_a380/src/FlyByWireInterface.cpp +++ b/fbw-a380x/src/wasm/fbw_a380/src/FlyByWireInterface.cpp @@ -332,7 +332,7 @@ void FlyByWireInterface::setupLocalVariables() { idFmaSpeedProtectionActive = std::make_unique("A32NX_FMA_SPEED_PROTECTION_MODE"); idFmaSoftAltModeActive = std::make_unique("A32NX_FMA_SOFT_ALT_MODE"); idFmaCruiseAltModeActive = std::make_unique("A32NX_FMA_CRUISE_ALT_MODE"); - idFmaApproachCapability = std::make_unique("A32NX_ApproachCapability"); + idFmaApproachCapability = std::make_unique("A32NX_APPROACH_CAPABILITY"); idFmaTripleClick = std::make_unique("A32NX_FMA_TRIPLE_CLICK"); idFmaModeReversion = std::make_unique("A32NX_FMA_MODE_REVERSION"); diff --git a/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.cpp b/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.cpp index 0ea0c773bcd..18dd71601b0 100644 --- a/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.cpp +++ b/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.cpp @@ -622,6 +622,7 @@ bool SimConnectInterface::prepareSimInputSimConnectDataDefinitions() { result &= addInputDataDefinition(hSimConnect, 0, Events::AUTOPILOT_DISENGAGE_SET, "AUTOPILOT_DISENGAGE_SET", true); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTOPILOT_DISENGAGE_TOGGLE, "AUTOPILOT_DISENGAGE_TOGGLE", true); result &= addInputDataDefinition(hSimConnect, 0, Events::TOGGLE_FLIGHT_DIRECTOR, "TOGGLE_FLIGHT_DIRECTOR", false); + result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_AUTOPILOT_DISENGAGE, "A32NX.AUTOPILOT_DISENGAGE", false); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_FCU_AP_1_PUSH, "A32NX.FCU_AP_1_PUSH", false); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_FCU_AP_2_PUSH, "A32NX.FCU_AP_2_PUSH", false); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_FCU_AP_DISCONNECT_PUSH, "A32NX.FCU_AP_DISCONNECT_PUSH", false); @@ -693,6 +694,7 @@ bool SimConnectInterface::prepareSimInputSimConnectDataDefinitions() { result &= addInputDataDefinition(hSimConnect, 0, Events::AUTO_THROTTLE_ARM, "AUTO_THROTTLE_ARM", true); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTO_THROTTLE_DISCONNECT, "AUTO_THROTTLE_DISCONNECT", true); + result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_AUTO_THROTTLE_DISCONNECT, "A32NX.AUTO_THROTTLE_DISCONNECT", false); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTO_THROTTLE_TO_GA, "AUTO_THROTTLE_TO_GA", true); result &= addInputDataDefinition(hSimConnect, 0, Events::A32NX_ATHR_RESET_DISABLE, "A32NX.ATHR_RESET_DISABLE", false); @@ -2216,7 +2218,7 @@ void SimConnectInterface::processEventWithOneParam(const DWORD eventId, const DW std::cout << "WASM: event triggered: AUTOPILOT_DISENGAGE_SET" << std::endl; // Re emitting masked event for autopilot disconnection - execute_calculator_code("(>K:A32NX.AUTOPILOT_DISENGAGE)", nullptr, nullptr, nullptr); + sendEvent(SimConnectInterface::Events::A32NX_AUTOPILOT_DISENGAGE, 0, SIMCONNECT_GROUP_PRIORITY_STANDARD); } break; } @@ -2678,7 +2680,7 @@ void SimConnectInterface::processEventWithOneParam(const DWORD eventId, const DW std::cout << "WASM: event triggered: AUTO_THROTTLE_DISCONNECT" << std::endl; // Re emitting masked event for autobrake disconnection - execute_calculator_code("(>K:A32NX.AUTO_THROTTLE_DISCONNECT)", nullptr, nullptr, nullptr); + sendEvent(Events::A32NX_AUTO_THROTTLE_DISCONNECT, 0, SIMCONNECT_GROUP_PRIORITY_STANDARD); break; } diff --git a/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.h b/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.h index a97dd8195f0..b5c6093f39f 100644 --- a/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.h +++ b/fbw-a380x/src/wasm/fbw_a380/src/interface/SimConnectInterface.h @@ -48,6 +48,7 @@ class SimConnectInterface { AUTOPILOT_DISENGAGE_SET, AUTOPILOT_DISENGAGE_TOGGLE, TOGGLE_FLIGHT_DIRECTOR, + A32NX_AUTOPILOT_DISENGAGE, A32NX_FCU_AP_1_PUSH, A32NX_FCU_AP_2_PUSH, A32NX_FCU_AP_DISCONNECT_PUSH, @@ -116,6 +117,7 @@ class SimConnectInterface { AP_MACH_HOLD, AUTO_THROTTLE_ARM, AUTO_THROTTLE_DISCONNECT, + A32NX_AUTO_THROTTLE_DISCONNECT, AUTO_THROTTLE_TO_GA, A32NX_ATHR_RESET_DISABLE, A32NX_THROTTLE_MAPPING_SET_DEFAULTS,