From 4bedac5d2c1a25f2c09c9201d124880aeb9295e8 Mon Sep 17 00:00:00 2001 From: flogross89 <63071941+flogross89@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:26:06 +0300 Subject: [PATCH] feat(pfd): Add vertical deviation indicator, ROW/ROP/OANS warnings (#8605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * linting * port LinearDeviationIndicator from a32nx PFD * add PFD warnings for ROW, ROP, OANS * add speed margins to a380x * add ROP/ROW/OANS PFD warnings to a32nx * update two-line message, fix bug in getDisplayIndex() * make RWY TOO SHORT smaller * Disable second ISIS; update colors for PFD tapes, PFD horizon, EWD THR gauges * restructure code, add A380X-only warnings * refactor Publisher structure, leave only data with existing sources * switch to data words for ROS/ROP and OANS PFD alerts * clean up * feat: add some A429 utils * fix(pfd): tidy up publishers * feat: add fwc publisher * fix(pfd): use fwc data for stall warning * A380X: GPWS PFD alerts, ROP/ROW aural alerts, A32NX: Add GPWS discrete word to GPWS, Both: warnings position update * use correct discrete word bit fields * Add ROP/ROW aural alerts to A32NX * fix lint after rebase * lint-fix * update font for IF WET RWY TOO SHORT (fix for a32nx) * lint + lint-fixes * add LS reminder for ILS, frequency of ILS information has same font size before and after decimal point * consolidate to single definitions.scss, use standard colors for thrust gauge and PFD tapes * move ISIS changes to other PR * move logic from A32NX_FWC.js to PseudoFWC * inhibit single chimes for the first two seconds after power-on --------- Co-authored-by: Michael Corcoran --- .github/CHANGELOG.md | 1 + fbw-a32nx/docs/a320-simvars.md | 58 ++++ .../FlyByWire_A320_NEO/sound/sound.xml | 46 ++- .../html_ui/Pages/A32NX_Core/A32NX_GPWS.js | 51 ++- .../systems/instruments/src/EWD/PseudoFWC.ts | 303 ++++++++++++++---- .../src/systems/instruments/src/PFD/PFD.tsx | 2 + .../instruments/src/PFD/animations.scss | 14 +- .../systems/instruments/src/PFD/config.json | 6 +- .../instruments/src/PFD/instrument.tsx | 58 ++-- .../src/PFD/shared/ArincValueProvider.ts | 10 +- .../src/PFD/shared/SimplaneValueProvider.ts | 12 +- .../systems/instruments/src/PFD/tsconfig.json | 4 +- fbw-a32nx/src/systems/tsconfig.json | 3 +- .../FlyByWire_A380_842/sound/sound.xml | 44 ++- .../instruments/src/Common/definitions.scss | 2 +- .../src/MsfsAvionicsCommon/CdsDisplayUnit.tsx | 4 +- .../src/PFD/AttitudeIndicatorWarningsA380.tsx | 142 ++++++++ .../src/PFD/LandingSystemIndicator.tsx | 56 +++- .../src/PFD/LinearDeviationIndicator.tsx | 133 ++++++++ .../src/systems/instruments/src/PFD/PFD.tsx | 9 +- .../instruments/src/PFD/SpeedIndicator.tsx | 82 +++++ .../instruments/src/PFD/animations.scss | 14 +- .../systems/instruments/src/PFD/config.json | 3 +- .../instruments/src/PFD/instrument.tsx | 25 +- .../src/PFD/shared/definitions.scss | 20 -- .../systems/instruments/src/PFD/style.scss | 9 +- .../systems/instruments/src/PFD/tsconfig.json | 4 +- .../systems/systems-host/systems/LegacyFwc.ts | 27 +- .../systems-host/systems/LegacyGpws.ts | 56 +++- .../src/MsfsAvionicsCommon/index.ts | 1 + .../providers/FwcPublisher.ts | 43 +++ .../providers/RopRowOansPublisher.ts | 18 ++ .../providers/TawsPublisher.ts | 48 +++ .../src/MsfsAvionicsCommon/providers/index.ts | 3 + .../systems/instruments/src/PFD/.eslintrc.js | 12 + .../src/PFD/AttitudeIndicatorWarnings.tsx | 237 ++++++++++++++ .../instruments/src/PFD/animations.scss | 77 +++++ .../src/systems/instruments/src/PFD/index.ts | 1 + .../systems/instruments/src/PFD/style.scss | 283 ++++++++++++++++ .../systems/instruments/src/PFD/tsconfig.json | 21 ++ .../shared/src/Arinc429RegisterSubject.ts | 24 ++ fbw-common/src/systems/shared/src/arinc429.ts | 16 + fbw-common/src/systems/tsconfig.json | 4 - 43 files changed, 1808 insertions(+), 178 deletions(-) create mode 100644 fbw-a380x/src/systems/instruments/src/PFD/AttitudeIndicatorWarningsA380.tsx create mode 100644 fbw-a380x/src/systems/instruments/src/PFD/LinearDeviationIndicator.tsx delete mode 100644 fbw-a380x/src/systems/instruments/src/PFD/shared/definitions.scss create mode 100644 fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/index.ts create mode 100644 fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/FwcPublisher.ts create mode 100644 fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/RopRowOansPublisher.ts create mode 100644 fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/TawsPublisher.ts create mode 100644 fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/index.ts create mode 100644 fbw-common/src/systems/instruments/src/PFD/.eslintrc.js create mode 100644 fbw-common/src/systems/instruments/src/PFD/AttitudeIndicatorWarnings.tsx create mode 100644 fbw-common/src/systems/instruments/src/PFD/animations.scss create mode 100644 fbw-common/src/systems/instruments/src/PFD/index.ts create mode 100644 fbw-common/src/systems/instruments/src/PFD/style.scss create mode 100644 fbw-common/src/systems/instruments/src/PFD/tsconfig.json diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b99fbcce54a..797a46c8ef2 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -92,6 +92,7 @@ 1. [EFCS] Fix ground spoiler retraction after increasing TLA slightly above 0 - @lukecologne (luke) 1. [FWC] Improved LDG LT memo to take into account light position - @BravoMike99 (bruno_pt99) 1. [PRESS] Add pressurization system failures - @mjuhe (Miquel Juhe) +1. [PFD] Implement alerts within artificial horizon (ROP, ROW, OANS, stall, windshear) @flogross89 (Flo) ## 0.11.0 diff --git a/fbw-a32nx/docs/a320-simvars.md b/fbw-a32nx/docs/a320-simvars.md index e14d1daf56d..616a6ed1873 100644 --- a/fbw-a32nx/docs/a320-simvars.md +++ b/fbw-a32nx/docs/a320-simvars.md @@ -22,6 +22,8 @@ - [Landing Gear (ATA 32)](#landing-gear-ata-32) - [ATC (ATA 34)](#atc-ata-34) - [Radio Altimeter (ATA 34)](#radio-altimeter-ata-34) + - [GPWS / TAWS (ATA 34)](#gpws--taws-ata-34) + - [ROW / ROP / OANS (ATA 34)](#row--rop--oans-ata-34) - [Electronic Flight Bag (ATA 46)](#electronic-flight-bag-ata-46) ## Uncategorized @@ -3913,6 +3915,62 @@ In the variables below, {number} should be replaced with one item in the set: { - 1 - 2 +## GPWS / TAWS (ATA 34) +- `A32NX_EGPWS_ALERT_{1 | 2}_DISCRETE_WORD_1` + - Data word for GPWS alerts. Used for displaying alerts on the PFD (on the A380) and triggering aural warnings + - Arinc429 + - | Bit | Description | + |:---:|:----------------------:| + | 11 | SINKRATE | + | 12 | PULL UP | + | 13 | TERRAIN | + | 14 | DON'T SINK | + | 15 | TOO LOW GEAR | + | 16 | TOO LOW FLAPS | + | 17 | TOO LOW TERRAIN | + | 18 | GLIDESLOPE | + | 20 | TERRAIN PULL UP | + | 22 | TERRAIN AHEAD PULL UP | + | 27 | TERRAIN AHEAD | + +- `A32NX_EGPWS_ALERT_{1 | 2}_DISCRETE_WORD_2` + - Data word for GPWS alerts. Used for displaying alerts on the PFD (on the A380) or on the GPWS visual indicators on the A320 + - Arinc429 + - | Bit | Description | + |:---:|:----------------------------:| + | 11 | G/S CANCEL | + | 12 | GPWS ALERT | + | 13 | GPWS WARNING | + | 14 | GPWS INOP | + | 15 | W/S WARNING | + | 16 | AUDIO ON | + | 22 | TERRAIN AWARENESS WARNING | + | 23 | TERRAIN AWARENESS CAUTION | + | 24 | TERRAIN AWARENESS INOP | + | 25 | EXTERNAL FAULT | + | 26 | TERRAIN AWARENESS NOT AVAIL. | + +## ROW / ROP / OANS (ATA 34) + +- A32NX_ROW_ROP_WORD_1 + - Data word for ROW and ROP functions. Used for displaying alerts on the PFD. + - Arinc429 + - | Bit | Description | + |:---:|:---------------------------------:| + | 11 | ROW/ROP operative | + | 12 | ROP: Active with autobrake | + | 13 | ROP: Active with manual braking | + | 14 | ROW Wet: Runway too short | + | 15 | ROW Dry: Runway too short | + +- A32NX_OANS_WORD_1 + - Data word for OANS functions. Used for displaying alerts on the PFD. + - Arinc429 + - | Bit | Description | + |:---:|:---------------------------------:| + | 11 | OANS: Runway ahead | + + ## Electronic Flight Bag (ATA 46) - A32NX_PUSHBACK_SYSTEM_ENABLED 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 1927b61bc40..eddb808bf5b 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 @@ -2306,6 +2306,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2313,8 +2350,6 @@ - - @@ -2388,13 +2423,6 @@ - - - - - - - diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_GPWS.js b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_GPWS.js index 482de23a968..74bf17a45d9 100644 --- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_GPWS.js +++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/A32NX_Core/A32NX_GPWS.js @@ -98,7 +98,7 @@ class A32NX_GPWS { {}, ], onChange: (current, _) => { - SimVar.SetSimVarValue("L:A32NX_GPWS_GS_Warning_Active", "Bool", current >= 1); + this.setGlideSlopeWarning(current >= 1); } } ]; @@ -109,6 +109,44 @@ class A32NX_GPWS { this.AltCallState.setState("ground"); this.RetardState = A32NX_Util.createMachine(RetardStateMachine); this.RetardState.setState("landed"); + + this.egpwsAlertDiscreteWord1 = Arinc429Word.empty(); + this.egpwsAlertDiscreteWord2 = Arinc429Word.empty(); + } + + gpwsUpdateDiscreteWords() { + this.egpwsAlertDiscreteWord1.ssm = Arinc429SignStatusMatrix.NormalOperation; + this.egpwsAlertDiscreteWord1.setBitValue(11, this.modes[0].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(12, this.modes[0].current === 2); + this.egpwsAlertDiscreteWord1.setBitValue(13, this.modes[1].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(12, this.modes[1].current === 2); + this.egpwsAlertDiscreteWord1.setBitValue(14, this.modes[2].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(15, this.modes[3].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(16, this.modes[3].current === 2); + this.egpwsAlertDiscreteWord1.setBitValue(17, this.modes[3].current === 3); + this.egpwsAlertDiscreteWord1.setBitValue(18, this.modes[4].current === 1); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_1', this.egpwsAlertDiscreteWord1.value, this.egpwsAlertDiscreteWord1.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_1', this.egpwsAlertDiscreteWord1.value, this.egpwsAlertDiscreteWord1.ssm); + + this.egpwsAlertDiscreteWord2.ssm = Arinc429SignStatusMatrix.NormalOperation; + this.egpwsAlertDiscreteWord2.setBitValue(14, false); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + } + + setGlideSlopeWarning(state) { + SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', state ? 1 : 0); // Still need this for XML + this.egpwsAlertDiscreteWord2.setBitValue(11, state); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + } + + setGpwsWarning(state) { + SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', state ? 1 : 0); // Still need this for XML + this.egpwsAlertDiscreteWord2.setBitValue(12, state); + this.egpwsAlertDiscreteWord2.setBitValue(13, state); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); } init() { @@ -116,8 +154,8 @@ class A32NX_GPWS { this.radnav.init(NavMode.FOUR_SLOTS); - SimVar.SetSimVarValue("L:A32NX_GPWS_GS_Warning_Active", "Bool", 0); - SimVar.SetSimVarValue("L:A32NX_GPWS_Warning_Active", "Bool", 0); + this.setGlideSlopeWarning(false); + this.setGpwsWarning(false); NXDataStore.getAndSubscribe('CONFIG_A32NX_FWC_RADIO_AUTO_CALL_OUT_PINS', (k, v) => k === 'CONFIG_A32NX_FWC_RADIO_AUTO_CALL_OUT_PINS' && (this.autoCallOutPins = v), DEFAULT_RADIO_AUTO_CALL_OUTS); } @@ -171,11 +209,12 @@ class A32NX_GPWS { this.Mode4MaxRAAlt = NaN; } - SimVar.SetSimVarValue("L:A32NX_GPWS_GS_Warning_Active", "Bool", 0); - SimVar.SetSimVarValue("L:A32NX_GPWS_Warning_Active", "Bool", 0); + this.setGlideSlopeWarning(false); + this.setGpwsWarning(false); } this.GPWSComputeLightsAndCallouts(); + this.egpwsAlertDiscreteWord1(); if ((mda !== 0 || (dh !== -1 && dh !== -2) && phase === FmgcFlightPhases.APPROACH)) { let minimumsDA; //MDA or DH @@ -276,7 +315,7 @@ class A32NX_GPWS { } const illuminateGpwsLight = activeTypes.some((type) => type.gpwsLight); - SimVar.SetSimVarValue("L:A32NX_GPWS_Warning_Active", "Bool", illuminateGpwsLight); + this.setGpwsWarning(illuminateGpwsLight); } /** diff --git a/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts b/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts index add96361b8a..9461efbf1a7 100644 --- a/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts +++ b/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts @@ -12,10 +12,13 @@ import { ConsumerSubject, SimVarValueType, SubscribableMapFunctions, + StallWarningEvents, } from '@microsoft/msfs-sdk'; import { Arinc429Register, + Arinc429RegisterSubject, + Arinc429SignStatusMatrix, Arinc429Word, NXDataStore, NXLogicClockNode, @@ -32,6 +35,15 @@ export function xor(a: boolean, b: boolean): boolean { return !!((a ? 1 : 0) ^ (b ? 1 : 0)); } +/** + * Counts the number of truthy values in an array of booleans + * @param args + * @returns + */ +function countTrue(...args: boolean[]): number { + return args.reduce((accu, b) => (b ? accu + 1 : accu), 0); +} + interface EWDItem { flightPhaseInhib: number[]; /** warning is active */ @@ -59,7 +71,7 @@ enum FwcAuralWarning { } export class PseudoFWC { - private readonly sub = this.bus.getSubscriber(); + private readonly sub = this.bus.getSubscriber(); /** Time to inhibit SCs after one is trigger in ms */ private static readonly AURAL_SC_INHIBIT_TIME = 2000; @@ -117,10 +129,13 @@ export class PseudoFWC { private nonCancellableWarningCount = 0; + private readonly stallWarning = Subject.create(false); + private readonly masterWarningOutput = MappedSubject.create( SubscribableMapFunctions.or(), this.masterWarning, this.fireActive, + this.stallWarning, ); private readonly auralCrcOutput = MappedSubject.create( @@ -129,6 +144,8 @@ export class PseudoFWC { this.fireActive, ); + private readonly fwcOut126 = Arinc429RegisterSubject.createEmpty(); + /* 21 - AIR CONDITIONING AND PRESSURIZATION */ private readonly acsc1DiscreteWord1 = Arinc429Register.empty(); @@ -580,7 +597,7 @@ export class PseudoFWC { /* LANDING GEAR AND LIGHTS */ - private readonly aircraftOnGround = Subject.create(0); + private readonly aircraftOnGround = Subject.create(false); private readonly antiskidActive = Subject.create(false); @@ -600,6 +617,10 @@ export class PseudoFWC { private readonly lgciu2DiscreteWord1 = Arinc429Register.empty(); + private readonly lgciu1DiscreteWord2 = Arinc429Register.empty(); + + private readonly lgciu2DiscreteWord2 = Arinc429Register.empty(); + private readonly nwSteeringDisc = Subject.create(false); private readonly parkBrake = Subject.create(false); @@ -614,6 +635,28 @@ export class PseudoFWC { private readonly lgNotDownPulse2 = new NXLogicPulseNode(); + private readonly lgciu1OnGroundDisagreeConf = new NXLogicConfirmNode(1, true); + + private readonly lgciu1OnGroundAgreeConf = new NXLogicConfirmNode(0.5, true); + + private readonly lgciu1OnGroundDisagreeMem = new NXLogicMemoryNode(true); + + private readonly lgciu2OnGroundDisagreeConf = new NXLogicConfirmNode(1, true); + + private readonly lgciu2OnGroundAgreeConf = new NXLogicConfirmNode(0.5, true); + + private readonly lgciu2OnGroundDisagreeMem = new NXLogicMemoryNode(true); + + private readonly ra1OnGroundMem = new NXLogicMemoryNode(true); + + private readonly ra2OnGroundMem = new NXLogicMemoryNode(true); + + private readonly ignoreRaOnGroundTrigger = new NXLogicTriggeredMonostableNode(10, true); + + private readonly onGroundConf = new NXLogicConfirmNode(1, true); + + private onGroundImmediate = false; + /* NAVIGATION */ private readonly adirsRemainingAlignTime = Subject.create(0); @@ -624,9 +667,13 @@ export class PseudoFWC { private readonly adiru3State = Subject.create(0); - private readonly computedAirSpeed = Subject.create(Arinc429Word.empty()); + private readonly adr1Cas = Subject.create(Arinc429Word.empty()); + + private readonly adr2Cas = Arinc429Register.empty(); - private readonly computedAirSpeedToNearest2 = this.computedAirSpeed.map((it) => Math.round(it.value / 2) * 2); + private readonly adr3Cas = Arinc429Register.empty(); + + private readonly computedAirSpeedToNearest2 = this.adr1Cas.map((it) => Math.round(it.value / 2) * 2); private readonly height1Failed = Subject.create(false); @@ -638,6 +685,8 @@ export class PseudoFWC { private readonly flapsIndex = Subject.create(0); + private stallWarningRaw = ConsumerValue.create(this.sub.on('stall_warning_on'), false); + /** ENGINE AND THROTTLE */ private readonly engine1Master = ConsumerSubject.create(this.sub.on('engine1Master'), 0); @@ -894,6 +943,21 @@ export class PseudoFWC { SimVar.SetSimVarValue('L:A32NX_FWC_2_LG_RED_ARROW', SimVarValueType.Bool, on); }, true); + this.stallWarning.sub((v) => { + this.fwcOut126.setBitValue(17, v); + // set the sound on/off + SimVar.SetSimVarValue('L:A32NX_AUDIO_STALL_WARNING', 'bool', v); + }, true); + this.aircraftOnGround.sub((v) => this.fwcOut126.setBitValue(28, v)); + + this.fwcOut126.sub((v) => { + Arinc429Word.toSimVarValue('L:A32NX_FWC_1_DISCRETE_WORD_126', v.value, v.ssm); + Arinc429Word.toSimVarValue('L:A32NX_FWC_2_DISCRETE_WORD_126', v.value, v.ssm); + }, true); + + // FIXME depend on FWC state + this.fwcOut126.setSsm(Arinc429SignStatusMatrix.NormalOperation); + const sub = this.bus.getSubscriber(); this.fuelCtrTankModeSelMan.setConsumer(sub.on('fuel_ctr_tk_mode_sel_man')); @@ -907,6 +971,12 @@ export class PseudoFWC { this.rightOuterInnerValve.setConsumer(sub.on('fuel_valve_open_5')); this.rightFuelPump1Auto.setConsumer(sub.on('fuel_pump_switch_3')); this.rightFuelPump2Auto.setConsumer(sub.on('fuel_pump_switch_6')); + + // Inhibit single chimes for the first two seconds after power-on + this.auralSingleChimeInhibitTimer.schedule( + () => (this.auralSingleChimePending = false), + PseudoFWC.AURAL_SC_INHIBIT_TIME, + ); } mapOrder(array, order): [] { @@ -1003,6 +1073,7 @@ export class PseudoFWC { this.flightPhase129.set([1, 2, 9].includes(this.fwcFlightPhase.get())); 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()); // TO CONFIG button this.toConfigTestRaw = SimVar.GetSimVarValue('L:A32NX_BTN_TOCONFIG', 'bool') > 0; @@ -1045,12 +1116,22 @@ export class PseudoFWC { this.flapsIndex.set(SimVar.GetSimVarValue('L:A32NX_FLAPS_CONF_INDEX', 'number')); - this.computedAirSpeed.set(Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_COMPUTED_AIRSPEED')); + this.adr1Cas.set(Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_COMPUTED_AIRSPEED')); + this.adr2Cas.setFromSimVar('L:A32NX_ADIRS_ADR_2_COMPUTED_AIRSPEED'); + this.adr3Cas.setFromSimVar('L:A32NX_ADIRS_ADR_3_COMPUTED_AIRSPEED'); - // needs to happen before dual eng failure check - this.aircraftOnGround.set(SimVar.GetSimVarValue('SIM ON GROUND', 'Bool')); + // RA acquisition + this.radioHeight1.setFromSimVar('L:A32NX_RA_1_RADIO_ALTITUDE'); + this.radioHeight2.setFromSimVar('L:A32NX_RA_2_RADIO_ALTITUDE'); + + /* ELECTRICAL acquisition */ + this.dcESSBusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED', 'bool')); + this.dc2BusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_DC_2_BUS_IS_POWERED', 'bool')); + this.ac1BusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_AC_1_BUS_IS_POWERED', 'bool')); + this.ac2BusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_AC_2_BUS_IS_POWERED', 'bool')); + this.acESSBusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED', 'bool')); - /* ENGINE AND THROTTLE */ + /* ENGINE AND THROTTLE acquisition */ this.engine1State.set(SimVar.GetSimVarValue('L:A32NX_ENGINE_STATE:1', 'Enum')); this.engine2State.set(SimVar.GetSimVarValue('L:A32NX_ENGINE_STATE:2', 'Enum')); @@ -1059,6 +1140,7 @@ export class PseudoFWC { this.N2Eng1.set(SimVar.GetSimVarValue('L:A32NX_ENGINE_N2:1', 'number')); this.N2Eng2.set(SimVar.GetSimVarValue('L:A32NX_ENGINE_N2:2', 'number')); this.N1IdleEng.set(SimVar.GetSimVarValue('L:A32NX_ENGINE_IDLE_N1', 'number')); + // FIXME move out of acquisition to logic below const oneEngineAboveMinPower = this.engine1AboveIdle.get() || this.engine2AboveIdle.get(); this.engine1Generator.set(SimVar.GetSimVarValue('L:A32NX_ELEC_ENG_GEN_1_POTENTIAL_NORMAL', 'bool')); @@ -1071,8 +1153,6 @@ export class PseudoFWC { this.apuBleedValveOpen.set(SimVar.GetSimVarValue('L:A32NX_APU_BLEED_AIR_VALVE_OPEN', 'bool')); this.radioAlt.set(SimVar.GetSimVarValue('PLANE ALT ABOVE GROUND MINUS CG', 'feet')); - this.radioHeight1.setFromSimVar('L:A32NX_RA_1_RADIO_ALTITUDE'); - this.radioHeight2.setFromSimVar('L:A32NX_RA_2_RADIO_ALTITUDE'); this.fac1Failed.set(SimVar.GetSimVarValue('L:A32NX_FBW_FAC_FAILED:1', 'boost psi')); @@ -1092,27 +1172,8 @@ export class PseudoFWC { this.autoThrustStatus.set(SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_STATUS', 'enum')); 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.thrustLeverNotSet.set(this.autothrustLeverWarningFlex.get() || this.autothrustLeverWarningToga.get()); - // FIXME ECU doesn't have the necessary output words so we go purely on TLA - const flexThrustLimit = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_THRUST_LIMIT_TYPE', 'number') === 3; - const toPower = - this.throttle1Position.get() >= 45 || - (this.throttle1Position.get() >= 35 && flexThrustLimit) || - this.throttle2Position.get() >= 45 || - (this.throttle2Position.get() >= 35 && flexThrustLimit); - this.eng1Or2TakeoffPowerConfirm.write(toPower, deltaTime); - const raAbove1500 = this.radioHeight1.valueOr(0) > 1500 || this.radioHeight2.valueOr(0) > 1500; - this.eng1Or2TakeoffPower.set(toPower || (this.eng1Or2TakeoffPowerConfirm.read() && !raAbove1500)); - this.engDualFault.set( - !this.aircraftOnGround.get() && - ((this.fireButton1.get() && this.fireButton2.get()) || - (!this.engine1ValueSwitch.get() && !this.engine2ValueSwitch.get()) || - (this.engine1State.get() === 0 && this.engine2State.get() === 0) || - (!this.engine1CoreAtOrAboveMinIdle.get() && !this.engine2CoreAtOrAboveMinIdle.get())), - ); - - /* HYDRAULICS */ + /* HYDRAULICS acquisition */ this.blueElecPumpPBAuto.set(SimVar.GetSimVarValue('L:A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO', 'bool')); this.blueLP.set(SimVar.GetSimVarValue('L:A32NX_HYD_BLUE_EDPUMP_LOW_PRESS', 'bool')); @@ -1133,7 +1194,7 @@ export class PseudoFWC { const greenSysPressurised = SimVar.GetSimVarValue('L:A32NX_HYD_GREEN_SYSTEM_1_SECTION_PRESSURE_SWITCH', 'bool'); const yellowSysPressurised = SimVar.GetSimVarValue('L:A32NX_HYD_YELLOW_SYSTEM_1_SECTION_PRESSURE_SWITCH', 'bool'); - /* ADIRS */ + /* ADIRS acquisition */ this.adirsRemainingAlignTime.set(SimVar.GetSimVarValue('L:A32NX_ADIRS_REMAINING_IR_ALIGNMENT_TIME', 'Seconds')); @@ -1151,32 +1212,11 @@ export class PseudoFWC { this.height1Failed.set(height1.isFailureWarning()); this.height2Failed.set(height2.isFailureWarning()); // overspeed - const adr3Cas = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_3_COMPUTED_AIRSPEED'); const adr3MaxCas = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_3_MAX_AIRSPEED'); - let overspeedWarning = this.adr3OverspeedWarning.write( - adr3Cas.isNormalOperation() && adr3MaxCas.isNormalOperation() && adr3Cas.value > adr3MaxCas.value + 8, - this.aircraftOnGround.get() || - !(adr3Cas.isNormalOperation() && adr3MaxCas.isNormalOperation()) || - adr3Cas.value < adr3MaxCas.value + 4, - ); const adr1Discrete1 = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_1_DISCRETE_WORD_1'); const adr2Discrete1 = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_2_DISCRETE_WORD_1'); - if ( - !(adr1Discrete1.isNormalOperation() || adr1Discrete1.isFunctionalTest()) || - !(adr2Discrete1.isNormalOperation() || adr2Discrete1.isFunctionalTest()) - ) { - const adr3Discrete1 = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_3_DISCRETE_WORD_1'); - overspeedWarning ||= adr3Discrete1.getBitValueOr(9, false); - } - overspeedWarning ||= adr1Discrete1.getBitValueOr(9, false) || adr2Discrete1.getBitValueOr(9, false); - this.overspeedWarning.set(overspeedWarning); - /* LANDING GEAR AND LIGHTS */ - - // const [left1LandingGear] = useSimVar('L:A32NX_LGCIU_1_LEFT_GEAR_COMPRESSED', 'bool', 500); - // const [right1LandingGear] = useSimVar('L:A32NX_LGCIU_1_RIGHT_GEAR_COMPRESSED', 'bool', 500); - // const aircraftOnGround = left1LandingGear === 1 || right1LandingGear === 1; - // FIXME The landing gear triggers the dual engine failure on loading + /* LANDING GEAR AND LIGHTS acquisition */ this.antiskidActive.set(SimVar.GetSimVarValue('ANTISKID BRAKES ACTIVE', 'bool')); this.brakeFan.set(SimVar.GetSimVarValue('L:A32NX_BRAKE_FAN', 'bool')); @@ -1189,9 +1229,17 @@ export class PseudoFWC { this.lgciu1Fault.set(SimVar.GetSimVarValue('L:A32NX_LGCIU_1_FAULT', 'bool')); this.lgciu2Fault.set(SimVar.GetSimVarValue('L:A32NX_LGCIU_2_FAULT', 'bool')); this.lgciu1DiscreteWord1.setFromSimVar('L:A32NX_LGCIU_1_DISCRETE_WORD_1'); - this.lgciu2DiscreteWord1.setFromSimVar('L:A32NX_LGCIU_1_DISCRETE_WORD_1'); + this.lgciu2DiscreteWord1.setFromSimVar('L:A32NX_LGCIU_2_DISCRETE_WORD_1'); + this.lgciu1DiscreteWord2.setFromSimVar('L:A32NX_LGCIU_1_DISCRETE_WORD_2'); + this.lgciu2DiscreteWord2.setFromSimVar('L:A32NX_LGCIU_2_DISCRETE_WORD_2'); this.parkBrake.set(SimVar.GetSimVarValue('L:A32NX_PARK_BRAKE_LEVER_POS', 'Bool')); this.nwSteeringDisc.set(SimVar.GetSimVarValue('L:A32NX_HYD_NW_STRG_DISC_ECAM_MEMO', 'Bool')); + const leftCompressedHardwireLgciu1 = + this.dcESSBusPowered.get() && SimVar.GetSimVarValue('A32NX_LGCIU_1_L_GEAR_COMPRESSED', 'bool') > 0; + const leftCompressedHardwireLgciu2 = + this.dc2BusPowered.get() && SimVar.GetSimVarValue('A32NX_LGCIU_2_L_GEAR_COMPRESSED', 'bool') > 0; + + // General logic const mainGearDownlocked = (this.lgciu1DiscreteWord1.bitValueOr(23, false) || this.lgciu2DiscreteWord1.bitValueOr(23, false)) && (this.lgciu1DiscreteWord1.bitValueOr(24, false) || this.lgciu2DiscreteWord1.bitValueOr(24, false)); @@ -1200,6 +1248,83 @@ export class PseudoFWC { mainGearDownlocked && (this.lgciu1DiscreteWord1.bitValueOr(25, false) || this.lgciu2DiscreteWord1.bitValueOr(25, false)); + // on ground logic + const lgciu1Disagree = xor(leftCompressedHardwireLgciu1, this.lgciu1DiscreteWord2.bitValue(13)); + this.lgciu1OnGroundDisagreeConf.write(lgciu1Disagree, deltaTime); + this.lgciu1OnGroundAgreeConf.write(!lgciu1Disagree, deltaTime); + this.lgciu1OnGroundDisagreeMem.write( + (!this.lgciu1DiscreteWord2.isNormalOperation() && !this.lgciu1DiscreteWord2.isFunctionalTest()) || + this.lgciu1OnGroundDisagreeConf.read(), + this.lgciu1OnGroundAgreeConf.read(), + ); + const lgciu2Disagree = xor(leftCompressedHardwireLgciu2, this.lgciu2DiscreteWord2.bitValue(13)); + this.lgciu2OnGroundDisagreeConf.write(lgciu2Disagree, deltaTime); + this.lgciu2OnGroundAgreeConf.write(!lgciu2Disagree, deltaTime); + this.lgciu2OnGroundDisagreeMem.write( + (!this.lgciu2DiscreteWord2.isNormalOperation() && !this.lgciu2DiscreteWord2.isFunctionalTest()) || + this.lgciu2OnGroundDisagreeConf.read(), + this.lgciu2OnGroundAgreeConf.read(), + ); + const lgciuOnGroundDisagree = this.lgciu1OnGroundDisagreeMem.read() || this.lgciu2OnGroundDisagreeMem.read(); + const onGroundA = + leftCompressedHardwireLgciu1 && + this.lgciu1DiscreteWord2.bitValue(13) && + leftCompressedHardwireLgciu2 && + this.lgciu2DiscreteWord2.bitValue(13); + + // TODO some renaming + this.ignoreRaOnGroundTrigger.write( + this.radioHeight1.isNoComputedData() && this.radioHeight2.isNoComputedData() && !lgciuOnGroundDisagree, + deltaTime, + ); + this.ra1OnGroundMem.write( + this.radioHeight1.value < 5, + !leftCompressedHardwireLgciu1 || !leftCompressedHardwireLgciu2, + ); + this.ra2OnGroundMem.write( + this.radioHeight2.value < 5, + !leftCompressedHardwireLgciu1 || !leftCompressedHardwireLgciu2, + ); + const ra1OnGround = + (this.radioHeight1.isNormalOperation() || this.radioHeight1.isFunctionalTest()) && + (this.radioHeight1.value < 5 || this.ra1OnGroundMem.read()); + const ra2OnGround = + (this.radioHeight2.isNormalOperation() || this.radioHeight2.isFunctionalTest()) && + (this.radioHeight2.value < 5 || this.ra2OnGroundMem.read()); + const onGroundCount = countTrue( + leftCompressedHardwireLgciu1, + leftCompressedHardwireLgciu2, + ra1OnGround, + ra2OnGround, + ); + const raInvalid = this.radioHeight1.isFailureWarning() || this.radioHeight2.isFailureWarning(); + this.onGroundImmediate = + (onGroundA && this.ignoreRaOnGroundTrigger.read()) || + (onGroundCount > 2 && !raInvalid) || + (onGroundCount > 1 && raInvalid); + this.aircraftOnGround.set(this.onGroundConf.write(this.onGroundImmediate, deltaTime)); + + // 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 + const flexThrustLimit = SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_THRUST_LIMIT_TYPE', 'number') === 3; + const toPower = + this.throttle1Position.get() >= 45 || + (this.throttle1Position.get() >= 35 && flexThrustLimit) || + this.throttle2Position.get() >= 45 || + (this.throttle2Position.get() >= 35 && flexThrustLimit); + this.eng1Or2TakeoffPowerConfirm.write(toPower, deltaTime); + const raAbove1500 = this.radioHeight1.valueOr(0) > 1500 || this.radioHeight2.valueOr(0) > 1500; + this.eng1Or2TakeoffPower.set(toPower || (this.eng1Or2TakeoffPowerConfirm.read() && !raAbove1500)); + + this.engDualFault.set( + !this.aircraftOnGround.get() && + ((this.fireButton1.get() && this.fireButton2.get()) || + (!this.engine1ValueSwitch.get() && !this.engine2ValueSwitch.get()) || + (this.engine1State.get() === 0 && this.engine2State.get() === 0) || + (!this.engine1CoreAtOrAboveMinIdle.get() && !this.engine2CoreAtOrAboveMinIdle.get())), + ); + /* 22 - AUTOFLIGHT */ const fm1DiscreteWord3 = Arinc429Word.fromSimVarValue('L:A32NX_FM1_DISCRETE_WORD_3'); const fm2DiscreteWord3 = Arinc429Word.fromSimVarValue('L:A32NX_FM2_DISCRETE_WORD_3'); @@ -1210,6 +1335,22 @@ export class PseudoFWC { this.toConfigCheckedInPhase2Or3 = true; } + let overspeedWarning = this.adr3OverspeedWarning.write( + this.adr3Cas.isNormalOperation() && adr3MaxCas.isNormalOperation() && this.adr3Cas.value > adr3MaxCas.value + 8, + this.aircraftOnGround.get() || + !(this.adr3Cas.isNormalOperation() && adr3MaxCas.isNormalOperation()) || + this.adr3Cas.value < adr3MaxCas.value + 4, + ); + if ( + !(adr1Discrete1.isNormalOperation() || adr1Discrete1.isFunctionalTest()) || + !(adr2Discrete1.isNormalOperation() || adr2Discrete1.isFunctionalTest()) + ) { + const adr3Discrete1 = Arinc429Word.fromSimVarValue('L:A32NX_ADIRS_ADR_3_DISCRETE_WORD_1'); + overspeedWarning ||= adr3Discrete1.getBitValueOr(9, false); + } + overspeedWarning ||= adr1Discrete1.getBitValueOr(9, false) || adr2Discrete1.getBitValueOr(9, false); + this.overspeedWarning.set(overspeedWarning); + // TO SPEEDS NOT INSERTED const fmToSpeedsNotInserted = fm1DiscreteWord3.getBitValueOr(18, false) && fm2DiscreteWord3.getBitValueOr(18, false); @@ -1262,14 +1403,6 @@ export class PseudoFWC { /** MCDU TO CONF 3 selected */ const mcduToFlapPos3 = fm1DiscreteWord2.getBitValueOr(16, false) || fm2DiscreteWord2.getBitValueOr(16, false); - /* ELECTRICAL */ - - this.dcESSBusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED', 'bool')); - this.dc2BusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_DC_2_BUS_IS_POWERED', 'bool')); - this.ac1BusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_AC_1_BUS_IS_POWERED', 'bool')); - this.ac2BusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_AC_2_BUS_IS_POWERED', 'bool')); - this.acESSBusPowered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED', 'bool')); - /* 21 - AIR CONDITIONING AND PRESSURIZATION */ this.acsc1DiscreteWord1.setFromSimVar('L:A32NX_COND_ACSC_1_DISCRETE_WORD_1'); @@ -1915,6 +2048,20 @@ export class PseudoFWC { !((flightPhase6 && !bothRaInvalid) || flightPhase45) && (this.lgNotDownNoCancel.get() || this.lgNotDown.get()); this.lgLeverRedArrow.set(redArrow); + // 32 - Surveillance Logic + const isNormalLaw = fcdc1DiscreteWord1.getBitValue(11) || fcdc2DiscreteWord1.getBitValue(11); + // we need to check this since the MSFS SDK stall warning does not. + const isCasAbove60 = + this.adr1Cas.get().valueOr(0) > 60 || this.adr2Cas.valueOr(0) > 60 || this.adr3Cas.valueOr(0) > 60; + this.stallWarning.set( + !isNormalLaw && + isCasAbove60 && + this.stallWarningRaw.get() && + flightPhase567 && + this.radioHeight1.valueOr(Infinity) > 1500 && + this.radioHeight2.valueOr(Infinity) > 1500, + ); + /* FIRE */ this.fireButton1.set(SimVar.GetSimVarValue('L:A32NX_FIRE_BUTTON_ENG1', 'bool')); @@ -2313,6 +2460,38 @@ export class PseudoFWC { PseudoFWC.AURAL_SC_PLAY_TIME, ); } + + this.updateRowRopWarnings(); + } + + updateRowRopWarnings() { + const w = Arinc429Word.fromSimVarValue('L:A32NX_ROW_ROP_WORD_1'); + + // ROW + SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_RWY_TOO_SHORT', 'bool', w.getBitValueOr(15, false)); + + // ROP + // MAX BRAKING, only for manual braking, if maximum pedal braking is not applied + const maxBrakingSet = + SimVar.GetSimVarValue('L:A32NX_LEFT_BRAKE_PEDAL_INPUT', 'number') > 90 || + SimVar.GetSimVarValue('L:A32NX_RIGHT_BRAKE_PEDAL_INPUT', 'number') > 90; + const maxBraking = w.getBitValueOr(13, false) && !maxBrakingSet; + SimVar.SetSimVarValue('L:A32NX_AUDIO_ROP_MAX_BRAKING', 'bool', 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.getBitValueOr(12, false) || w.getBitValueOr(13, false)) && !maxReverseSet; + SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_SET_MAX_REVERSE', 'bool', !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', + ias <= 80 && ias > 4 && (w.getBitValueOr(12, false) || w.getBitValueOr(13, false)), + ); } ewdMessageFailures: EWDMessageDict = { diff --git a/fbw-a32nx/src/systems/instruments/src/PFD/PFD.tsx b/fbw-a32nx/src/systems/instruments/src/PFD/PFD.tsx index 9d92c3adafd..f90f2cc01db 100644 --- a/fbw-a32nx/src/systems/instruments/src/PFD/PFD.tsx +++ b/fbw-a32nx/src/systems/instruments/src/PFD/PFD.tsx @@ -12,6 +12,7 @@ import { } from '@flybywiresim/fbw-sdk'; import { A320Failure } from '@failures'; +import { AttitudeIndicatorWarnings } from '@flybywiresim/pfd'; import { DmcLogicEvents } from '../MsfsAvionicsCommon/providers/DmcPublisher'; import { LagFilter } from './PFDUtils'; import { Arinc429Values } from './shared/ArincValueProvider'; @@ -173,6 +174,7 @@ export class PFDComponent extends DisplayComponent { + ; - private readonly dmcPublisher: DmcPublisher; + private readonly dmcPublisher = new DmcPublisher(this.bus); + // FIXME fit this into the normal backplane pattern private fmsDataPublisher: FmsDataPublisher; + private readonly ropRowOansPublisher = new RopRowOansPublisher(this.bus); + + private readonly fwcPublisher = new FwcPublisher(this.bus); + /** * "mainmenu" = 0 * "loading" = 1 @@ -46,13 +55,15 @@ class A32NX_PFD extends BaseInstrument { constructor() { super(); - this.bus = new ArincEventBus(); - this.simVarPublisher = new PFDSimvarPublisher(this.bus); - this.hEventPublisher = new HEventPublisher(this.bus); - this.arincProvider = new ArincValueProvider(this.bus); - this.simplaneValueProvider = new SimplaneValueProvider(this.bus); - this.clock = new Clock(this.bus); - this.dmcPublisher = new DmcPublisher(this.bus); + + this.backplane.addPublisher('PfdSimvars', this.simVarPublisher); + this.backplane.addPublisher('hEvent', this.hEventPublisher); + this.backplane.addInstrument('arincProvider', this.arincProvider); + this.backplane.addInstrument('simPlane', this.simplaneValueProvider); + this.backplane.addInstrument('Clock', this.clock); + this.backplane.addPublisher('Dmc', this.dmcPublisher); + this.backplane.addPublisher('RopRowOans', this.ropRowOansPublisher); + this.backplane.addPublisher('Fwc', this.fwcPublisher); } get templateID(): string { @@ -79,9 +90,7 @@ class A32NX_PFD extends BaseInstrument { const stateSubject = Subject.create<'L' | 'R'>(getDisplayIndex() === 1 ? 'L' : 'R'); this.fmsDataPublisher = new FmsDataPublisher(this.bus, stateSubject); - this.arincProvider.init(); - this.clock.init(); - this.dmcPublisher.startPublish(); + this.backplane.init(); FSComponent.render(, document.getElementById('PFD_CONTENT')); @@ -95,21 +104,16 @@ class A32NX_PFD extends BaseInstrument { public Update(): void { super.Update(); + this.backplane.onUpdate(); + if (this.gameState !== 3) { const gamestate = this.getGameState(); if (gamestate === 3) { - this.simVarPublisher.startPublish(); - this.hEventPublisher.startPublish(); this.adirsValueProvider.start(); - this.dmcPublisher.startPublish(); this.fmsDataPublisher.startPublish(); } this.gameState = gamestate; } else { - this.simVarPublisher.onUpdate(); - this.simplaneValueProvider.onUpdate(); - this.clock.onUpdate(); - this.dmcPublisher.onUpdate(); this.fmsDataPublisher.onUpdate(); } } diff --git a/fbw-a32nx/src/systems/instruments/src/PFD/shared/ArincValueProvider.ts b/fbw-a32nx/src/systems/instruments/src/PFD/shared/ArincValueProvider.ts index d10f241fb84..5fafda83e16 100644 --- a/fbw-a32nx/src/systems/instruments/src/PFD/shared/ArincValueProvider.ts +++ b/fbw-a32nx/src/systems/instruments/src/PFD/shared/ArincValueProvider.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0 -import { ConsumerSubject, MathUtils, Publisher, Subscription } from '@microsoft/msfs-sdk'; +import { ConsumerSubject, Instrument, MathUtils, Publisher, Subscription } from '@microsoft/msfs-sdk'; import { ArincEventBus, Arinc429Register, Arinc429Word, Arinc429WordData } from '@flybywiresim/fbw-sdk'; import { getDisplayIndex } from 'instruments/src/PFD/PFD'; @@ -55,7 +55,7 @@ export interface Arinc429Values { fmTransAltRaw: number; fmTransLvlRaw: number; } -export class ArincValueProvider { +export class ArincValueProvider implements Instrument { private roll = new Arinc429Word(0); private pitch = Arinc429Register.empty(); @@ -120,6 +120,7 @@ export class ArincValueProvider { constructor(private readonly bus: ArincEventBus) {} + /** @inheritdoc */ public init() { const publisher = this.bus.getPublisher(); const subscriber = this.bus.getSubscriber(); @@ -518,6 +519,11 @@ export class ArincValueProvider { this.fm2Healthy.sub(this.determineFmToUse.bind(this), true); } + /** @inheritdoc */ + public onUpdate(): void { + // noop + } + private determineAndPublishChosenRadioAltitude(publisher: Publisher) { const ownRadioAltitudeHasData = !this.ownRadioAltitude.isFailureWarning() && !this.ownRadioAltitude.isNoComputedData(); diff --git a/fbw-a32nx/src/systems/instruments/src/PFD/shared/SimplaneValueProvider.ts b/fbw-a32nx/src/systems/instruments/src/PFD/shared/SimplaneValueProvider.ts index 076fc005874..5bd89b4827f 100644 --- a/fbw-a32nx/src/systems/instruments/src/PFD/shared/SimplaneValueProvider.ts +++ b/fbw-a32nx/src/systems/instruments/src/PFD/shared/SimplaneValueProvider.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0 -import { Publisher } from '@microsoft/msfs-sdk'; +import { Instrument, Publisher } from '@microsoft/msfs-sdk'; import { ArincEventBus } from '@flybywiresim/fbw-sdk'; export type SimplaneBaroMode = 'QNH' | 'QFE' | 'STD'; @@ -17,14 +17,20 @@ export interface SimplaneValues { selectedAltitude: number; baroMode: SimplaneBaroMode; } -export class SimplaneValueProvider { +export class SimplaneValueProvider implements Instrument { private publisher: Publisher; constructor(private readonly bus: ArincEventBus) { this.publisher = this.bus.getPublisher(); } - public onUpdate() { + /** @inheritdoc */ + public init(): void { + // noop + } + + /** @inheritdoc */ + public onUpdate(): void { const units = Simplane.getPressureSelectedUnits(); const pressure = Simplane.getPressureValue(units); const isSelected = Simplane.getAutoPilotAirspeedSelected(); diff --git a/fbw-a32nx/src/systems/instruments/src/PFD/tsconfig.json b/fbw-a32nx/src/systems/instruments/src/PFD/tsconfig.json index 2d56720368e..85632976a98 100644 --- a/fbw-a32nx/src/systems/instruments/src/PFD/tsconfig.json +++ b/fbw-a32nx/src/systems/instruments/src/PFD/tsconfig.json @@ -27,7 +27,9 @@ "@shared/*": ["./shared/src/*"], "@tcas/*": ["./tcas/src/*"], "@typings/*": ["../../../fbw-common/src/typings/*"], - "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index-no-react.ts"] + "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index-no-react.ts"], + "@flybywiresim/pfd": ["../../../fbw-common/src/systems/instruments/src/PFD/index.ts"], + "@flybywiresim/msfs-avionics-common": ["../../../fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/index.ts"] } } } diff --git a/fbw-a32nx/src/systems/tsconfig.json b/fbw-a32nx/src/systems/tsconfig.json index a97b05c32e6..8da0d744708 100644 --- a/fbw-a32nx/src/systems/tsconfig.json +++ b/fbw-a32nx/src/systems/tsconfig.json @@ -28,7 +28,8 @@ "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index.ts"], "@flybywiresim/flypad": ["../../../fbw-common/src/systems/instruments/src/EFB/index.ts"], "@flybywiresim/clock": ["../../../fbw-common/src/systems/instruments/src/Clock/index.ts"], - "@flybywiresim/bat": ["../../../fbw-common/src/systems/instruments/src/BAT/index.ts"] + "@flybywiresim/bat": ["../../../fbw-common/src/systems/instruments/src/BAT/index.ts"], + "@flybywiresim/pfd": ["../../../fbw-common/src/systems/instruments/src/PFD/index.ts"] } }, } diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/sound/sound.xml b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/sound/sound.xml index c9813d57010..e8f98f24a87 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/sound/sound.xml +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/sound/sound.xml @@ -1100,6 +1100,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1187,13 +1224,6 @@ - - - - - - - diff --git a/fbw-a380x/src/systems/instruments/src/Common/definitions.scss b/fbw-a380x/src/systems/instruments/src/Common/definitions.scss index df7799e67d8..35e0f0d15b7 100644 --- a/fbw-a380x/src/systems/instruments/src/Common/definitions.scss +++ b/fbw-a380x/src/systems/instruments/src/Common/definitions.scss @@ -12,7 +12,7 @@ $font-size-huge: 22px; $font-size-title: 24px; $display-white: #ffffff; -$display-grey: #787878; +$display-grey: #4A5071; $display-dark-grey: #b3b3b3; $display-light-grey: lightgray; $display-amber: #e68000; diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/CdsDisplayUnit.tsx b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/CdsDisplayUnit.tsx index e6bdb16b93f..2ebe4312d58 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/CdsDisplayUnit.tsx +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/CdsDisplayUnit.tsx @@ -20,8 +20,8 @@ import './common.scss'; export const getDisplayIndex = () => { const url = Array.from(document.querySelectorAll('vcockpit-panel > *')) - .find((it) => it.tagName.toLowerCase() !== 'wasm-instrument') - .getAttribute('url'); + ?.find((it) => it.tagName.toLowerCase() !== 'wasm-instrument') + ?.getAttribute('url'); return url ? parseInt(url.substring(url.length - 1), 10) : 0; }; diff --git a/fbw-a380x/src/systems/instruments/src/PFD/AttitudeIndicatorWarningsA380.tsx b/fbw-a380x/src/systems/instruments/src/PFD/AttitudeIndicatorWarningsA380.tsx new file mode 100644 index 00000000000..304e56bf3f8 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/PFD/AttitudeIndicatorWarningsA380.tsx @@ -0,0 +1,142 @@ +import { DisplayComponent, FSComponent, MappedSubject, Subject, VNode } from '@microsoft/msfs-sdk'; +import { Arinc429RegisterSubject, ArincEventBus } from '@flybywiresim/fbw-sdk'; +import { TawsDataEvents } from '@flybywiresim/msfs-avionics-common'; + +interface AttitudeIndicatorWarningsA380Props { + bus: ArincEventBus; + instrument: BaseInstrument; +} + +export class AttitudeIndicatorWarningsA380 extends DisplayComponent { + private readonly warningGroupRef = FSComponent.createRef(); + + private readonly gpwsWord1 = Arinc429RegisterSubject.createEmpty(); + + private readonly gpwsPullUpActive = this.gpwsWord1.map((w) => w.bitValueOr(12, false)); + + private readonly gpwsSinkRateActive = this.gpwsWord1.map((w) => w.bitValueOr(11, false)); + + private readonly gpwsDontSinkActive = this.gpwsWord1.map((w) => w.bitValueOr(14, false)); + + private readonly gpwsTooLowGearActive = this.gpwsWord1.map((w) => w.bitValueOr(15, false)); + + private readonly gpwsTooLowTerrainActive = this.gpwsWord1.map((w) => w.bitValueOr(17, false)); + + private readonly gpwsTooLowFlapsActive = this.gpwsWord1.map((w) => w.bitValueOr(16, false)); + + private readonly gpwsGlideSlopeActive = this.gpwsWord1.map((w) => w.bitValueOr(18, false)); + + onAfterRender(node: VNode): void { + super.onAfterRender(node); + + const sub = this.props.bus.getSubscriber(); + + sub.on('egpws_alert_discrete_word_1_1').whenChanged().handle((v) => { + this.gpwsWord1.setWord(v); + }); + } + + render(): VNode { + return ( + + pullUp, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + PULL UP + + sinkRate && !pullUp, + this.gpwsSinkRateActive, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + SINK RATE + + dontSink && !pullUp, + this.gpwsDontSinkActive, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + DONT SINK + + tooLowGear && !pullUp, + this.gpwsTooLowGearActive, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + TOO LOW GEAR + + tooLowTerrain && !pullUp, + this.gpwsTooLowTerrainActive, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + TOO LOW TERRAIN + + tooLowFlaps && !pullUp, + this.gpwsTooLowFlapsActive, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + TOO LOW FLAPS + + glideSlope && !pullUp, + this.gpwsGlideSlopeActive, + this.gpwsPullUpActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + GLIDE SLOPE + + + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/PFD/LandingSystemIndicator.tsx b/fbw-a380x/src/systems/instruments/src/PFD/LandingSystemIndicator.tsx index c7e3a77b7fa..1e5d0fb8983 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/LandingSystemIndicator.tsx +++ b/fbw-a380x/src/systems/instruments/src/PFD/LandingSystemIndicator.tsx @@ -1,4 +1,13 @@ -import { DisplayComponent, EventBus, FSComponent, HEvent, Subject, VNode } from '@microsoft/msfs-sdk'; +import { + ConsumerSubject, + DisplayComponent, + EventBus, + FSComponent, + HEvent, + MappedSubject, + Subject, + VNode, +} from '@microsoft/msfs-sdk'; import { getDisplayIndex } from 'instruments/src/PFD/PFD'; import { Arinc429Word } from '@flybywiresim/fbw-sdk'; import { Arinc429Values } from './shared/ArincValueProvider'; @@ -96,6 +105,7 @@ export class LandingSystem extends DisplayComponent<{ bus: EventBus; instrument: + @@ -223,7 +233,7 @@ class LandingSystemInfo extends DisplayComponent<{ bus: EventBus }> { {this.freqTextLeading} - + {this.freqTextTrailing} @@ -602,9 +612,49 @@ class MarkerBeaconIndicator extends DisplayComponent<{ bus: EventBus }> { render(): VNode { return ( - + {this.markerText} ); } } + +class LsReminder extends DisplayComponent<{ bus: EventBus }> { + private readonly lsReminderRef = FSComponent.createRef(); + + private readonly hasLoc = ConsumerSubject.create(null, false); + + private readonly lsButton = ConsumerSubject.create(null, false); + + private readonly ilsReminderShown = MappedSubject.create( + ([hasLoc, lsButton]) => hasLoc && lsButton, + this.hasLoc, + this.lsButton, + ); + + onAfterRender(node: VNode): void { + super.onAfterRender(node); + + const sub = this.props.bus.getSubscriber(); + + this.hasLoc.setConsumer(sub.on('hasLoc').whenChanged()); + this.lsButton.setConsumer(sub.on(getDisplayIndex() === 2 ? 'ls2Button' : 'ls1Button').whenChanged()); + + // normally the ident and freq should be always displayed when an ILS freq is set, but currently it only show when we have a signal + this.ilsReminderShown.sub((it) => { + if (it) { + this.lsReminderRef.instance.style.display = 'inline'; + } else { + this.lsReminderRef.instance.style.display = 'none'; + } + }); + } + + render(): VNode { + return ( + + ILS + + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/PFD/LinearDeviationIndicator.tsx b/fbw-a380x/src/systems/instruments/src/PFD/LinearDeviationIndicator.tsx new file mode 100644 index 00000000000..53b4c8b3e97 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/PFD/LinearDeviationIndicator.tsx @@ -0,0 +1,133 @@ +import { DisplayComponent, EventBus, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; +import { FmsVars } from 'instruments/src/MsfsAvionicsCommon/providers/FmsDataPublisher'; +import { Arinc429Values } from 'instruments/src/PFD/shared/ArincValueProvider'; + +type LinearDeviationIndicatorProps = { + bus: EventBus, +} + +export class LinearDeviationIndicator extends DisplayComponent { + private shouldShowLinearDeviation = false; + + private componentTransform = Subject.create(''); + + private upperLinearDeviationReadoutText = Subject.create('00'); + + private upperLinearDeviationReadoutVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private lowerLinearDeviationReadoutText = Subject.create('00'); + + private lowerLinearDeviationReadoutVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private linearDeviationDotVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private linearDeviationDotUpperHalfVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private linearDeviationDotLowerHalfVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private latchSymbolVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + // TODO: Use ARINC value for this + private flightPathAltitude: Feet = 0; + + onAfterRender(node: VNode): void { + super.onAfterRender(node); + + const sub = this.props.bus.getSubscriber(); + + sub.on('altitudeAr').handle((alt) => { + if (!alt.isNormalOperation() || !this.shouldShowLinearDeviation) { + this.upperLinearDeviationReadoutVisibility.set('hidden'); + this.lowerLinearDeviationReadoutVisibility.set('hidden'); + this.linearDeviationDotLowerHalfVisibility.set('hidden'); + this.linearDeviationDotUpperHalfVisibility.set('hidden'); + this.linearDeviationDotVisibility.set('hidden'); + + return; + } + + const deviation = alt.value - this.flightPathAltitude; + const pixelOffset = this.pixelOffsetFromDeviation(Math.max(Math.min(deviation, 500), -500)); + + this.componentTransform.set(`translate(0 ${pixelOffset})`); + + const linearDeviationReadoutText = Math.min(99, Math.round(Math.abs(deviation) / 100)).toFixed(0).padStart(2, '0'); + + if (this.upperLinearDeviationReadoutVisibility.get() === 'visible') { + this.upperLinearDeviationReadoutText.set(linearDeviationReadoutText); + } + + if (this.lowerLinearDeviationReadoutVisibility.get() === 'visible') { + this.lowerLinearDeviationReadoutText.set(linearDeviationReadoutText); + } + + if (deviation > 540) { + this.lowerLinearDeviationReadoutVisibility.set('visible'); + this.linearDeviationDotLowerHalfVisibility.set('visible'); + + this.upperLinearDeviationReadoutVisibility.set('hidden'); + this.linearDeviationDotUpperHalfVisibility.set('hidden'); + + this.linearDeviationDotVisibility.set('hidden'); + } else if (deviation > -500 && deviation < 500) { + this.lowerLinearDeviationReadoutVisibility.set('hidden'); + this.linearDeviationDotLowerHalfVisibility.set('hidden'); + + this.upperLinearDeviationReadoutVisibility.set('hidden'); + this.linearDeviationDotUpperHalfVisibility.set('hidden'); + + this.linearDeviationDotVisibility.set('visible'); + } else if (deviation < -540) { + this.lowerLinearDeviationReadoutVisibility.set('hidden'); + this.linearDeviationDotLowerHalfVisibility.set('hidden'); + + this.upperLinearDeviationReadoutVisibility.set('visible'); + this.linearDeviationDotUpperHalfVisibility.set('visible'); + + this.linearDeviationDotVisibility.set('hidden'); + } + }); + + sub.on('linearDeviationActive').whenChanged().handle((isActive) => this.shouldShowLinearDeviation = isActive); + + sub.on('verticalProfileLatched').whenChanged().handle((s) => this.latchSymbolVisibility.set(s ? 'visible' : 'hidden')); + + sub.on('targetAltitude').atFrequency(1000 / 60).handle((targetAltitude) => { + this.flightPathAltitude = targetAltitude; + }); + } + + render(): VNode { + return ( + + {this.upperLinearDeviationReadoutText} + + + + + + + {this.lowerLinearDeviationReadoutText} + + ); + } + + private pixelOffsetFromDeviation(deviation: number) { + return deviation * 40.5 / 500; + } +} diff --git a/fbw-a380x/src/systems/instruments/src/PFD/PFD.tsx b/fbw-a380x/src/systems/instruments/src/PFD/PFD.tsx index f5fbb513b96..70778b05585 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/PFD.tsx +++ b/fbw-a380x/src/systems/instruments/src/PFD/PFD.tsx @@ -1,7 +1,11 @@ import { A380Failure } from '@flybywiresim/failures'; import { ClockEvents, ComponentProps, DisplayComponent, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; -import { LowerArea } from 'instruments/src/PFD/LowerArea'; import { Arinc429Word, ArincEventBus, FailuresConsumer } from '@flybywiresim/fbw-sdk'; + +import { AttitudeIndicatorWarnings } from '@flybywiresim/pfd'; +import { AttitudeIndicatorWarningsA380 } from 'instruments/src/PFD/AttitudeIndicatorWarningsA380'; +import { LowerArea } from 'instruments/src/PFD/LowerArea'; +import { LinearDeviationIndicator } from 'instruments/src/PFD/LinearDeviationIndicator'; import { CdsDisplayUnit, DisplayUnitID } from '../MsfsAvionicsCommon/CdsDisplayUnit'; import { LagFilter } from './PFDUtils'; import { Arinc429Values } from './shared/ArincValueProvider'; @@ -162,6 +166,8 @@ export class PFDComponent extends DisplayComponent { + + { /> + diff --git a/fbw-a380x/src/systems/instruments/src/PFD/SpeedIndicator.tsx b/fbw-a380x/src/systems/instruments/src/PFD/SpeedIndicator.tsx index 6c9f68731bd..cb4c426df0b 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/SpeedIndicator.tsx +++ b/fbw-a380x/src/systems/instruments/src/PFD/SpeedIndicator.tsx @@ -9,6 +9,7 @@ import { VNode, } from '@microsoft/msfs-sdk'; import { Arinc429Word, ArincEventBus } from '@flybywiresim/fbw-sdk'; +import { FmsVars } from 'instruments/src/MsfsAvionicsCommon/providers/FmsDataPublisher'; import { LagFilter, RateLimiter } from './PFDUtils'; import { PFDSimvars } from './shared/PFDSimvarPublisher'; import { VerticalTape } from './VerticalTape'; @@ -1160,11 +1161,92 @@ class SpeedTarget extends DisplayComponent<{ bus: ArincEventBus }> { style="transform: translate3d(0px, 0px, 0px)" d="m19.274 81.895 5.3577 1.9512v-6.0476l-5.3577 1.9512" /> + ); } } +class SpeedMargins extends DisplayComponent<{ bus: ArincEventBus }> { + private shouldShowMargins = false; + + private currentSpeed = Subject.create(Arinc429Word.empty()); + + private upperSpeedMarginVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private lowerSpeedMarginVisibility = Subject.create<'visible' | 'hidden'>('hidden'); + + private upperMarginTransform = Subject.create('translate(0 0)'); + + private lowerMarginTransform = Subject.create('translate(0 0)'); + + onAfterRender(node: VNode): void { + super.onAfterRender(node); + const sub = this.props.bus.getArincSubscriber(); + + sub + .on('showSpeedMargins') + .whenChanged() + .handle((active) => (this.shouldShowMargins = active)); + + sub + .on('speedAr') + .withArinc429Precision(2) + .handle((s) => this.currentSpeed.set(s)); + + sub.on('upperSpeedMargin').handle(this.updateMargin(this.upperSpeedMarginVisibility, this.upperMarginTransform)); + sub.on('lowerSpeedMargin').handle(this.updateMargin(this.lowerSpeedMarginVisibility, this.lowerMarginTransform)); + } + + render(): VNode { + return ( + + + + + ); + } + + private updateMargin(visibility: Subject<'visible' | 'hidden'>, transform: Subject) { + return (speed: number) => { + const shouldForceHideMargins = !this.shouldShowMargins || !this.currentSpeed.get().isNormalOperation(); + const marginIsVisible = visibility.get() === 'visible'; + + if (shouldForceHideMargins) { + if (marginIsVisible) { + visibility.set('hidden'); + } + + return; + } + + const isInRange = Math.abs(this.currentSpeed.get().value - speed) < DisplayRange; + if (isInRange) { + const offset = ( + Math.round((100 * (this.currentSpeed.get().value - speed) * DistanceSpacing) / ValueSpacing) / 100 + ).toFixed(2); + transform.set(`translate(0 ${offset})`); + } + + if (isInRange !== marginIsVisible) { + visibility.set(isInRange ? 'visible' : 'hidden'); + } + }; + } +} + export class MachNumber extends DisplayComponent<{ bus: EventBus }> { private machTextSub = Subject.create(''); diff --git a/fbw-a380x/src/systems/instruments/src/PFD/animations.scss b/fbw-a380x/src/systems/instruments/src/PFD/animations.scss index 97f9bd61f66..e0f1df54129 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/animations.scss +++ b/fbw-a380x/src/systems/instruments/src/PFD/animations.scss @@ -7,10 +7,10 @@ @mixin GenericPulsingStroke($color, $name) { @keyframes #{$name} { - 0% {stroke: scale-color($color, $lightness: -30%);} - 50% {stroke: scale-color($color, $lightness: -30%);} - 51% {stroke: scale-color($color, $lightness: 30%);} - 100% {stroke: scale-color($color, $lightness: 30%);} + 0% {stroke: scale-color($color, $lightness: -50%);} + 50% {stroke: scale-color($color, $lightness: -50%);} + 51% {stroke: scale-color($color, $lightness: 50%);} + 100% {stroke: scale-color($color, $lightness: 50%);} } animation-name: $name; } @@ -69,3 +69,9 @@ animation-duration: 200ms; animation-iteration-count: infinite; } + +.RwyAheadAnimation { + @include GenericPulsingStroke($display-yellow, pulse-yellow-stroke); + animation-duration: 1s; + animation-iteration-count: infinite; +} diff --git a/fbw-a380x/src/systems/instruments/src/PFD/config.json b/fbw-a380x/src/systems/instruments/src/PFD/config.json index b83fde2556a..5fd4d2dc9f4 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/config.json +++ b/fbw-a380x/src/systems/instruments/src/PFD/config.json @@ -2,6 +2,7 @@ "index": "./instrument.tsx", "isInteractive": false, "extraDeps": [ - "../MsfsAvionicsCommon" + "../MsfsAvionicsCommon", + "fbw-common/src/systems/instruments/src/PFD" ] } diff --git a/fbw-a380x/src/systems/instruments/src/PFD/instrument.tsx b/fbw-a380x/src/systems/instruments/src/PFD/instrument.tsx index 941254c6ad1..12a8c398efd 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/instrument.tsx +++ b/fbw-a380x/src/systems/instruments/src/PFD/instrument.tsx @@ -1,14 +1,16 @@ -import { Clock, FSComponent, HEventPublisher, InstrumentBackplane } from '@microsoft/msfs-sdk'; -import { ArincEventBus } from '@flybywiresim/fbw-sdk'; - +import { Clock, FSComponent, HEventPublisher, InstrumentBackplane, Subject } from '@microsoft/msfs-sdk'; +import { ArincEventBus, EfisSide } from '@flybywiresim/fbw-sdk'; +import { getDisplayIndex } from 'instruments/src/MsfsAvionicsCommon/CdsDisplayUnit'; +import { DmcEvents, DmcPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/DmcPublisher'; +import { FmsDataPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/FmsDataPublisher'; import { PFDComponent } from './PFD'; import { AdirsValueProvider } from './shared/AdirsValueProvider'; import { ArincValueProvider } from './shared/ArincValueProvider'; import { PFDSimvarPublisher } from './shared/PFDSimvarPublisher'; import { SimplaneValueProvider } from './shared/SimplaneValueProvider'; -import { DmcEvents, DmcPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/DmcPublisher'; import './style.scss'; +import { RopRowOansPublisher, TawsPublisher } from '@flybywiresim/msfs-avionics-common'; class A380X_PFD extends BaseInstrument { private readonly bus = new ArincEventBus(); @@ -29,9 +31,19 @@ class A380X_PFD extends BaseInstrument { private readonly dmcPublisher = new DmcPublisher(this.bus); + private readonly fmsDataPublisher: FmsDataPublisher; + + private readonly ropRowOansPublisher = new RopRowOansPublisher(this.bus); + + private readonly tawsPublisher = new TawsPublisher(this.bus); + constructor() { super(); + const side: EfisSide = getDisplayIndex() === 1 ? 'L' : 'R'; + const stateSubject = Subject.create<'L' | 'R'>(side); + this.fmsDataPublisher = new FmsDataPublisher(this.bus, stateSubject); + this.backplane.addInstrument('Clock', this.clock); this.backplane.addPublisher('HEvent', this.hEventPublisher); this.backplane.addPublisher('PfdSimVars', this.simVarPublisher); @@ -39,6 +51,9 @@ class A380X_PFD extends BaseInstrument { this.backplane.addInstrument('Simplane', this.simplaneValueProvider); this.backplane.addInstrument('AdirsProvider', this.adirsValueProvider); this.backplane.addPublisher('DmcPublisher', this.dmcPublisher); + this.backplane.addPublisher('FmsDataPublisher', this.fmsDataPublisher); + this.backplane.addPublisher('RopRowOansPublisher', this.ropRowOansPublisher); + this.backplane.addPublisher('TawsPublisher', this.tawsPublisher); } get templateID(): string { @@ -58,8 +73,6 @@ class A380X_PFD extends BaseInstrument { this.backplane.init(); - this.bus.getSubscriber().on('trueRefActive').handle(console.log); - FSComponent.render(, document.getElementById('PFD_CONTENT')); // Remove "instrument didn't load" text diff --git a/fbw-a380x/src/systems/instruments/src/PFD/shared/definitions.scss b/fbw-a380x/src/systems/instruments/src/PFD/shared/definitions.scss deleted file mode 100644 index 28684a9de4c..00000000000 --- a/fbw-a380x/src/systems/instruments/src/PFD/shared/definitions.scss +++ /dev/null @@ -1,20 +0,0 @@ -$font-size-small: 14px; -$font-size-medium: 16px; -$font-size-large: 17px; -$font-size-larger: 18px; -$font-size-xlarge: 20px; -$font-size-huge: 22px; -$font-size-title: 24px; - -$display-white: #ffffff; -$display-grey: #787878; -$display-dark-grey: #b3b3b3; -$display-light-grey: lightgray; -$display-amber: #e68000; -$display-cyan: #00ffff; -$display-green: #00ff00; -$display-magenta: #ff94ff; -$display-red: #ff0000; -$display-yellow: #ffff00; - -$display-background: #000000; diff --git a/fbw-a380x/src/systems/instruments/src/PFD/style.scss b/fbw-a380x/src/systems/instruments/src/PFD/style.scss index adc1b5ed281..95a4023eb84 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/style.scss +++ b/fbw-a380x/src/systems/instruments/src/PFD/style.scss @@ -1,10 +1,10 @@ -@import "shared/definitions"; +@import "../Common/definitions"; @import "animations"; @font-face { font-family: "Ecam"; //noinspection CssUnknownTarget - src: url("/Fonts/ECAMFontRegular.ttf") format("truetype"); + src: url("/Fonts/FBW-Display-EIS-A380.ttf") format("truetype"); font-weight: normal; font-style: normal; } @@ -244,10 +244,10 @@ text.Red { } .EarthFill { - fill: #9c480c; + fill: #5D2518; } .SkyFill { - fill: #0698ff; + fill: #0073DE; } .BlackFill { fill: $display-background; @@ -257,6 +257,7 @@ text.Red { fill: $display-grey; stroke: none; } + .BackgroundFill { fill: $display-background; stroke: none; diff --git a/fbw-a380x/src/systems/instruments/src/PFD/tsconfig.json b/fbw-a380x/src/systems/instruments/src/PFD/tsconfig.json index 1b6e248abd5..0f6112b0744 100644 --- a/fbw-a380x/src/systems/instruments/src/PFD/tsconfig.json +++ b/fbw-a380x/src/systems/instruments/src/PFD/tsconfig.json @@ -21,7 +21,9 @@ "@flybywiresim/failures": ["failures"], "@tcas/*": ["./tcas/src/*"], "@typings/*": ["../typings"], - "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index-no-react.ts"] + "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index-no-react.ts"], + "@flybywiresim/pfd": ["../../../fbw-common/src/systems/instruments/src/PFD/index.ts"], + "@flybywiresim/msfs-avionics-common": ["../../../fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/index.ts"] } } } diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts b/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts index 07e80472a40..8462c987ea1 100644 --- a/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts +++ b/fbw-a380x/src/systems/systems-host/systems/LegacyFwc.ts @@ -1,7 +1,7 @@ /* eslint-disable no-underscore-dangle */ /* eslint-disable camelcase */ // TODO remove this once Rust implementation is up and running -import { Arinc429Word } from '@flybywiresim/fbw-sdk'; +import { Arinc429RegisterSubject, Arinc429Word } from '@flybywiresim/fbw-sdk'; enum FwcFlightPhase { ElecPwr = 1, @@ -168,6 +168,7 @@ export class LegacyFwc { this._updateTakeoffMemo(_deltaTime); this._updateLandingMemo(_deltaTime); this._updateAltitudeWarning(); + this.updateRowRopWarnings(); } _updateButtons(_deltaTime: number) { @@ -489,6 +490,30 @@ export class LegacyFwc { } return true; } + + updateRowRopWarnings() { + const w = Arinc429Word.fromSimVarValue('L:A32NX_ROW_ROP_WORD_1'); + + // ROW + SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_RWY_TOO_SHORT', 'bool', w.getBitValueOr(15, false)); + + // ROP + // MAX BRAKING, only for manual braking, if maximum pedal braking is not applied + const maxBrakingSet = SimVar.GetSimVarValue('L:A32NX_LEFT_BRAKE_PEDAL_INPUT', 'number') > 90 || SimVar.GetSimVarValue('L:A32NX_RIGHT_BRAKE_PEDAL_INPUT', 'number') > 90; + const maxBraking = w.getBitValueOr(13, false) && !maxBrakingSet; + SimVar.SetSimVarValue('L:A32NX_AUDIO_ROP_MAX_BRAKING', 'bool', maxBraking); + + // SET MAX REVERSE, if not already max. reverse set and !MAX_BRAKING + const maxReverseSet = SimVar.GetSimVarValue('L:XMLVAR_Throttle2Position', 'number') < 0.1 && SimVar.GetSimVarValue('L:XMLVAR_Throttle3Position', 'number') < 0.1; + const maxReverse = (w.getBitValueOr(12, false) || w.getBitValueOr(13, false)) && !maxReverseSet; + SimVar.SetSimVarValue('L:A32NX_AUDIO_ROW_SET_MAX_REVERSE', 'bool', !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', (ias <= 80 && ias > 4) && (w.getBitValueOr(12, false) || w.getBitValueOr(13, false))); + + + } } /* diff --git a/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts b/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts index b789f05767b..5b916f33102 100644 --- a/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts +++ b/fbw-a380x/src/systems/systems-host/systems/LegacyGpws.ts @@ -1,4 +1,4 @@ -import { Arinc429Word, NXDataStore, RadioAutoCallOutFlags } from '@flybywiresim/fbw-sdk'; +import { Arinc429SignStatusMatrix, Arinc429Word, NXDataStore, RadioAutoCallOutFlags } from '@flybywiresim/fbw-sdk'; import { FmgcFlightPhase } from '@shared/flightphase'; import { LegacySoundManager, soundList } from 'systems-host/systems/LegacySoundManager'; @@ -49,6 +49,10 @@ export class LegacyGpws { RetardState: LegacyStateMachine; + egpwsAlertDiscreteWord1 = Arinc429Word.empty(); + + egpwsAlertDiscreteWord2 = Arinc429Word.empty(); + // eslint-disable-next-line camelcase constructor(private soundManager: LegacySoundManager) { this.autoCallOutPins = DEFAULT_RADIO_AUTO_CALL_OUTS; @@ -116,7 +120,7 @@ export class LegacyGpws { {}, ], onChange: (current) => { - SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', current >= 1); + this.setGlideSlopeWarning(current >= 1); }, }, ]; @@ -129,11 +133,48 @@ export class LegacyGpws { this.RetardState.setState('landed'); } + gpwsUpdateDiscreteWords() { + this.egpwsAlertDiscreteWord1.ssm = Arinc429SignStatusMatrix.NormalOperation; + this.egpwsAlertDiscreteWord1.setBitValue(11, this.modes[0].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(12, this.modes[0].current === 2); + this.egpwsAlertDiscreteWord1.setBitValue(13, this.modes[1].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(12, this.modes[1].current === 2); + this.egpwsAlertDiscreteWord1.setBitValue(14, this.modes[2].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(15, this.modes[3].current === 1); + this.egpwsAlertDiscreteWord1.setBitValue(16, this.modes[3].current === 2); + this.egpwsAlertDiscreteWord1.setBitValue(17, this.modes[3].current === 3); + this.egpwsAlertDiscreteWord1.setBitValue(18, this.modes[4].current === 1); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_1', this.egpwsAlertDiscreteWord1.value, this.egpwsAlertDiscreteWord1.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_1', this.egpwsAlertDiscreteWord1.value, this.egpwsAlertDiscreteWord1.ssm); + + this.egpwsAlertDiscreteWord2.ssm = Arinc429SignStatusMatrix.NormalOperation; + this.egpwsAlertDiscreteWord2.setBitValue(14, false); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + } + + setGlideSlopeWarning(state: boolean) { + SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', state ? 1 : 0); // Still need this for XML + this.egpwsAlertDiscreteWord2.setBitValue(11, state); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + } + + setGpwsWarning(state: boolean) { + SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', state ? 1 : 0); // Still need this for XML + this.egpwsAlertDiscreteWord2.setBitValue(12, state); + this.egpwsAlertDiscreteWord2.setBitValue(13, state); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_1_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + Arinc429Word.toSimVarValue('L:A32NX_EGPWS_ALERT_2_DISCRETE_WORD_2', this.egpwsAlertDiscreteWord2.value, this.egpwsAlertDiscreteWord2.ssm); + } + init() { console.log('A32NX_GPWS init'); - SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', 0); - SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', 0); + this.setGlideSlopeWarning(false); + this.setGpwsWarning(false); + this.egpwsAlertDiscreteWord1.ssm = Arinc429SignStatusMatrix.NormalOperation; + this.egpwsAlertDiscreteWord1.setBitValue(12, false); // eslint-disable-next-line max-len NXDataStore.getAndSubscribe('CONFIG_A32NX_FWC_RADIO_AUTO_CALL_OUT_PINS', (k, v) => k === 'CONFIG_A32NX_FWC_RADIO_AUTO_CALL_OUT_PINS' && (this.autoCallOutPins = Number(v)), DEFAULT_RADIO_AUTO_CALL_OUTS.toString()); @@ -188,11 +229,12 @@ export class LegacyGpws { this.Mode4MaxRAAlt = NaN; } - SimVar.SetSimVarValue('L:A32NX_GPWS_GS_Warning_Active', 'Bool', 0); - SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', 0); + this.setGlideSlopeWarning(false); + this.setGpwsWarning(false); } this.GPWSComputeLightsAndCallouts(); + this.gpwsUpdateDiscreteWords(); if ((mda !== 0 || (dh !== -1 && dh !== -2) && phase === FmgcFlightPhase.Approach)) { let minimumsDA; // MDA or DH @@ -293,7 +335,7 @@ export class LegacyGpws { } const illuminateGpwsLight = activeTypes.some((type) => type.gpwsLight); - SimVar.SetSimVarValue('L:A32NX_GPWS_Warning_Active', 'Bool', illuminateGpwsLight); + this.setGpwsWarning(illuminateGpwsLight); } /** diff --git a/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/index.ts b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/index.ts new file mode 100644 index 00000000000..254ec8d9f00 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/index.ts @@ -0,0 +1 @@ +export * from './providers'; diff --git a/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/FwcPublisher.ts b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/FwcPublisher.ts new file mode 100644 index 00000000000..72731085182 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/FwcPublisher.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2024 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +/* eslint-disable camelcase */ + +import { + EventBus, + IndexedEventType, + PublishPacer, + SimVarPublisher, + SimVarPublisherEntry, + SimVarValueType, +} from '@microsoft/msfs-sdk'; + +interface BaseFwcEvents { + fwc_discrete_word_126: number; +} + +/** + * Indexed events related to air data computer information. + */ +type FwcIndexedEvents = { + [P in keyof BaseFwcEvents as IndexedEventType

]: BaseFwcEvents[P]; +}; + +export interface FwcDataEvents extends BaseFwcEvents, FwcIndexedEvents {} + +export class FwcPublisher extends SimVarPublisher { + /** + * Creates an AdcPublisher. + * @param bus The event bus to which to publish. + * @param pacer An optional pacer to use to control the rate of publishing. + */ + public constructor(bus: EventBus, pacer?: PublishPacer) { + const simvars = new Map>([ + [ + 'fwc_discrete_word_126', + { name: 'L:A32NX_FWC_#index#_DISCRETE_WORD_126', type: SimVarValueType.Number, indexed: true }, + ], + ]); + super(simvars, bus, pacer); + } +} diff --git a/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/RopRowOansPublisher.ts b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/RopRowOansPublisher.ts new file mode 100644 index 00000000000..c45591b7310 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/RopRowOansPublisher.ts @@ -0,0 +1,18 @@ +import { EventBus, SimVarPublisher, SimVarValueType } from '@microsoft/msfs-sdk'; + +export interface RopRowOansSimVars { + rowRopWord1Raw: number; + oansWord1Raw: number; +} + +export class RopRowOansPublisher extends SimVarPublisher { + constructor(bus: EventBus) { + super( + new Map([ + ['rowRopWord1Raw', { name: 'L:A32NX_ROW_ROP_WORD_1', type: SimVarValueType.Number }], + ['oansWord1Raw', { name: 'L:A32NX_OANS_WORD_1', type: SimVarValueType.Number }], + ]), + bus, + ); + } +} diff --git a/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/TawsPublisher.ts b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/TawsPublisher.ts new file mode 100644 index 00000000000..e30376866bb --- /dev/null +++ b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/TawsPublisher.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2024 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +/* eslint-disable camelcase */ + +import { + EventBus, + IndexedEventType, + PublishPacer, + SimVarPublisher, + SimVarPublisherEntry, + SimVarValueType, +} from '@microsoft/msfs-sdk'; + +interface BaseTawsEvents { + egpws_alert_discrete_word_1: number; + egpws_alert_discrete_word_2: number; +} + +/** + * Indexed events related to air data computer information. + */ +type TawsIndexedEvents = { + [P in keyof BaseTawsEvents as IndexedEventType

]: BaseTawsEvents[P]; +}; + +export interface TawsDataEvents extends BaseTawsEvents, TawsIndexedEvents {} + +export class TawsPublisher extends SimVarPublisher { + /** + * Creates an AdcPublisher. + * @param bus The event bus to which to publish. + * @param pacer An optional pacer to use to control the rate of publishing. + */ + public constructor(bus: EventBus, pacer?: PublishPacer) { + const simvars = new Map>([ + [ + 'egpws_alert_discrete_word_1', + { name: 'L:A32NX_EGPWS_ALERT_#index#_DISCRETE_WORD_1', type: SimVarValueType.Number, indexed: true }, + ], + [ + 'egpws_alert_discrete_word_2', + { name: 'L:A32NX_EGPWS_ALERT_#index#_DISCRETE_WORD_2', type: SimVarValueType.Number, indexed: true }, + ], + ]); + super(simvars, bus, pacer); + } +} diff --git a/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/index.ts b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/index.ts new file mode 100644 index 00000000000..6b689ec0684 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/MsfsAvionicsCommon/providers/index.ts @@ -0,0 +1,3 @@ +export * from './FwcPublisher'; +export * from './TawsPublisher'; +export * from './RopRowOansPublisher'; diff --git a/fbw-common/src/systems/instruments/src/PFD/.eslintrc.js b/fbw-common/src/systems/instruments/src/PFD/.eslintrc.js new file mode 100644 index 00000000000..2b55b4be125 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/PFD/.eslintrc.js @@ -0,0 +1,12 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +'use strict'; + +module.exports = { + extends: '../../../../../../.eslintrc.js', + + // overrides airbnb, use sparingly + rules: { 'react/no-unknown-property': 'off', 'react/style-prop-object': 'off' }, +}; diff --git a/fbw-common/src/systems/instruments/src/PFD/AttitudeIndicatorWarnings.tsx b/fbw-common/src/systems/instruments/src/PFD/AttitudeIndicatorWarnings.tsx new file mode 100644 index 00000000000..68ffa7b3edc --- /dev/null +++ b/fbw-common/src/systems/instruments/src/PFD/AttitudeIndicatorWarnings.tsx @@ -0,0 +1,237 @@ +import { DisplayComponent, FSComponent, MappedSubject, Subject, VNode } from '@microsoft/msfs-sdk'; + +import { Arinc429RegisterSubject, Arinc429Word, ArincEventBus } from '@flybywiresim/fbw-sdk'; +import { RopRowOansSimVars, FwcDataEvents, TawsDataEvents } from '../MsfsAvionicsCommon/providers'; + +interface AttitudeIndicatorWarningsProps { + bus: ArincEventBus; + instrument: BaseInstrument; +} + +export class AttitudeIndicatorWarnings extends DisplayComponent { + private readonly warningGroupRef = FSComponent.createRef(); + + private readonly maxReverseActive = Subject.create(false); + + private readonly maxReverseMaxBrakingActive = Subject.create(false); + + private readonly ifWetRwyTooShortActive = Subject.create(false); + + private readonly rwyTooShortActive = Subject.create(false); + + private readonly rwyAheadActive = Subject.create(false); + + private readonly fwcWord126 = Arinc429RegisterSubject.createEmpty(); + + private readonly gpwsWord2 = Arinc429RegisterSubject.createEmpty(); + + // stall warning = stall bit && !on ground bit + private readonly stallActive = this.fwcWord126.map((v) => v.bitValueOr(17, false) && !v.bitValue(28)); + + // FIXME no source yet + private readonly stopRudderInputActive = Subject.create(false); + + private readonly windshearActive = this.gpwsWord2.map((w) => w.bitValueOr(15, false)); + + // FIXME no source yet + private readonly wsAheadCaution = Subject.create(false); + + // FIXME no source yet + private readonly wsAheadWarning = Subject.create(false); + + onAfterRender(node: VNode): void { + super.onAfterRender(node); + + const sub = this.props.bus.getSubscriber(); + + sub + .on('rowRopWord1Raw') + .whenChanged() + .handle((raw) => { + const ar = new Arinc429Word(raw); + + this.maxReverseActive.set(ar.getBitValueOr(12, false)); + this.maxReverseMaxBrakingActive.set(ar.getBitValueOr(13, false)); + this.ifWetRwyTooShortActive.set(ar.getBitValueOr(14, false)); + this.rwyTooShortActive.set(ar.getBitValueOr(15, false)); + }); + + sub + .on('oansWord1Raw') + .whenChanged() + .handle((raw) => { + const ar = new Arinc429Word(raw); + this.rwyAheadActive.set(ar.getBitValueOr(11, false)); + }); + + sub + .on('fwc_discrete_word_126_1') + .whenChanged() + .handle((v) => { + this.fwcWord126.setWord(v); + }); + + sub + .on('egpws_alert_discrete_word_2_1') + .whenChanged() + .handle((v) => { + this.gpwsWord2.setWord(v); + }); + } + + render(): VNode { + return ( + + + (maxReverse || maxRmB) && !wetTooShort && !tooShort && !stall, + this.maxReverseActive, + this.maxReverseMaxBrakingActive, + this.ifWetRwyTooShortActive, + this.rwyTooShortActive, + this.stallActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + MAX REVERSE + + + maxBraking && !wetTooShort && !tooShort && !stall && !stopRudder, + this.maxReverseMaxBrakingActive, + this.ifWetRwyTooShortActive, + this.rwyTooShortActive, + this.stallActive, + this.stopRudderInputActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + MAX BRAKING + + ifWetTooShort && !tooShort && !stall, + this.ifWetRwyTooShortActive, + this.rwyTooShortActive, + this.stallActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + IF WET:RWY TOO SHORT + + rwyTooShort && !stall, + this.rwyTooShortActive, + this.stallActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + RWY TOO SHORT + + (it ? 'block' : 'none')) }} + > + STALL + {'\xa0\xa0\xa0'} + STALL + + stopRudder && !wetTooShort && !tooShort && !stall, + this.stopRudderInputActive, + this.ifWetRwyTooShortActive, + this.rwyTooShortActive, + this.stallActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + STOP RUDDER INPUT + + windshear && !maxReverse && !maxBraking, + this.windshearActive, + this.maxReverseActive, + this.maxReverseMaxBrakingActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + WINDSHEAR + + + wsCaution && !wsWarning && !maxReverse && !maxBraking && !windshear && !stall, + this.wsAheadCaution, + this.wsAheadWarning, + this.maxReverseActive, + this.maxReverseMaxBrakingActive, + this.windshearActive, + this.stallActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + W/S AHEAD + + + wsAheadWarning && !maxReverse && !maxBraking && !windshear && !stall, + this.wsAheadWarning, + this.maxReverseActive, + this.maxReverseMaxBrakingActive, + this.windshearActive, + this.stallActive, + ).map((it) => (it ? 'block' : 'none')), + }} + > + W/S AHEAD + + (it ? 'block' : 'none')) }}> + + + RWY AHEAD + + + + ); + } +} diff --git a/fbw-common/src/systems/instruments/src/PFD/animations.scss b/fbw-common/src/systems/instruments/src/PFD/animations.scss new file mode 100644 index 00000000000..e0f1df54129 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/PFD/animations.scss @@ -0,0 +1,77 @@ +@keyframes blinking { + 0% {opacity: 0;} + 50% {opacity: 0;} + 51% {opacity: 1;} + 100% {opacity: 1;} +} + +@mixin GenericPulsingStroke($color, $name) { + @keyframes #{$name} { + 0% {stroke: scale-color($color, $lightness: -50%);} + 50% {stroke: scale-color($color, $lightness: -50%);} + 51% {stroke: scale-color($color, $lightness: 50%);} + 100% {stroke: scale-color($color, $lightness: 50%);} + } + animation-name: $name; +} +@mixin GenericPulsingFill($color, $name) { + @keyframes #{$name} { + 0% {fill: scale-color($color, $lightness: -30%);} + 50% {fill: scale-color($color, $lightness: -30%);} + 51% {fill: scale-color($color, $lightness: 30%);} + 100% {fill: scale-color($color, $lightness: 30%);} + } + animation-name: $name; +} + +@keyframes OuterMarkerAnim { + 0% {opacity: 0;} + 33% {opacity: 0;} + 34% {opacity: 1;} + 100% {opacity: 1;} +} + +@keyframes MiddleMarkerAnim { + 0% {opacity: 0} + 10% {opacity: 0} + 11% {opacity: 1} + 27% {opacity: 1} + 28% {opacity: 0} + 44% {opacity: 0} + 45% {opacity: 1} + 100% {opacity: 1} +} + +.BlinkInfinite { + animation-name: blinking; + animation-duration: 1s; + animation-iteration-count: infinite; +} + +.Blink9Seconds { + animation-name: blinking; + animation-duration: 1s; + animation-iteration-count: 9; +} + +.OuterMarkerBlink { + animation-name: OuterMarkerAnim; + animation-duration: 460ms; + animation-iteration-count: infinite; +} +.MiddleMarkerBlink { + animation-name: MiddleMarkerAnim; + animation-duration: 730ms; + animation-iteration-count: infinite; +} +.InnerMarkerBlink { + animation-name: blinking; + animation-duration: 200ms; + animation-iteration-count: infinite; +} + +.RwyAheadAnimation { + @include GenericPulsingStroke($display-yellow, pulse-yellow-stroke); + animation-duration: 1s; + animation-iteration-count: infinite; +} diff --git a/fbw-common/src/systems/instruments/src/PFD/index.ts b/fbw-common/src/systems/instruments/src/PFD/index.ts new file mode 100644 index 00000000000..fdeaf397e1d --- /dev/null +++ b/fbw-common/src/systems/instruments/src/PFD/index.ts @@ -0,0 +1 @@ +export * from './AttitudeIndicatorWarnings'; diff --git a/fbw-common/src/systems/instruments/src/PFD/style.scss b/fbw-common/src/systems/instruments/src/PFD/style.scss new file mode 100644 index 00000000000..778aa5003d5 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/PFD/style.scss @@ -0,0 +1,283 @@ +@import "../MsfsAvionicsCommon/definitions.scss"; +@import "./animations.scss"; + +@font-face { + font-family: "Ecam"; + //noinspection CssUnknownTarget + src: url("/Fonts/ECAMFontRegular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +.pfd-svg { + position: absolute; + width: 768px; + height: 1024px; + background: $display-background; + font-family: "Ecam", monospace !important; +} + +.PulseCyanFill { + @include GenericPulsingFill($display-cyan, pulse-cyan-fill); + animation-duration: 1s; + animation-iteration-count: infinite; +} + +.PulseAmber9Seconds { + @include GenericPulsingFill($display-amber, pulse-amber-fill); + animation-duration: 1s; + animation-iteration-count: 9; +} + +.BarAmber { + fill: $display-amber; + stroke: $display-amber; + stroke-width: 0.13px; +} +.BarRed { + fill: $display-red; + stroke: $display-red; + stroke-width: 0.11px; +} + +.SmallStroke { + stroke-width: 0.10mm; + stroke-linecap: round; +} + +.NormalStroke { + stroke-width: 0.16mm; + stroke-linecap: round; +} + +.ThickStroke { + stroke-width: 0.22mm; + stroke-linecap: round; +} + +.HugeStroke { + stroke-width: 0.32mm; + stroke-linecap: round; +} + +.SmallOutline { + stroke-width: 0.09mm; + stroke: $display-background !important; + paint-order: markers stroke fill; +} +.NormalOutline { + stroke-width: 0.21mm; + stroke: $display-background; + fill: none; + stroke-linecap: round; +} +.ThickOutline { + stroke-width: 0.29mm; + stroke: $display-background; + fill: none; + stroke-linecap: round; +} +.HugeOutline { + stroke-width: 0.39mm; + stroke: $display-background; + fill: none; + stroke-linecap: round; +} + +.CornerRound { + stroke-linejoin: round; +} + +.TextOutline { + paint-order: stroke fill markers; + + stroke-width: 0.05mm; + stroke: $display-background !important; +} + +.FontLargest { + font-size: 7px; +} + +.FontLarge { + font-size: 6.5px; +} + +.FontMedium { + font-size: 6px; +} + +.FontIntermediate { + font-size: 5.5px; +} + +.FontMediumSmaller { + font-size: 5.4px; +} + +.FontSmall { + font-size: 5px; +} + +.FontSmallest { + font-size: 4.5px; +} + +.FontTiny { + font-size: 4px; +} + +.StartAlign { + text-align: start; + text-anchor: start; +} +.MiddleAlign { + text-align: center; + text-anchor: middle; +} +.EndAlign { + text-align: end; + text-anchor: end; +} + +.Magenta { + fill: none; + stroke: $display-magenta; +} +text.Magenta { + fill: $display-magenta; + stroke: none; +} + +.Cyan { + fill: none; + stroke: $display-cyan; +} +.Cyan.Fill.Stroke { + fill: $display-cyan; + stroke: $display-cyan; +} +text.Cyan { + fill: $display-cyan; + stroke: none; +} +tspan.Cyan { + fill: $display-cyan; + stroke: none; +} + +.None { + fill: none; + stroke: none; +} + +.White { + fill: none; + stroke: $display-white; +} +.White.Fill { + fill: $display-white; + stroke: none; +} +.White.Fill.Stroke { + fill: $display-white; + stroke: $display-white; +} +text.White { + fill: $display-white; + stroke: none; +} + +.Green { + stroke: $display-green; + fill: none; +} + +.Green.Fill { + fill: $display-green; + stroke: none; +} + +text.Green { + fill: $display-green; + stroke: none; +} + +.Amber { + stroke: $display-amber; + fill: none; +} +text.Amber { + fill: $display-amber; + stroke: none; +} + +.Yellow { + stroke: $display-yellow; + fill: none; +} +.Yellow.Fill { + fill: $display-yellow; + stroke: none; +} +text.Yellow { + fill: $display-yellow; + stroke: none; +} + +.Red { + stroke: $display-red; + fill: none; +} +.Red.Fill { + fill: $display-red; + stroke: none; +} +text.Red { + fill: $display-red; + stroke: none; +} + +.Grey { + stroke: $display-grey; + fill: none; +} + +.EarthFill { + fill: #5D2518; +} +.SkyFill { + fill: #0073DE; +} +.BlackFill { + fill: $display-background; +} + +.TapeBackground { + fill: $display-grey; + stroke: none; +} + +.BackgroundFill { + fill: $display-background; + stroke: none; + fill-rule: evenodd; +} + +.ModeChangedPath { + visibility: visible !important; +} + +.HiddenElement { + display: none; +} + +/* .BacklightBleed { + width: 100%; + height: 100%; + position: absolute; + z-index: 998; + opacity: 0.75; + background-color: rgba(0, 0, 255, 0.025); + box-shadow: inset 0px 0px 30px 10px rgba(0, 75, 255, 0.04); +} */ diff --git a/fbw-common/src/systems/instruments/src/PFD/tsconfig.json b/fbw-common/src/systems/instruments/src/PFD/tsconfig.json new file mode 100644 index 00000000000..9a4fe03f55c --- /dev/null +++ b/fbw-common/src/systems/instruments/src/PFD/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.json", + + "compilerOptions": { + "incremental": false /* Enables incremental builds */, + "target": "es2017" /* Specifies the ES2017 target, compatible with Coherent GT */, + "module": "es2015" /* Ensures that modules are at least es2015 */, + "strict": false /* Enables strict type checking, highly recommended but optional */, + "esModuleInterop": true /* Emits additional JS to work with CommonJS modules */, + "skipLibCheck": true /* Skip type checking on library .d.ts files */, + "forceConsistentCasingInFileNames": true /* Ensures correct import casing */, + "moduleResolution": "node" /* Enables compatibility with MSFS SDK bare global imports */, + "jsxFactory": "FSComponent.buildComponent" /* Required for FSComponent framework JSX */, + "jsxFragmentFactory": "FSComponent.Fragment" /* Required for FSComponent framework JSX */, + "jsx": "react", /* Required for FSComponent framework JSX */ + "paths": { + "@typings/*": ["../../../fbw-common/src/typings/*"], + "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index-no-react.ts"] + } + } +} diff --git a/fbw-common/src/systems/shared/src/Arinc429RegisterSubject.ts b/fbw-common/src/systems/shared/src/Arinc429RegisterSubject.ts index e9d49b61584..a94b17c4a05 100644 --- a/fbw-common/src/systems/shared/src/Arinc429RegisterSubject.ts +++ b/fbw-common/src/systems/shared/src/Arinc429RegisterSubject.ts @@ -28,4 +28,28 @@ export class Arinc429RegisterSubject extends Subject { this.notify(); } } + + setValue(value: typeof this.value.value): void { + const oldValue = this.value.value; + this.value.setValue(value); + if (oldValue !== this.value.value) { + this.notify(); + } + } + + setBitValue(bit: number, value: boolean): void { + const oldValue = this.value.value; + this.value.setBitValue(bit, value); + if (oldValue !== this.value.value) { + this.notify(); + } + } + + setSsm(ssm: typeof this.value.ssm): void { + const oldSsm = this.value.ssm; + this.value.setSsm(ssm); + if (oldSsm !== this.value.ssm) { + this.notify(); + } + } } diff --git a/fbw-common/src/systems/shared/src/arinc429.ts b/fbw-common/src/systems/shared/src/arinc429.ts index 88ba0dce131..02ff5de5c6e 100644 --- a/fbw-common/src/systems/shared/src/arinc429.ts +++ b/fbw-common/src/systems/shared/src/arinc429.ts @@ -114,6 +114,22 @@ export class Arinc429Register implements Arinc429WordData { this.value = this.f32View[0]; } + setValue(value: typeof this.value): void { + this.value = value; + } + + setBitValue(bit: number, value: boolean): void { + if (value) { + this.value |= 1 << (bit - 1); + } else { + this.value &= ~(1 << (bit - 1)); + } + } + + setSsm(ssm: typeof this.ssm): void { + this.ssm = ssm; + } + setFromSimVar(name: string): void { this.set(SimVar.GetSimVarValue(name, 'number')); } diff --git a/fbw-common/src/systems/tsconfig.json b/fbw-common/src/systems/tsconfig.json index 43b7348c77f..1ceb0da7319 100644 --- a/fbw-common/src/systems/tsconfig.json +++ b/fbw-common/src/systems/tsconfig.json @@ -12,10 +12,6 @@ "jsx": "react", "skipLibCheck": true, "paths": { -// "@datalink/aoc": ["datalink/aoc/src/index.ts"], -// "@datalink/atc": ["datalink/atc/src/index.ts"], -// "@datalink/common": ["datalink/common/src/index.ts"], -// "@datalink/router": ["datalink/router/src/index.ts"], "@fmgc/*": ["../../../fbw-a32nx/src/systems/fmgc/src/*"], "@shared/*": ["../../../fbw-a32nx/src/systems/shared/src/*"], "@simbridge/*": ["../../../fbw-a32nx/src/systems/simbridge-client/src/*"],