diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 38e640d4c40..026d79cb0c9 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -52,6 +52,7 @@ 1. [SD] Connect SD BLEED page APU bleed pressure indication to correct ADRs - @BlueberryKing (BlueberryKing) 1. [FMS] Use station declination when appropriate for fix info and place/bearing radials - @tracernz (Mike) 1. [MCDU] Fixed the FMGC annunciator light not illuminating - @tracernz (Mike) +1. [COND] Add Air Conditioning systems failures - @mjuhe (Miquel Juhe) ## 0.10.0 diff --git a/fbw-a32nx/docs/a320-simvars.md b/fbw-a32nx/docs/a320-simvars.md index 2a37660ce71..fb66673b25d 100644 --- a/fbw-a32nx/docs/a320-simvars.md +++ b/fbw-a32nx/docs/a320-simvars.md @@ -2628,6 +2628,59 @@ In the variables below, {number} should be replaced with one item in the set: { ## Air Conditioning / Pressurisation / Ventilation +- A32NX_COND_ACSC_{number}_DISCRETE_WORD_1 + - Number 1 or 2 + - Discrete Data word 1 of the ACSC bus output (label 060) + - Arinc429 + - | Bit | Description | + |:---:|:----------------------------------------------------:| + | 11 | Duct overheat F/D warning | + | 12 | Duct overheat FWD warning | + | 13 | Duct overheat AFT warning | + | 14 | Not used | + | 15 | Not used | + | 16 | Not used | + | 17 | Spare | + | 18 | Trim air pressure high | + | 19 | ACSC Lane 1 Active | + | 20 | TAPRV status - close | + | 21 | ACSC Lane 1 INOP | + | 22 | ACSC Lane 2 INOP | + | 23 | Hot air switch position on | + | 24 | G + T fan off/fault | + | 25 | Recirc fan LH fault/OVHT | + | 26 | Recirc fan RH fault/OVHT | + | 27 | TAPRV disagree | + | 28 | Trim air system fault | + | 29 | ACSC Installed | + +- A32NX_COND_ACSC_{number}_DISCRETE_WORD_2 + - Number 1 or 2 + - Discrete Data word 2 of the ACSC bus output (label 061) + - Bits with * not yet implemented + - Arinc429 + - | Bit | Description | + |:---:|:----------------------------------------------------:| + | 11 | Spare | + | 12 | *K1 half wing anti-ice on | + | 13 | *K2 full wing anti-ice on | + | 14 | *K3 nacelle anti-ice on | + | 15 | *K4 air cond with two packs on | + | 16 | *K5 air cond with one pack on | + | 17 | *K6 air cond with two packs and one engine on | + | 18 | Trim valve F/D inop | + | 19 | Trim valve FWD inop | + | 20 | Trim valve AFT inop | + | 21 | Not used | + | 22 | Not used | + | 23 | *FCV status (Both pakcs off) | + | 24 | *One pack operation | + | 25 | *FCV status (Both pakcs on) | + | 26 | Spare | + | 27 | *Nacelle anti-ice eng 2 open | + | 28 | *Nacelle anti-ice eng 1 open | + | 29 | Spare | + - A32NX_COND_{id}_TEMP - Degree Celsius - Temperature as measured in each of the cabin zones and cockpit @@ -2660,14 +2713,6 @@ In the variables below, {number} should be replaced with one item in the set: { - FWD - AFT -- A32NX_HOT_AIR_VALVE_IS_ENABLED - - Bool - - True if the trim air system is enabled (pushbutton in auto and power supplied to system) - -- A32NX_HOT_AIR_VALVE_IS_OPEN - - Bool - - True if the trim air system is enabled and the hot air valve is open - - A32NX_OVHD_COND_{id}_SELECTOR_KNOB - Percentage - Percent rotation of the overhead temperature selectors for each of the cabin zones @@ -2759,13 +2804,6 @@ In the variables below, {number} should be replaced with one item in the set: { - Bool - True if CAB FANS pushbutton is in the on position (no white light) -- A32NX_PACKS_{number}_IS_SUPPLYING - - Bool - - True if the corresponding pack is on and supplying air to the cabin - - {number} - - 1 - - 2 - ## Pneumatic - A32NX_PNEU_ENG_{number}_IP_PRESSURE: diff --git a/fbw-a32nx/src/systems/failures/src/a320.ts b/fbw-a32nx/src/systems/failures/src/a320.ts index b8f282749fe..472b38d8201 100644 --- a/fbw-a32nx/src/systems/failures/src/a320.ts +++ b/fbw-a32nx/src/systems/failures/src/a320.ts @@ -1,6 +1,22 @@ // One can rightfully argue that this constant shouldn't be located in @flybywiresim/failures. // Once we create an A320 specific package, such as @flybywiresim/a320, we can move it there. export const A320Failure = Object.freeze({ + Acsc1Lane1: 21000, + Acsc1Lane2: 21001, + Acsc2Lane1: 21002, + Acsc2Lane2: 21003, + HotAir: 21004, + TrimAirHighPressure: 21005, + CkptTrimAirFailure: 21006, + FwdTrimAirFailure: 21007, + AftTrimAirFailure: 21008, + CkptDuctOvht: 21009, + FwdDuctOvht: 21010, + AftDuctOvht: 21011, + CabinFan1Failure: 21012, + CabinFan2Failure: 21013, + LabGalleyFan: 21014, + Fac1Failure: 22000, Fac2Failure: 22001, diff --git a/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx b/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx index 3ea7a0b898a..35dd9f017c5 100644 --- a/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx +++ b/fbw-a32nx/src/systems/instruments/src/Common/EWDMessages.tsx @@ -122,8 +122,49 @@ const EWDMessages = { '213122114': '\x1b<5m MAX FL.....100/MEA-MORA', '213122115': '\x1b<7m .IF CAB ALT>14000FT:', '213122116': '\x1b<5m -PAX OXY MASKS...MAN ON', + '216120601': '\x1b<4m\x1b4mAIR\x1bm PACK 1+2 FAULT', + '216120602': '\x1b<5m -PACK 1.............OFF', + '216120603': '\x1b<5m -PACK 2.............OFF', + '216120604': '\x1b<5m -DES TO FL 100/MEA-MORA', + '216120605': '\x1b<7m .WHEN DIFF PR <1 PSI', + '216120606': '\x1b<7m AND FL BELOW 100:', + '216120607': '\x1b<5m -RAM AIR.............ON', + '216120608': '\x1b<5m MAX FL.....100/MEA-MORA', + '216120201': '\x1b<4m\x1b4mAIR\x1bm PACK 1 FAULT', + '216120202': '\x1b<5m -PACK 1.............OFF', + '216120301': '\x1b<4m\x1b4mAIR\x1bm PACK 2 FAULT', + '216120302': '\x1b<5m -PACK 2.............OFF', '216120701': '\x1b<4m\x1b4mAIR\x1bm PACK 1 OFF', '216120801': '\x1b<4m\x1b4mAIR\x1bm PACK 2 OFF', + '216129101': '\x1b<4m\x1b4mAIR\x1bm COND CTL 1-A FAULT', + '216129401': '\x1b<4m\x1b4mAIR\x1bm COND CTL 2-A FAULT', + '216129701': '\x1b<4m\x1b4mAIR\x1bm COND CTL 1-B FAULT', + '216129801': '\x1b<4m\x1b4mAIR\x1bm COND CTL 2-B FAULT', + '216321001': '\x1b<4m\x1b4mCOND\x1bm CKPT DUCT OVHT', + '216321002': '\x1b<7m .WHEN DUCT TEMP<70 DEG C:', + '216321003': '\x1b<7m .WHEN DUCT TEMP<158 DEG F:', + '216321004': '\x1b<5m -HOT AIR....OFF THEN ON', + '216321101': '\x1b<4m\x1b4mCOND\x1bm FWD CAB DUCT OVHT', + '216321102': '\x1b<7m .WHEN DUCT TEMP<70 DEG C:', + '216321103': '\x1b<7m .WHEN DUCT TEMP<158 DEG F:', + '216321104': '\x1b<5m -HOT AIR....OFF THEN ON', + '216321201': '\x1b<4m\x1b4mCOND\x1bm AFT CAB DUCT OVHT', + '216321202': '\x1b<7m .WHEN DUCT TEMP<70 DEG C:', + '216321203': '\x1b<7m .WHEN DUCT TEMP<158 DEG F:', + '216321204': '\x1b<5m -HOT AIR....OFF THEN ON', + '216321801': '\x1b<4m\x1b4mCOND\x1bm L+R CAB FAN FAULT', + '216321802': '\x1b<5m -PACK FLOW...........HI', + '216326001': '\x1b<4m\x1b4mCOND\x1bm LAV+GALLEY FAN FAULT', + '216329001': '\x1b<4m\x1b4mCOND\x1bm HOT AIR FAULT', + '216329002': '\x1b<5m -HOT AIR............OFF', + '216329003': '\x1b<7m .IF HOT AIR STILL OPEN:', + '216329004': '\x1b<5m -PACK 1.............OFF', + '216329005': '\x1b<5m -PACK 2.............OFF', + '216330501': '\x1b<4m\x1b4mCOND\x1bm TRIM AIR SYS FAULT', + '216330502': '\x1b<4m -CPKT TRIM VALVE', + '216330503': '\x1b<4m -FWD CAB TRIM VALVE', + '216330504': '\x1b<4m -AFT CAB TRIM VALVE', + '216330505': '\x1b<4m -TRIM AIR HI PR', '221070001': '\x1b<4m\x1b4mT.O\x1bm SPEEDS TOO LOW', '221070002': '\x1b<5m -TOW AND T.O DATA.CHECK', '221071001': '\x1b<4m\x1b4mT.O\x1bm V1/VR/V2 DISAGREE', diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx index 000ad108cbf..6dfdf4c020d 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx @@ -15,6 +15,22 @@ interface FailuresOrchestratorContext { } const createOrchestrator = () => new FailuresOrchestrator('A32NX', [ + [21, A320Failure.Acsc1Lane1, 'ACSC 1 Lane 1'], + [21, A320Failure.Acsc1Lane2, 'ACSC 1 Lane 2'], + [21, A320Failure.Acsc2Lane1, 'ACSC 2 Lane 1'], + [21, A320Failure.Acsc2Lane2, 'ACSC 2 Lane 2'], + [21, A320Failure.HotAir, 'Trim Air Pressure Regulating Valve'], + [21, A320Failure.TrimAirHighPressure, 'Trim Air System High Pressure'], + [21, A320Failure.CkptTrimAirFailure, 'Cockpit Trim Air Valve'], + [21, A320Failure.FwdTrimAirFailure, 'Forward Zone Trim Air Valve'], + [21, A320Failure.AftTrimAirFailure, 'Aft Zone Trim Air Valve'], + [21, A320Failure.CkptDuctOvht, 'Cockpit Duct Overheat'], + [21, A320Failure.FwdDuctOvht, 'Forward Zone Duct Overheat'], + [21, A320Failure.AftDuctOvht, 'Aft Zone Duct Overheat'], + [21, A320Failure.CabinFan1Failure, 'Cabin Fan 1'], + [21, A320Failure.CabinFan2Failure, 'Cabin Fan 2'], + [21, A320Failure.LabGalleyFan, 'Extraction Fan of lavatory and galley'], + [22, A320Failure.Fac1Failure, 'FAC 1'], [22, A320Failure.Fac2Failure, 'FAC 2'], diff --git a/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts b/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts index 6558c58a4dc..e086a14a281 100644 --- a/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts +++ b/fbw-a32nx/src/systems/instruments/src/EWD/PseudoFWC.ts @@ -93,7 +93,15 @@ export class PseudoFWC { this.fireActive, ); - /* PRESSURIZATION */ + /* 21 - AIR CONDITIONING AND PRESSURIZATION */ + + private readonly acsc1DiscreteWord1 = Arinc429Register.empty(); + + private readonly acsc1DiscreteWord2 = Arinc429Register.empty(); + + private readonly acsc2DiscreteWord1 = Arinc429Register.empty(); + + private readonly acsc2DiscreteWord2 = Arinc429Register.empty(); private readonly apuBleedValveOpen = Subject.create(false); @@ -105,8 +113,58 @@ export class PseudoFWC { private readonly cabAltSetResetState2 = Subject.create(false); + private readonly cabFanHasFault1 = Subject.create(false); + + private readonly cabFanHasFault2 = Subject.create(false); + private readonly excessPressure = Subject.create(false); + private readonly acsc1Lane1Fault = Subject.create(false); + + private readonly acsc1Lane2Fault = Subject.create(false); + + private readonly acsc2Lane1Fault = Subject.create(false); + + private readonly acsc2Lane2Fault = Subject.create(false); + + private readonly acsc1Fault = Subject.create(false); + + private readonly acsc2Fault = Subject.create(false); + + private readonly pack1And2Fault = Subject.create(false); + + private readonly ramAirOn = Subject.create(false); + + private readonly hotAirDisagrees = Subject.create(false); + + private readonly hotAirOpen = Subject.create(false); + + private readonly hotAirPbOn = Subject.create(false); + + private readonly trimAirFault = Subject.create(false); + + private readonly ckptTrimFault = Subject.create(false); + + private readonly fwdTrimFault = Subject.create(false); + + private readonly aftTrimFault = Subject.create(false); + + private readonly trimAirHighPressure = Subject.create(false); + + private readonly ckptDuctOvht = Subject.create(false); + + private readonly fwdDuctOvht = Subject.create(false); + + private readonly aftDuctOvht = Subject.create(false); + + private readonly anyDuctOvht = Subject.create(false); + + private readonly lavGalleyFanFault = Subject.create(false); + + private readonly pack1On = Subject.create(false); + + private readonly pack2On = Subject.create(false); + private readonly packOffBleedAvailable1 = new NXLogicConfirmNode(5, false); private readonly packOffBleedAvailable2 = new NXLogicConfirmNode(5, false); @@ -1039,7 +1097,46 @@ export class PseudoFWC { 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')); - /* AIR CONDITIONING */ + /* 21 - AIR CONDITIONING AND PRESSURIZATION */ + + // FIXME: Should take both words + this.acsc1DiscreteWord1.setFromSimVar('L:A32NX_COND_ACSC_1_DISCRETE_WORD_1'); + this.acsc1DiscreteWord2.setFromSimVar('L:A32NX_COND_ACSC_1_DISCRETE_WORD_2'); + this.acsc2DiscreteWord1.setFromSimVar('L:A32NX_COND_ACSC_2_DISCRETE_WORD_1'); + this.acsc2DiscreteWord2.setFromSimVar('L:A32NX_COND_ACSC_2_DISCRETE_WORD_2'); + + this.acsc1Lane1Fault.set(this.acsc1DiscreteWord1.bitValueOr(21, false)); + this.acsc1Lane2Fault.set(this.acsc1DiscreteWord1.bitValueOr(22, false)); + this.acsc2Lane1Fault.set(this.acsc2DiscreteWord1.bitValueOr(21, false)); + this.acsc2Lane2Fault.set(this.acsc2DiscreteWord1.bitValueOr(22, false)); + + const acsc1FT = this.acsc1DiscreteWord1.isFailureWarning(); + const acsc2FT = this.acsc2DiscreteWord1.isFailureWarning(); + this.acsc1Fault.set(acsc1FT && !acsc2FT); + this.acsc2Fault.set(!acsc1FT && acsc2FT); + const acscBothFault = acsc1FT && acsc2FT; + + this.ramAirOn.set(SimVar.GetSimVarValue('L:A32NX_AIRCOND_RAMAIR_TOGGLE', 'bool')); + + this.cabFanHasFault1.set(this.acsc1DiscreteWord1.bitValueOr(25, false) || this.acsc2DiscreteWord1.bitValueOr(25, false)); + this.cabFanHasFault2.set(this.acsc1DiscreteWord1.bitValueOr(26, false) || this.acsc2DiscreteWord1.bitValueOr(26, false)); + + this.hotAirDisagrees.set(this.acsc1DiscreteWord1.bitValueOr(27, false) && this.acsc2DiscreteWord1.bitValueOr(27, false)); + this.hotAirOpen.set(!this.acsc1DiscreteWord1.bitValueOr(20, false) || !this.acsc2DiscreteWord1.bitValueOr(20, false)); + this.hotAirPbOn.set(this.acsc1DiscreteWord1.bitValueOr(23, false) || this.acsc2DiscreteWord1.bitValueOr(23, false)); + + this.trimAirFault.set(this.acsc1DiscreteWord1.bitValueOr(28, false) || this.acsc2DiscreteWord1.bitValueOr(28, false)); + this.ckptTrimFault.set(this.acsc1DiscreteWord2.bitValueOr(18, false) || this.acsc2DiscreteWord2.bitValueOr(18, false)); + this.fwdTrimFault.set(this.acsc1DiscreteWord2.bitValueOr(19, false) || this.acsc2DiscreteWord2.bitValueOr(19, false)); + this.aftTrimFault.set(this.acsc1DiscreteWord2.bitValueOr(20, false) || this.acsc2DiscreteWord2.bitValueOr(20, false)); + this.trimAirHighPressure.set(this.acsc1DiscreteWord1.bitValueOr(18, false) || this.acsc2DiscreteWord1.bitValueOr(18, false)); + + this.ckptDuctOvht.set(this.acsc1DiscreteWord1.bitValueOr(11, false) || this.acsc2DiscreteWord1.bitValueOr(11, false)); + this.fwdDuctOvht.set(this.acsc1DiscreteWord1.bitValueOr(12, false) || this.acsc2DiscreteWord1.bitValueOr(12, false)); + this.aftDuctOvht.set(this.acsc1DiscreteWord1.bitValueOr(13, false) || this.acsc2DiscreteWord1.bitValueOr(13, false)); + this.anyDuctOvht.set(this.ckptDuctOvht.get() || this.fwdDuctOvht.get() || this.aftDuctOvht.get()); + + this.lavGalleyFanFault.set(this.acsc1DiscreteWord1.bitValueOr(24, false) || this.acsc2DiscreteWord1.bitValueOr(24, false)); const crossbleedFullyClosed = SimVar.GetSimVarValue('L:A32NX_PNEU_XBLEED_VALVE_FULLY_CLOSED', 'bool'); const eng1Bleed = SimVar.GetSimVarValue('L:A32NX_OVHD_PNEU_ENG_1_BLEED_PB_IS_AUTO', 'bool'); @@ -1048,8 +1145,8 @@ export class PseudoFWC { const eng2BleedPbFault = SimVar.GetSimVarValue('L:A32NX_OVHD_PNEU_ENG_2_BLEED_PB_HAS_FAULT', 'bool'); const pack1Fault = SimVar.GetSimVarValue('L:A32NX_OVHD_COND_PACK_1_PB_HAS_FAULT', 'bool'); const pack2Fault = SimVar.GetSimVarValue('L:A32NX_OVHD_COND_PACK_2_PB_HAS_FAULT', 'bool'); - const pack1On = SimVar.GetSimVarValue('L:A32NX_OVHD_COND_PACK_1_PB_IS_ON', 'bool'); - const pack2On = SimVar.GetSimVarValue('L:A32NX_OVHD_COND_PACK_2_PB_IS_ON', 'bool'); + this.pack1On.set(SimVar.GetSimVarValue('L:A32NX_OVHD_COND_PACK_1_PB_IS_ON', 'bool')); + this.pack2On.set(SimVar.GetSimVarValue('L:A32NX_OVHD_COND_PACK_2_PB_IS_ON', 'bool')); this.excessPressure.set(SimVar.GetSimVarValue('L:A32NX_PRESS_EXCESS_CAB_ALT', 'bool')); this.cabAltSetResetState1.set( @@ -1060,8 +1157,9 @@ export class PseudoFWC { ); this.packOffBleedAvailable1.write((eng1Bleed === 1 && !eng1BleedPbFault) || !crossbleedFullyClosed, deltaTime); this.packOffBleedAvailable2.write((eng2Bleed === 1 && !eng2BleedPbFault) || !crossbleedFullyClosed, deltaTime); - this.packOffNotFailed1Status.set(this.packOffNotFailed1.write(!pack1On && !pack1Fault && this.packOffBleedAvailable1.read() && this.fwcFlightPhase.get() === 6, deltaTime)); - this.packOffNotFailed2Status.set(this.packOffNotFailed2.write(!pack2On && !pack2Fault && this.packOffBleedAvailable2.read() && this.fwcFlightPhase.get() === 6, deltaTime)); + this.packOffNotFailed1Status.set(this.packOffNotFailed1.write(!this.pack1On.get() && !pack1Fault && this.packOffBleedAvailable1.read() && this.fwcFlightPhase.get() === 6, deltaTime)); + this.packOffNotFailed2Status.set(this.packOffNotFailed2.write(!this.pack2On.get() && !pack2Fault && this.packOffBleedAvailable2.read() && this.fwcFlightPhase.get() === 6, deltaTime)); + this.pack1And2Fault.set(acscBothFault || (this.packOffNotFailed1Status.get() && this.acsc2Fault.get()) || (this.packOffNotFailed2Status.get() && this.acsc1Fault.get())); /* OTHER STUFF */ @@ -2169,6 +2267,191 @@ export class PseudoFWC { sysPage: 2, side: 'LEFT', }, + 2161206: { // PACK 1+2 FAULT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.pack1And2Fault, + whichCodeToReturn: () => [ + 0, + this.pack1On.get() ? 1 : null, + this.pack2On.get() ? 2 : null, + !this.aircraftOnGround.get() && !this.ramAirOn.get() ? 3 : null, + !this.aircraftOnGround.get() && !this.ramAirOn.get() ? 4 : null, + !this.aircraftOnGround.get() && !this.ramAirOn.get() ? 5 : null, + !this.aircraftOnGround.get() && !this.ramAirOn.get() ? 6 : null, + !this.aircraftOnGround.get() ? 7 : null, + ], + codesToReturn: ['216120601', '216120602', '216120603', '216120604', '216120605', '216120606', '216120607', '216120608'], + memoInhibit: () => false, + failure: 2, + sysPage: 1, + side: 'LEFT', + }, + 2161202: { // PACK 1 FAULT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.acsc1Fault, + whichCodeToReturn: () => [ + 0, + this.pack1On.get() ? 1 : null, + ], + codesToReturn: ['216120201', '216120202'], + memoInhibit: () => false, + failure: 2, + sysPage: 1, + side: 'LEFT', + }, + 2161203: { // PACK 2 FAULT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.acsc2Fault, + whichCodeToReturn: () => [ + 0, + this.pack2On.get() ? 1 : null, + ], + codesToReturn: ['216120301', '216120302'], + memoInhibit: () => false, + failure: 2, + sysPage: 1, + side: 'LEFT', + }, + 2161207: { // PACK 1 ABNORMALLY OFF + flightPhaseInhib: [1, 2, 3, 4, 5, 7, 8, 9, 10], + simVarIsActive: this.packOffNotFailed1Status, + whichCodeToReturn: () => [0], + codesToReturn: ['216120701'], + memoInhibit: () => false, + failure: 2, + sysPage: 1, + side: 'LEFT', + }, + 2161208: { // PACK 2 ABNORMALLY OFF + flightPhaseInhib: [1, 2, 3, 4, 5, 7, 8, 9, 10], + simVarIsActive: this.packOffNotFailed2Status, + whichCodeToReturn: () => [0], + codesToReturn: ['216120801'], + memoInhibit: () => false, + failure: 2, + sysPage: 1, + side: 'LEFT', + }, + 2161291: { // COND CTL 1-A FAULT + flightPhaseInhib: [2, 3, 4, 5, 6, 7, 8, 9], + simVarIsActive: MappedSubject.create(([acsc1Lane1Fault, acsc1Lane2Fault]) => acsc1Lane1Fault && !acsc1Lane2Fault, this.acsc1Lane1Fault, this.acsc1Lane2Fault), + whichCodeToReturn: () => [0], + codesToReturn: ['216129101'], + memoInhibit: () => false, + failure: 1, + sysPage: -1, + side: 'LEFT', + }, + 2161297: { // COND CTL 1-B FAULT + flightPhaseInhib: [2, 3, 4, 5, 6, 7, 8, 9], + simVarIsActive: MappedSubject.create(([acsc1Lane1Fault, acsc1Lane2Fault]) => !acsc1Lane1Fault && acsc1Lane2Fault, this.acsc1Lane1Fault, this.acsc1Lane2Fault), + whichCodeToReturn: () => [0], + codesToReturn: ['216129701'], + memoInhibit: () => false, + failure: 1, + sysPage: -1, + side: 'LEFT', + }, + 2161294: { // COND CTL 2-A FAULT + flightPhaseInhib: [2, 3, 4, 5, 6, 7, 8, 9], + simVarIsActive: MappedSubject.create(([acsc2Lane1Fault, acsc2Lane2Fault]) => acsc2Lane1Fault && !acsc2Lane2Fault, this.acsc2Lane1Fault, this.acsc2Lane2Fault), + whichCodeToReturn: () => [0], + codesToReturn: ['216129401'], + memoInhibit: () => false, + failure: 1, + sysPage: -1, + side: 'LEFT', + }, + 2161298: { // COND CTL 2-B FAULT + flightPhaseInhib: [2, 3, 4, 5, 6, 7, 8, 9], + simVarIsActive: MappedSubject.create(([acsc2Lane1Fault, acsc2Lane2Fault]) => !acsc2Lane1Fault && acsc2Lane2Fault, this.acsc2Lane1Fault, this.acsc2Lane2Fault), + whichCodeToReturn: () => [0], + codesToReturn: ['216129801'], + memoInhibit: () => false, + failure: 1, + sysPage: -1, + side: 'LEFT', + }, + 2163210: { // CKPT DUCT OVHT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.ckptDuctOvht, + whichCodeToReturn: () => [0, 1, null, 3], // TODO: Add support for Fahrenheit + codesToReturn: ['216321001', '216321002', '216321003', '216321004'], + memoInhibit: () => false, + failure: 2, + sysPage: 7, + side: 'LEFT', + }, + 2163211: { // FWD DUCT OVHT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.fwdDuctOvht, + whichCodeToReturn: () => [0, 1, null, 3], // TODO: Add support for Fahrenheit + codesToReturn: ['216321101', '216321102', '216321103', '216321104'], + memoInhibit: () => false, + failure: 2, + sysPage: 7, + side: 'LEFT', + }, + 2163212: { // AFT DUCT OVHT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.aftDuctOvht, + whichCodeToReturn: () => [0, 1, null, 3], // TODO: Add support for Fahrenheit + codesToReturn: ['216321201', '216321202', '216321203', '216321204'], + memoInhibit: () => false, + failure: 2, + sysPage: 7, + side: 'LEFT', + }, + 2163218: { // L+R CAB FAN FAULT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: MappedSubject.create(([cabFanHasFault1, cabFanHasFault2]) => cabFanHasFault1 && cabFanHasFault2, this.cabFanHasFault1, this.cabFanHasFault2), + whichCodeToReturn: () => [0, 1], + codesToReturn: ['216321801', '216321802'], + memoInhibit: () => false, + failure: 2, + sysPage: 7, + side: 'LEFT', + }, + 2163260: { // LAV+GALLEY FAN FAULT + flightPhaseInhib: [3, 4, 5, 7, 8, 9], + simVarIsActive: this.lavGalleyFanFault, + whichCodeToReturn: () => [0], + codesToReturn: ['216326001'], + memoInhibit: () => false, + failure: 1, + sysPage: -1, + side: 'LEFT', + }, + 2163290: { // HOT AIR FAULT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.hotAirDisagrees, + whichCodeToReturn: () => [0, + this.hotAirPbOn.get() ? 1 : null, + (this.anyDuctOvht.get() && this.hotAirPbOn.get()) ? 2 : null, + (this.anyDuctOvht.get() && this.pack1On.get()) ? 3 : null, + (this.anyDuctOvht.get() && this.pack2On.get()) ? 4 : null, + ], + codesToReturn: ['216329001', '216329002', '216329003', '216329004', '216329005'], + memoInhibit: () => false, + failure: 2, + sysPage: 7, + side: 'LEFT', + }, + 2163305: { // TRIM AIR SYS FAULT + flightPhaseInhib: [3, 4, 5, 7, 8], + simVarIsActive: this.trimAirFault, + whichCodeToReturn: () => [0, + this.ckptTrimFault.get() ? 1 : null, + this.fwdTrimFault.get() ? 2 : null, + this.aftTrimFault.get() ? 3 : null, + this.trimAirHighPressure.get() ? 4 : null, + ], + codesToReturn: ['216330501', '216330502', '216330503', '216330504', '216330505'], + memoInhibit: () => false, + failure: 1, + sysPage: -1, + side: 'LEFT', + }, 2600150: { // SMOKE FWD CARGO SMOKE flightPhaseInhib: [4, 5, 7, 8], simVarIsActive: this.cargoFireTest, @@ -2206,26 +2489,6 @@ export class PseudoFWC { sysPage: -1, side: 'LEFT', }, - 2161207: { // PACK 1 ABNORMALLY OFF - flightPhaseInhib: [1, 2, 3, 4, 5, 7, 8, 9, 10], - simVarIsActive: this.packOffNotFailed1Status, - whichCodeToReturn: () => [0], - codesToReturn: ['216120701'], - memoInhibit: () => false, - failure: 2, - sysPage: 1, - side: 'LEFT', - }, - 2161208: { // PACK 2 ABNORMALLY OFF - flightPhaseInhib: [1, 2, 3, 4, 5, 7, 8, 9, 10], - simVarIsActive: this.packOffNotFailed2Status, - whichCodeToReturn: () => [0], - codesToReturn: ['216120801'], - memoInhibit: () => false, - failure: 2, - sysPage: 1, - side: 'LEFT', - }, 3200060: { // NW ANTI SKID INACTIVE flightPhaseInhib: [4, 5], simVarIsActive: this.antiskidActive.map((v) => !v), diff --git a/fbw-a32nx/src/systems/instruments/src/SD/Pages/Cond/Cond.tsx b/fbw-a32nx/src/systems/instruments/src/SD/Pages/Cond/Cond.tsx index f2e58617fb9..5622f989d5e 100644 --- a/fbw-a32nx/src/systems/instruments/src/SD/Pages/Cond/Cond.tsx +++ b/fbw-a32nx/src/systems/instruments/src/SD/Pages/Cond/Cond.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: GPL-3.0 import React from 'react'; -import { useSimVar, usePersistentProperty } from '@flybywiresim/fbw-sdk'; +import { useArinc429Var, usePersistentProperty, useSimVar } from '@flybywiresim/fbw-sdk'; import { UnitType } from '@microsoft/msfs-sdk'; import { SvgGroup } from '../../Common/SvgGroup'; import Valve from './Valve'; @@ -16,20 +16,30 @@ export const CondPage = () => { const [unit] = usePersistentProperty('CONFIG_USING_METRIC_UNIT', '1'); - const [cockpitTrimAirValve] = useSimVar('L:A32NX_COND_CKPT_TRIM_AIR_VALVE_POSITION', 'number', 1000); - let [cockpitTrimTemp] = useSimVar('L:A32NX_COND_CKPT_DUCT_TEMP', 'celsius', 1000); + const acsc1DiscreteWord1 = useArinc429Var('L:A32NX_COND_ACSC_1_DISCRETE_WORD_1'); + const acsc2DiscreteWord1 = useArinc429Var('L:A32NX_COND_ACSC_2_DISCRETE_WORD_1'); + const acscDiscreteWord1 = !acsc1DiscreteWord1.isFailureWarning() ? acsc1DiscreteWord1 : acsc2DiscreteWord1; + + // TODO: If both Sign Status are Failure Warning or No Computed Data, the whole page should display XX's + + const [cockpitTrimAirValve] = useSimVar('L:A32NX_COND_CKPT_TRIM_AIR_VALVE_POSITION', 'number', 100); + let [cockpitTrimTemp] = useSimVar('L:A32NX_COND_CKPT_DUCT_TEMP', 'celsius', 100); let [cockpitCabinTemp] = useSimVar('L:A32NX_COND_CKPT_TEMP', 'celsius', 1000); - const [fwdTrimAirValve] = useSimVar('L:A32NX_COND_FWD_TRIM_AIR_VALVE_POSITION', 'number', 1000); - let [fwdTrimTemp] = useSimVar('L:A32NX_COND_FWD_DUCT_TEMP', 'celsius', 1000); + const [fwdTrimAirValve] = useSimVar('L:A32NX_COND_FWD_TRIM_AIR_VALVE_POSITION', 'number', 100); + let [fwdTrimTemp] = useSimVar('L:A32NX_COND_FWD_DUCT_TEMP', 'celsius', 100); let [fwdCabinTemp] = useSimVar('L:A32NX_COND_FWD_TEMP', 'celsius', 1000); - const [aftTrimAirValve] = useSimVar('L:A32NX_COND_AFT_TRIM_AIR_VALVE_POSITION', 'number', 1000); - let [aftTrimTemp] = useSimVar('L:A32NX_COND_AFT_DUCT_TEMP', 'celsius', 1000); + const [aftTrimAirValve] = useSimVar('L:A32NX_COND_AFT_TRIM_AIR_VALVE_POSITION', 'number', 100); + let [aftTrimTemp] = useSimVar('L:A32NX_COND_AFT_DUCT_TEMP', 'celsius', 100); let [aftCabinTemp] = useSimVar('L:A32NX_COND_AFT_TEMP', 'celsius', 1000); - const [hotAirOpen] = useSimVar('L:A32NX_HOT_AIR_VALVE_IS_OPEN', 'bool', 1000); - const [hotAirEnabled] = useSimVar('L:A32NX_HOT_AIR_VALVE_IS_ENABLED', 'bool', 1000); + const hotAirOpen = !acscDiscreteWord1.getBitValueOr(20, false); + const hotAirPositionDisagrees = acsc1DiscreteWord1.getBitValueOr(27, false) && acsc2DiscreteWord1.getBitValueOr(27, false); + const hotAirSwitchPosition = acscDiscreteWord1.getBitValueOr(23, false); + + const cabFanHasFault1 = acscDiscreteWord1.getBitValueOr(25, false); + const cabFanHasFault2 = acscDiscreteWord1.getBitValueOr(26, false); if (unit === '0') { // converting to F if 'lbs' selected in EFB cockpitTrimTemp = UnitType.CELSIUS.convertTo(cockpitTrimTemp, UnitType.FAHRENHEIT); @@ -48,11 +58,8 @@ export const CondPage = () => { TEMP : {unit === '1' ? '°C' : '°F'} - { /* Not yet implemented - FAN - FAN - ALTN MODE - */} + FAN + FAN {/* Plane shape */} @@ -61,13 +68,40 @@ export const CondPage = () => { {/* Cockpit */} - + {/* Fwd */} - + {/* Aft */} - + {/* Valve and tubes */} @@ -75,9 +109,9 @@ export const CondPage = () => { HOT AIR - - - + + + ); @@ -91,24 +125,26 @@ type CondUnitProps = { x: number, y: number, offset: number, - hotAir: number + hotAir: boolean } const CondUnit = ({ title, trimAirValve, cabinTemp, trimTemp, x, y, offset, hotAir } : CondUnitProps) => { const rotateTemp = offset + (trimAirValve * 86 / 100); + const overheat = trimTemp > 80; return ( {title} {cabinTemp.toFixed(0)} - {trimTemp.toFixed(0)} + {trimTemp.toFixed(0)} C H - + {/* TODO: When Trim valves are failed the gauge should be replaced by amber XX */} + diff --git a/fbw-a32nx/src/wasm/systems/a320_systems/src/air_conditioning.rs b/fbw-a32nx/src/wasm/systems/a320_systems/src/air_conditioning.rs index d5963d8f6eb..26d2df13102 100644 --- a/fbw-a32nx/src/wasm/systems/a320_systems/src/air_conditioning.rs +++ b/fbw-a32nx/src/wasm/systems/a320_systems/src/air_conditioning.rs @@ -1,12 +1,12 @@ use systems::{ accept_iterable, air_conditioning::{ - acs_controller::{AirConditioningSystemController, Pack}, + acs_controller::{AcscId, AirConditioningSystemController, Pack}, cabin_air::CabinAirSimulation, cabin_pressure_controller::CabinPressureController, pressure_valve::{OutflowValve, SafetyValve}, AdirsToAirCondInterface, Air, AirConditioningOverheadShared, AirConditioningPack, CabinFan, - DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, OverheadFlowSelector, + Channel, DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, OverheadFlowSelector, PackFlowControllers, PressurizationConstants, PressurizationOverheadShared, TrimAirSystem, ZoneType, }, @@ -17,9 +17,12 @@ use systems::{ payload::NumberOfPassengers, pneumatic::PneumaticContainer, shared::{ - random_number, update_iterator::MaxStepLoop, AverageExt, CabinAltitude, CabinSimulation, - ControllerSignal, ElectricalBusType, EngineCorrectedN1, EngineFirePushButtons, - EngineStartState, LgciuWeightOnWheels, PackFlowValveState, PneumaticBleed, + arinc429::{Arinc429Word, SignStatus}, + random_number, + update_iterator::MaxStepLoop, + AverageExt, CabinAltitude, CabinSimulation, ControllerSignal, ElectricalBusType, + EngineCorrectedN1, EngineFirePushButtons, EngineStartState, LgciuWeightOnWheels, + PackFlowValveState, PneumaticBleed, }, simulation::{ InitContext, Read, SimulationElement, SimulationElementVisitor, SimulatorReader, @@ -237,7 +240,8 @@ impl SimulationElement for A320Cabin { } pub struct A320AirConditioningSystem { - acsc: AirConditioningSystemController<3, 2>, + acs_interface: [AirConditioningSystemInterfaceUnit; 2], + acsc: [AirConditioningSystemController<3, 2>; 2], cabin_fans: [CabinFan; 2], mixer_unit: MixerUnit<3>, // Temporary structure until packs are simulated @@ -250,22 +254,60 @@ pub struct A320AirConditioningSystem { impl A320AirConditioningSystem { pub(crate) fn new(context: &mut InitContext, cabin_zones: &[ZoneType; 3]) -> Self { Self { - acsc: AirConditioningSystemController::new( - context, - cabin_zones, - vec![ - ElectricalBusType::DirectCurrent(1), - ElectricalBusType::AlternatingCurrent(1), - ], - vec![ - ElectricalBusType::DirectCurrent(2), - ElectricalBusType::AlternatingCurrent(2), - ], - ), - cabin_fans: [CabinFan::new(ElectricalBusType::AlternatingCurrent(1)); 2], + acs_interface: [ + AirConditioningSystemInterfaceUnit::new( + context, + AcscId::Acsc1(Channel::ChannelOne), + cabin_zones, + ), + AirConditioningSystemInterfaceUnit::new( + context, + AcscId::Acsc2(Channel::ChannelOne), + cabin_zones, + ), + ], + acsc: [ + AirConditioningSystemController::new( + context, + AcscId::Acsc1(Channel::ChannelOne), + cabin_zones, + [ + [ + ElectricalBusType::AlternatingCurrent(1), // 103XP + ElectricalBusType::DirectCurrent(1), // 101PP + ], + [ + ElectricalBusType::AlternatingCurrent(2), // 202XP + ElectricalBusType::DirectCurrentEssential, // 4PP + ], + ], + ), + AirConditioningSystemController::new( + context, + AcscId::Acsc2(Channel::ChannelOne), + cabin_zones, + [ + [ + ElectricalBusType::AlternatingCurrent(1), // 101XP + ElectricalBusType::DirectCurrent(1), // 103PP + ], + [ + ElectricalBusType::AlternatingCurrent(2), // 204XP + ElectricalBusType::DirectCurrent(2), // 206PP + ], + ], + ), + ], + cabin_fans: [ + CabinFan::new(1, ElectricalBusType::AlternatingCurrent(1)), + CabinFan::new(2, ElectricalBusType::AlternatingCurrent(2)), + ], mixer_unit: MixerUnit::new(cabin_zones), - packs: [AirConditioningPack::new(), AirConditioningPack::new()], - trim_air_system: TrimAirSystem::new(context, cabin_zones), + packs: [ + AirConditioningPack::new(Pack(1)), + AirConditioningPack::new(Pack(2)), + ], + trim_air_system: TrimAirSystem::new(context, cabin_zones, &[1]), air_conditioning_overhead: A320AirConditioningSystemOverhead::new(context, cabin_zones), } @@ -283,10 +325,9 @@ impl A320AirConditioningSystem { pressurization_overhead: &A320PressurizationOverheadPanel, lgciu: [&impl LgciuWeightOnWheels; 2], ) { - self.acsc.update( + self.update_acsc( context, adirs, - &self.air_conditioning_overhead, cabin_simulation, engines, engine_fire_push_buttons, @@ -294,37 +335,119 @@ impl A320AirConditioningSystem { pressurization, pressurization_overhead, lgciu, - &self.trim_air_system, ); + self.update_fans(cabin_simulation); + + self.update_packs(context); + + self.update_mixer_unit(); + + self.update_trim_air_system(context); + + self.update_acsc_interface(); + + self.air_conditioning_overhead + .set_pack_pushbutton_fault(self.pack_fault_determination()); + self.air_conditioning_overhead.set_hot_air_pushbutton_fault( + self.acsc[0].hot_air_pb_fault_light_determination() + || self.acsc[1].hot_air_pb_fault_light_determination(), + ); + } + + fn update_acsc( + &mut self, + context: &UpdateContext, + adirs: &impl AdirsToAirCondInterface, + cabin_simulation: &impl CabinSimulation, + engines: [&impl EngineCorrectedN1; 2], + engine_fire_push_buttons: &impl EngineFirePushButtons, + pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), + pressurization: &impl CabinAltitude, + pressurization_overhead: &A320PressurizationOverheadPanel, + lgciu: [&impl LgciuWeightOnWheels; 2], + ) { + for acsc in self.acsc.iter_mut() { + acsc.update( + context, + adirs, + &self.air_conditioning_overhead, + cabin_simulation, + engines, + engine_fire_push_buttons, + pneumatic, + pressurization, + pressurization_overhead, + lgciu, + &self.trim_air_system, + ); + } + } + + fn update_fans(&mut self, cabin_simulation: &impl CabinSimulation) { for fan in self.cabin_fans.iter_mut() { - fan.update(cabin_simulation, &self.acsc.cabin_fans_controller()) + fan.update(cabin_simulation, &self.acsc[1].cabin_fans_controller()) } + } + fn update_packs(&mut self, context: &UpdateContext) { let pack_flow: [MassRate; 2] = [ - self.acsc.individual_pack_flow(Pack(1)), - self.acsc.individual_pack_flow(Pack(2)), + self.acsc[0].individual_pack_flow(), + self.acsc[1].individual_pack_flow(), ]; - let duct_demand_temperature = self.acsc.duct_demand_temperature(); - for (id, pack) in self.packs.iter_mut().enumerate() { - pack.update(pack_flow[id], &duct_demand_temperature) - } + let duct_demand_temperature = vec![ + self.acsc[0].duct_demand_temperature()[0], + self.acsc[1].duct_demand_temperature()[1], + self.acsc[1].duct_demand_temperature()[2], + ]; + + [0, 1].iter().for_each(|&id| { + self.packs[id].update( + context, + pack_flow[id], + &duct_demand_temperature, + self.acsc[id].both_channels_failure(), + ) + }); + } + + fn update_mixer_unit(&mut self) { let mut mixer_intakes: Vec<&dyn OutletAir> = vec![&self.packs[0], &self.packs[1]]; for fan in self.cabin_fans.iter() { mixer_intakes.push(fan) } self.mixer_unit.update(mixer_intakes); + } - self.trim_air_system - .update(context, &self.mixer_unit, &self.acsc); + fn update_trim_air_system(&mut self, context: &UpdateContext) { + self.trim_air_system.update( + context, + &self.mixer_unit, + &[ + &self.acsc[0].trim_air_pressure_regulating_valve_controller(), + &self.acsc[1].trim_air_pressure_regulating_valve_controller(), + ], + &[&self.acsc[0], &self.acsc[1], &self.acsc[1]], + ); + } - self.air_conditioning_overhead - .set_pack_pushbutton_fault(self.pack_fault_determination()); + fn update_acsc_interface(&mut self) { + for (index, interface) in self.acs_interface.iter_mut().enumerate() { + interface.update( + &self.air_conditioning_overhead, + &self.acsc[index], + &self.cabin_fans, + &self.trim_air_system, + ) + } } pub fn pack_fault_determination(&self) -> [bool; 2] { - self.acsc.pack_fault_determination() + [ + self.acsc[0].pack_fault_determination(), + self.acsc[1].pack_fault_determination(), + ] } pub fn mix_packs_air_update(&mut self, pack_container: &mut [impl PneumaticContainer; 2]) { @@ -337,7 +460,8 @@ impl PackFlowControllers for A320AirConditioningSystem { as PackFlowControllers>::PackFlowControllerSignal; fn pack_flow_controller(&self, pack_id: usize) -> &Self::PackFlowControllerSignal { - self.acsc.pack_flow_controller(pack_id) + // // Pack ID 1 or 2 + self.acsc[pack_id - 1].pack_flow_controller(pack_id - 1) } } @@ -351,7 +475,7 @@ impl OutletAir for A320AirConditioningSystem { fn outlet_air(&self) -> Air { let mut outlet_air = Air::new(); outlet_air.set_flow_rate( - self.acsc.individual_pack_flow(Pack(1)) + self.acsc.individual_pack_flow(Pack(2)), + self.acsc[0].individual_pack_flow() + self.acsc[1].individual_pack_flow(), ); outlet_air.set_pressure(self.trim_air_system.trim_air_outlet_pressure()); outlet_air.set_temperature(self.duct_temperature().iter().average()); @@ -362,7 +486,8 @@ impl OutletAir for A320AirConditioningSystem { impl SimulationElement for A320AirConditioningSystem { fn accept(&mut self, visitor: &mut V) { - self.acsc.accept(visitor); + accept_iterable!(self.acs_interface, visitor); + accept_iterable!(self.acsc, visitor); self.trim_air_system.accept(visitor); accept_iterable!(self.cabin_fans, visitor); @@ -372,6 +497,87 @@ impl SimulationElement for A320AirConditioningSystem { } } +struct AirConditioningSystemInterfaceUnit { + discrete_word_1_id: VariableIdentifier, + discrete_word_2_id: VariableIdentifier, + + cabin_zones: [ZoneType; 3], + discrete_word_1: Arinc429Word, + discrete_word_2: Arinc429Word, +} + +impl AirConditioningSystemInterfaceUnit { + fn new(context: &mut InitContext, acsc_id: AcscId, cabin_zones: &[ZoneType; 3]) -> Self { + Self { + discrete_word_1_id: context + .get_identifier(format!("COND_ACSC_{}_DISCRETE_WORD_1", acsc_id)), + discrete_word_2_id: context + .get_identifier(format!("COND_ACSC_{}_DISCRETE_WORD_2", acsc_id)), + + cabin_zones: *cabin_zones, + discrete_word_1: Arinc429Word::new(0, SignStatus::NoComputedData), + discrete_word_2: Arinc429Word::new(0, SignStatus::NoComputedData), + } + } + + fn update( + &mut self, + acs_overhead: &impl AirConditioningOverheadShared, + acsc: &AirConditioningSystemController<3, 2>, + cabin_fans: &[CabinFan; 2], + trim_air_system: &TrimAirSystem<3, 2>, + ) { + let duct_overheat = self.cabin_zones.map(|zone| acsc.duct_overheat(zone.id())); + let trim_air_valve_fault = self + .cabin_zones + .map(|zone| trim_air_system.trim_air_valve_has_fault(zone.id())); + + if acsc.both_channels_failure() { + self.discrete_word_1 = Arinc429Word::new(0, SignStatus::FailureWarning); + self.discrete_word_2 = Arinc429Word::new(0, SignStatus::FailureWarning); + } else { + self.discrete_word_1 = Arinc429Word::new(0, SignStatus::NormalOperation); + self.discrete_word_2 = Arinc429Word::new(0, SignStatus::NormalOperation); + + self.discrete_word_1.set_bit(11, duct_overheat[0]); + self.discrete_word_1.set_bit(12, duct_overheat[1]); + self.discrete_word_1.set_bit(13, duct_overheat[2]); + self.discrete_word_1 + .set_bit(18, trim_air_system.trim_air_high_pressure()); + self.discrete_word_1.set_bit(19, acsc.active_channel_1()); + self.discrete_word_1 + .set_bit(20, !acsc.trim_air_pressure_regulating_valve_is_open()); + self.discrete_word_1.set_bit(21, acsc.channel_1_inop()); + self.discrete_word_1.set_bit(22, acsc.channel_2_inop()); + self.discrete_word_1 + .set_bit(23, acs_overhead.hot_air_pushbutton_is_on()); + self.discrete_word_1.set_bit(24, acsc.galley_fan_fault()); + self.discrete_word_1.set_bit(25, cabin_fans[0].has_fault()); + self.discrete_word_1.set_bit(26, cabin_fans[1].has_fault()); + self.discrete_word_1 + .set_bit(27, acsc.taprv_position_disagrees()); + self.discrete_word_1.set_bit( + 28, + trim_air_valve_fault.iter().any(|&t| t) || trim_air_system.trim_air_high_pressure(), + ); + self.discrete_word_1.set_bit(29, true); // Permanently true + + self.discrete_word_2.set_bit(18, trim_air_valve_fault[0]); + self.discrete_word_2.set_bit(19, trim_air_valve_fault[1]); + self.discrete_word_2.set_bit(20, trim_air_valve_fault[2]); + // 23 - Both packs off + // 24 - One pack operation + } + } +} + +impl SimulationElement for AirConditioningSystemInterfaceUnit { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write(&self.discrete_word_1_id, self.discrete_word_1); + writer.write(&self.discrete_word_2_id, self.discrete_word_2); + } +} + pub(crate) struct A320AirConditioningSystemOverhead { flow_selector_id: VariableIdentifier, @@ -412,6 +618,10 @@ impl A320AirConditioningSystemOverhead { .enumerate() .for_each(|(index, pushbutton)| pushbutton.set_fault(pb_has_fault[index])); } + + fn set_hot_air_pushbutton_fault(&mut self, hot_air_pb_has_fault: bool) { + self.hot_air_pb.set_fault(hot_air_pb_has_fault); + } } impl AirConditioningOverheadShared diff --git a/fbw-a32nx/src/wasm/systems/a320_systems/src/pneumatic.rs b/fbw-a32nx/src/wasm/systems/a320_systems/src/pneumatic.rs index 61e85bced30..7ef1397740e 100644 --- a/fbw-a32nx/src/wasm/systems/a320_systems/src/pneumatic.rs +++ b/fbw-a32nx/src/wasm/systems/a320_systems/src/pneumatic.rs @@ -2169,6 +2169,7 @@ pub mod tests { dc_ess_bus: ElectricalBus, dc_ess_shed_bus: ElectricalBus, ac_1_bus: ElectricalBus, + ac_2_bus: ElectricalBus, // Electric buses states to be able to kill them dynamically is_dc_1_powered: bool, is_dc_2_powered: bool, @@ -2200,6 +2201,7 @@ pub mod tests { ElectricalBusType::DirectCurrentEssentialShed, ), ac_1_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(1)), + ac_2_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(2)), is_dc_1_powered: true, is_dc_2_powered: true, is_dc_ess_powered: true, @@ -2243,6 +2245,8 @@ pub mod tests { if self.is_ac_1_powered { electricity.flow(&self.powered_source, &self.ac_1_bus); } + + electricity.flow(&self.powered_source, &self.ac_2_bus); } fn update_after_power_distribution(&mut self, context: &UpdateContext) { diff --git a/fbw-a32nx/src/wasm/systems/a320_systems_wasm/src/lib.rs b/fbw-a32nx/src/wasm/systems/a320_systems_wasm/src/lib.rs index c218defcd70..6146c92bdf1 100644 --- a/fbw-a32nx/src/wasm/systems/a320_systems_wasm/src/lib.rs +++ b/fbw-a32nx/src/wasm/systems/a320_systems_wasm/src/lib.rs @@ -24,6 +24,7 @@ use reversers::reversers; use rudder::rudder; use spoilers::spoilers; use std::error::Error; +use systems::air_conditioning::{acs_controller::AcscId, Channel, ZoneType}; use systems::failures::FailureType; use systems::shared::{ AirbusElectricPumpId, AirbusEngineDrivenPumpId, ElectricalBusType, GearActuatorId, @@ -62,6 +63,33 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { .with_auxiliary_power_unit(Variable::named("OVHD_APU_START_PB_IS_AVAILABLE"), 8, 7)? .with_engines(2)? .with_failures(vec![ + ( + 21_000, + FailureType::Acsc(AcscId::Acsc1(Channel::ChannelOne)), + ), + ( + 21_001, + FailureType::Acsc(AcscId::Acsc1(Channel::ChannelTwo)), + ), + ( + 21_002, + FailureType::Acsc(AcscId::Acsc2(Channel::ChannelOne)), + ), + ( + 21_003, + FailureType::Acsc(AcscId::Acsc2(Channel::ChannelTwo)), + ), + (21_004, FailureType::HotAir(1)), + (21_005, FailureType::TrimAirHighPressure), + (21_006, FailureType::TrimAirFault(ZoneType::Cockpit)), + (21_007, FailureType::TrimAirFault(ZoneType::Cabin(1))), + (21_008, FailureType::TrimAirFault(ZoneType::Cabin(2))), + (21_009, FailureType::TrimAirOverheat(ZoneType::Cockpit)), + (21_010, FailureType::TrimAirOverheat(ZoneType::Cabin(1))), + (21_011, FailureType::TrimAirOverheat(ZoneType::Cabin(2))), + (21_012, FailureType::CabinFan(1)), + (21_013, FailureType::CabinFan(2)), + (21_014, FailureType::GalleyFans), (24_000, FailureType::TransformerRectifier(1)), (24_001, FailureType::TransformerRectifier(2)), (24_002, FailureType::TransformerRectifier(3)), diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/mod.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/mod.rs index 3520b6185f2..241d70ea80c 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/mod.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/mod.rs @@ -1,15 +1,15 @@ use systems::{ accept_iterable, air_conditioning::{ - acs_controller::{AirConditioningSystemController, Pack}, + acs_controller::{AcscId, AirConditioningSystemController, Pack}, cabin_air::CabinAirSimulation, cabin_pressure_controller::CabinPressureController, full_digital_agu_controller::FullDigitalAGUController, pressure_valve::{OutflowValve, SafetyValve}, AdirsToAirCondInterface, Air, AirConditioningOverheadShared, AirConditioningPack, CabinFan, - DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, OverheadFlowSelector, PackFlow, - PackFlowControllers, PressurizationConstants, PressurizationOverheadShared, TrimAirSystem, - ZoneType, + Channel, DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, OverheadFlowSelector, + PackFlow, PackFlowControllers, PressurizationConstants, PressurizationOverheadShared, + TrimAirSystem, ZoneType, }, overhead::{ AutoManFaultPushButton, NormalOnPushButton, OnOffFaultPushButton, OnOffPushButton, @@ -319,14 +319,17 @@ impl A380AirConditioningSystem { Self { acsc: AirConditioningSystemController::new( context, + AcscId::Acsc1(Channel::ChannelOne), cabin_zones, - vec![ - ElectricalBusType::DirectCurrent(1), - ElectricalBusType::AlternatingCurrent(1), - ], - vec![ - ElectricalBusType::DirectCurrent(2), - ElectricalBusType::AlternatingCurrent(2), + [ + [ + ElectricalBusType::AlternatingCurrent(1), // 103XP + ElectricalBusType::DirectCurrent(1), // 101PP + ], + [ + ElectricalBusType::AlternatingCurrent(2), // 202XP + ElectricalBusType::DirectCurrentEssential, // 4PP + ], ], ), fdac: [ @@ -345,10 +348,16 @@ impl A380AirConditioningSystem { ], ), ], - cabin_fans: [CabinFan::new(ElectricalBusType::AlternatingCurrent(1)); 2], + cabin_fans: [ + CabinFan::new(1, ElectricalBusType::AlternatingCurrent(1)), + CabinFan::new(2, ElectricalBusType::AlternatingCurrent(1)), + ], mixer_unit: MixerUnit::new(cabin_zones), - packs: [AirConditioningPack::new(), AirConditioningPack::new()], - trim_air_system: TrimAirSystem::new(context, cabin_zones), + packs: [ + AirConditioningPack::new(Pack(1)), + AirConditioningPack::new(Pack(2)), + ], + trim_air_system: TrimAirSystem::new(context, cabin_zones, &[1]), air_conditioning_overhead: A380AirConditioningSystemOverhead::new(context), } @@ -401,13 +410,15 @@ impl A380AirConditioningSystem { fan.update(cabin_simulation, &self.acsc.cabin_fans_controller()) } - let pack_flow: [MassRate; 2] = [ - self.acsc.individual_pack_flow(Pack(1)), - self.acsc.individual_pack_flow(Pack(2)), - ]; + let pack_flow = [self.fdac[0].pack_flow(), self.fdac[1].pack_flow()]; let duct_demand_temperature = self.acsc.duct_demand_temperature(); for (id, pack) in self.packs.iter_mut().enumerate() { - pack.update(pack_flow[id], &duct_demand_temperature) + pack.update( + context, + pack_flow[id], + &duct_demand_temperature, + self.acsc.both_channels_failure(), + ) } let mut mixer_intakes: Vec<&dyn OutletAir> = vec![&self.packs[0], &self.packs[1]]; @@ -416,8 +427,12 @@ impl A380AirConditioningSystem { } self.mixer_unit.update(mixer_intakes); - self.trim_air_system - .update(context, &self.mixer_unit, &self.acsc); + self.trim_air_system.update( + context, + &self.mixer_unit, + &[&self.acsc.trim_air_pressure_regulating_valve_controller(); 18], + &[&self.acsc; 18], + ); self.air_conditioning_overhead .set_pack_pushbutton_fault(self.pack_fault_determination()); @@ -457,9 +472,8 @@ impl DuctTemperature for A380AirConditioningSystem { impl OutletAir for A380AirConditioningSystem { fn outlet_air(&self) -> Air { let mut outlet_air = Air::new(); - outlet_air.set_flow_rate( - self.acsc.individual_pack_flow(Pack(1)) + self.acsc.individual_pack_flow(Pack(2)), - ); + outlet_air + .set_flow_rate(self.acsc.individual_pack_flow() + self.acsc.individual_pack_flow()); outlet_air.set_pressure(self.trim_air_system.trim_air_outlet_pressure()); outlet_air.set_temperature(self.duct_temperature().iter().average()); diff --git a/fbw-common/src/systems/shared/src/ata.ts b/fbw-common/src/systems/shared/src/ata.ts index 6416b1873f4..623e7624430 100644 --- a/fbw-common/src/systems/shared/src/ata.ts +++ b/fbw-common/src/systems/shared/src/ata.ts @@ -93,6 +93,7 @@ export const AtaChaptersTitle = { }; export const AtaChaptersDescription = Object.freeze({ + 21: 'The air conditioning system regulates the temperature, air flow and pressure inside the aircraft. Its main function is to supply a high level of comfort to the passengers and crew and protect the aircraft systems like the avionics.', 22: 'The Autoflight systems are responsible for a multitude of functions, including but not limited to: Flight Guidance (AP, FD, A/THR), Flight Management, Flight Augmentation (yaw damper, etc.), and Flight Envelope (Speed scale, Alpha floor, etc.).', 24: 'All things related to the electrical system. The electrical system supplies power from the engines, APU, batteries, or emergency generator to all cockpit instruments.', 27: 'The flight controls contain the various systems used to control the aircraft in flight, such as control surfaces, but also flight control computers. Failure of these systems may lead to loss of control over the aircraft, and/or loss of information about the status of the flight controls.', diff --git a/fbw-common/src/wasm/systems/systems/src/air_conditioning/acs_controller.rs b/fbw-common/src/wasm/systems/systems/src/air_conditioning/acs_controller.rs index 48d823db36e..c4d9a3ed3ac 100644 --- a/fbw-common/src/wasm/systems/systems/src/air_conditioning/acs_controller.rs +++ b/fbw-common/src/wasm/systems/systems/src/air_conditioning/acs_controller.rs @@ -1,9 +1,10 @@ use crate::{ + failures::{Failure, FailureType}, pneumatic::{EngineModeSelector, EngineState, PneumaticValveSignal}, shared::{ pid::PidController, CabinAltitude, CabinSimulation, ControllerSignal, DelayedTrueLogicGate, - ElectricalBusType, ElectricalBuses, EngineCorrectedN1, EngineFirePushButtons, - EngineStartState, LgciuWeightOnWheels, PackFlowValveState, PneumaticBleed, + ElectricalBusType, EngineCorrectedN1, EngineFirePushButtons, EngineStartState, + LgciuWeightOnWheels, PackFlowValveState, PneumaticBleed, }, simulation::{ InitContext, SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext, @@ -12,12 +13,12 @@ use crate::{ }; use super::{ - AdirsToAirCondInterface, AirConditioningOverheadShared, DuctTemperature, OverheadFlowSelector, - PackFlow, PackFlowControllers, PackFlowValveSignal, PressurizationOverheadShared, - TrimAirSystem, ZoneType, + AdirsToAirCondInterface, AirConditioningOverheadShared, Channel, DuctTemperature, + OperatingChannel, OverheadFlowSelector, PackFlow, PackFlowControllers, PackFlowValveSignal, + PressurizationOverheadShared, TrimAirControllers, TrimAirSystem, ZoneType, }; -use std::time::Duration; +use std::{fmt::Display, time::Duration}; use uom::si::{ f64::*, @@ -29,50 +30,97 @@ use uom::si::{ velocity::knot, }; -#[derive(PartialEq, Clone, Copy, Debug)] -enum ACSCActiveComputer { - Primary, - Secondary, - None, +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum AcscId { + Acsc1(Channel), + Acsc2(Channel), } +impl From for usize { + fn from(value: AcscId) -> Self { + match value { + AcscId::Acsc1(_) => 1, + AcscId::Acsc2(_) => 2, + } + } +} + +impl Display for AcscId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AcscId::Acsc1(_) => write!(f, "1"), + AcscId::Acsc2(_) => write!(f, "2"), + } + } +} + +#[derive(PartialEq)] +enum AcscFault { + OneChannelFault, + BothChannelsFault, +} + +/// A320 ACSC P/N S1803A0001-xx pub struct AirConditioningSystemController { + id: AcscId, + active_channel: OperatingChannel, + stand_by_channel: OperatingChannel, + aircraft_state: AirConditioningStateManager, zone_controller: Vec>, - pack_flow_controller: [PackFlowController; 2], + pack_flow_controller: PackFlowController, trim_air_system_controller: TrimAirSystemController, cabin_fans_controller: CabinFanController, - primary_powered_by: Vec, - primary_is_powered: bool, - secondary_powered_by: Vec, - secondary_is_powered: bool, + + internal_failure: Option, } impl AirConditioningSystemController { pub fn new( context: &mut InitContext, + id: AcscId, cabin_zone_ids: &[ZoneType; ZONES], - primary_powered_by: Vec, - secondary_powered_by: Vec, + powered_by: [[ElectricalBusType; 2]; 2], ) -> Self { - let zone_controller = cabin_zone_ids - .iter() - .map(ZoneController::new) - .collect::>>(); + let failure_types = match id { + AcscId::Acsc1(_) => [ + FailureType::Acsc(AcscId::Acsc1(Channel::ChannelOne)), + FailureType::Acsc(AcscId::Acsc1(Channel::ChannelTwo)), + ], + AcscId::Acsc2(_) => [ + FailureType::Acsc(AcscId::Acsc2(Channel::ChannelOne)), + FailureType::Acsc(AcscId::Acsc2(Channel::ChannelTwo)), + ], + }; + Self { + id, + + active_channel: OperatingChannel::new(1, failure_types[0], &powered_by[0]), + stand_by_channel: OperatingChannel::new(2, failure_types[1], &powered_by[1]), + aircraft_state: AirConditioningStateManager::new(), - zone_controller, - pack_flow_controller: [ - PackFlowController::new(context, Pack(1)), - PackFlowController::new(context, Pack(2)), - ], - trim_air_system_controller: TrimAirSystemController::new(context), + zone_controller: Self::zone_controller_initiation(id, cabin_zone_ids), + pack_flow_controller: PackFlowController::new(context, Pack(id.into())), + trim_air_system_controller: TrimAirSystemController::new(), cabin_fans_controller: CabinFanController::new(), - primary_powered_by, - primary_is_powered: false, - secondary_powered_by, - secondary_is_powered: false, + internal_failure: None, + } + } + + fn zone_controller_initiation( + id: AcscId, + cabin_zone_ids: &[ZoneType; ZONES], + ) -> Vec> { + // ACSC 1 regulates the cockpit temperature and ACSC 2 the cabin zones + if matches!(id, AcscId::Acsc1(_)) { + vec![ZoneController::new(&cabin_zone_ids[0])] + } else { + cabin_zone_ids[1..] + .iter() + .map(ZoneController::new) + .collect::>>() } } @@ -90,33 +138,33 @@ impl AirConditioningSystemController, ) { + self.fault_determination(); + let ground_speed = self.ground_speed(adirs).unwrap_or_default(); self.aircraft_state = self .aircraft_state .update(context, ground_speed, &engines, lgciu); - let operation_mode = self.operation_mode_determination(); - - for pack_flow_controller in self.pack_flow_controller.iter_mut() { - pack_flow_controller.update( - context, - &self.aircraft_state, - acs_overhead, - engine_fire_push_buttons, - pneumatic, - pressurization, - pressurization_overhead, - operation_mode, - ); - } + self.pack_flow_controller.update( + context, + &self.aircraft_state, + acs_overhead, + engine_fire_push_buttons, + pneumatic, + pressurization, + pressurization_overhead, + !self.both_channels_failure(), + ); - for (index, zone) in self.zone_controller.iter_mut().enumerate() { + let both_channels_failure = self.both_channels_failure(); + for zone in self.zone_controller.iter_mut() { zone.update( context, + self.id, acs_overhead, - cabin_temperature.cabin_temperature()[index], + !both_channels_failure, + cabin_temperature.cabin_temperature(), pressurization, - &operation_mode, ) } @@ -124,25 +172,59 @@ impl AirConditioningSystemController ACSCActiveComputer { - // TODO: Add failures - if self.primary_is_powered { - ACSCActiveComputer::Primary - } else if self.secondary_is_powered { - ACSCActiveComputer::Secondary + fn fault_determination(&mut self) { + self.active_channel.update_fault(); + self.stand_by_channel.update_fault(); + + self.internal_failure = if self.active_channel.has_fault() { + if self.stand_by_channel.has_fault() { + Some(AcscFault::BothChannelsFault) + } else { + self.switch_active_channel(); + Some(AcscFault::OneChannelFault) + } + } else if self.stand_by_channel.has_fault() { + Some(AcscFault::OneChannelFault) } else { - ACSCActiveComputer::None - } + None + }; + } + + fn switch_active_channel(&mut self) { + std::mem::swap(&mut self.stand_by_channel, &mut self.active_channel); + } + + pub fn active_channel_1(&self) -> bool { + matches!(self.active_channel.id(), Channel::ChannelOne) + } + + pub fn channel_1_inop(&self) -> bool { + [&self.active_channel, &self.stand_by_channel] + .iter() + .find(|channel| matches!(channel.id(), Channel::ChannelOne)) + .unwrap() + .has_fault() + } + + pub fn channel_2_inop(&self) -> bool { + [&self.active_channel, &self.stand_by_channel] + .iter() + .find(|channel| matches!(channel.id(), Channel::ChannelTwo)) + .unwrap() + .has_fault() + } + + pub fn both_channels_failure(&self) -> bool { + self.internal_failure == Some(AcscFault::BothChannelsFault) } fn ground_speed(&self, adirs: &impl AdirsToAirCondInterface) -> Option { @@ -152,31 +234,61 @@ impl AirConditioningSystemController [bool; 2] { - [ - self.pack_flow_controller[Pack(1).to_index()].fcv_fault_determination(), - self.pack_flow_controller[Pack(2).to_index()].fcv_fault_determination(), - ] - } - - pub(super) fn trim_air_valve_controllers(&self, zone_id: usize) -> TrimAirValveController { - self.trim_air_system_controller - .trim_air_valve_controllers(zone_id) + pub fn pack_fault_determination(&self) -> bool { + self.pack_flow_controller.fcv_fault_determination() || self.both_channels_failure() } pub fn cabin_fans_controller(&self) -> CabinFanController { self.cabin_fans_controller } - pub fn individual_pack_flow(&self, pack_id: Pack) -> MassRate { - self.pack_flow_controller[pack_id.to_index()].pack_flow() + pub fn individual_pack_flow(&self) -> MassRate { + self.pack_flow_controller.pack_flow() } - pub fn duct_demand_temperature(&self) -> Vec { - self.zone_controller + pub fn duct_demand_temperature(&self) -> [ThermodynamicTemperature; ZONES] { + let demand_temperature: Vec = self + .zone_controller .iter() .map(|zone| zone.duct_demand_temperature()) - .collect() + .collect(); + // Because each ACSC calculates the demand of its respective zone(s), we fill the vector for the trim air system + let mut filler_vector = [ThermodynamicTemperature::new::(24.); ZONES]; + if matches!(self.id, AcscId::Acsc1(_)) { + filler_vector[..1].copy_from_slice(&demand_temperature); + } else { + filler_vector[1..].copy_from_slice(&demand_temperature); + }; + filler_vector + } + + pub fn trim_air_pressure_regulating_valve_controller( + &self, + ) -> TrimAirPressureRegulatingValveController { + self.trim_air_system_controller.taprv_controller() + } + + pub fn trim_air_pressure_regulating_valve_is_open(&self) -> bool { + self.trim_air_system_controller.tarpv_is_open() + } + + pub fn duct_overheat(&self, zone_id: usize) -> bool { + self.trim_air_system_controller.duct_overheat(zone_id) + } + + pub fn hot_air_pb_fault_light_determination(&self) -> bool { + self.trim_air_system_controller.duct_overheat_monitor() + } + + pub fn galley_fan_fault(&self) -> bool { + self.zone_controller + .iter() + .any(|zone| zone.galley_fan_fault()) + } + + pub fn taprv_position_disagrees(&self) -> bool { + self.trim_air_system_controller + .taprv_disagree_status_monitor() } } @@ -184,7 +296,7 @@ impl PackFlow for AirConditioningSystemController { fn pack_flow(&self) -> MassRate { - self.pack_flow_controller[0].pack_flow() + self.pack_flow_controller[1].pack_flow() + self.pack_flow_controller.pack_flow() } } @@ -193,8 +305,17 @@ impl PackFlowControllers { type PackFlowControllerSignal = PackFlowController; - fn pack_flow_controller(&self, pack_id: usize) -> &Self::PackFlowControllerSignal { - &self.pack_flow_controller[pack_id - 1] + fn pack_flow_controller(&self, _pack_id: usize) -> &Self::PackFlowControllerSignal { + &self.pack_flow_controller + } +} + +impl TrimAirControllers + for AirConditioningSystemController +{ + fn trim_air_valve_controllers(&self, zone_id: usize) -> TrimAirValveController { + self.trim_air_system_controller + .trim_air_valve_controllers(zone_id) } } @@ -202,18 +323,13 @@ impl SimulationElement for AirConditioningSystemController { fn accept(&mut self, visitor: &mut T) { - accept_iterable!(self.pack_flow_controller, visitor); - self.trim_air_system_controller.accept(visitor); + self.active_channel.accept(visitor); + self.stand_by_channel.accept(visitor); - visitor.visit(self); - } + self.pack_flow_controller.accept(visitor); + accept_iterable!(self.zone_controller, visitor); - fn receive_power(&mut self, buses: &impl ElectricalBuses) { - self.primary_is_powered = self.primary_powered_by.iter().all(|&p| buses.is_powered(p)); - self.secondary_is_powered = self - .secondary_powered_by - .iter() - .all(|&p| buses.is_powered(p)); + visitor.visit(self); } } @@ -461,6 +577,8 @@ struct ZoneController { duct_demand_temperature: ThermodynamicTemperature, zone_selected_temperature: ThermodynamicTemperature, pid_controller: PidController, + + galley_fan_failure: Failure, } impl ZoneController { @@ -507,37 +625,38 @@ impl ZoneController { duct_demand_temperature: ThermodynamicTemperature::new::(24.), zone_selected_temperature: ThermodynamicTemperature::new::(24.), pid_controller, + + galley_fan_failure: Failure::new(FailureType::GalleyFans), } } fn update( &mut self, context: &UpdateContext, + acsc_id: AcscId, acs_overhead: &impl AirConditioningOverheadShared, - zone_measured_temperature: ThermodynamicTemperature, + is_enabled: bool, + zone_measured_temperature: Vec, pressurization: &impl CabinAltitude, - operation_mode: &ACSCActiveComputer, ) { - self.zone_selected_temperature = if matches!(operation_mode, ACSCActiveComputer::Secondary) - { - // If the Zone controller is working on secondary power, the zones are controlled to - // 24 degrees by the secondary computer + self.zone_selected_temperature = if !is_enabled { + // If unpowered or failed, the zone is maintained at fixed temperature ThermodynamicTemperature::new::(24.) } else { acs_overhead.selected_cabin_temperature(self.zone_id) }; - self.duct_demand_temperature = if matches!(operation_mode, ACSCActiveComputer::None) { - // If unpowered or failed, the pack controller would take over and deliver a fixed 20deg - // for the cockpit and 10 for the cabin - // Simulated here until packs are modelled - ThermodynamicTemperature::new::(if self.zone_id == 0 { - 20. + self.duct_demand_temperature = + if self.galley_fan_failure.is_active() && matches!(acsc_id, AcscId::Acsc2(_)) { + // Cabin zone temperature sensors are ventilated by air extracted by this fan, cabin temperature regulation is lost + // Cabin inlet duct is constant at 15C, cockpit air is unnafected + ThermodynamicTemperature::new::(15.) } else { - 10. - }) - } else { - self.calculate_duct_temp_demand(context, pressurization, zone_measured_temperature) - }; + self.calculate_duct_temp_demand( + context, + pressurization, + zone_measured_temperature[self.zone_id], + ) + }; } fn calculate_duct_temp_demand( @@ -637,6 +756,17 @@ impl ZoneController { fn duct_demand_temperature(&self) -> ThermodynamicTemperature { self.duct_demand_temperature } + + fn galley_fan_fault(&self) -> bool { + self.galley_fan_failure.is_active() + } +} + +impl SimulationElement for ZoneController { + fn accept(&mut self, visitor: &mut T) { + self.galley_fan_failure.accept(visitor); + visitor.visit(self); + } } #[derive(Clone, Copy)] @@ -669,13 +799,13 @@ pub struct PackFlowController { pack_flow_id: VariableIdentifier, id: usize, + is_enabled: bool, flow_demand: Ratio, fcv_open_allowed: bool, should_open_fcv: bool, pack_flow: MassRate, pack_flow_demand: MassRate, pid: PidController, - operation_mode: ACSCActiveComputer, fcv_timer_open: Duration, fcv_failed_open_monitor: DelayedTrueLogicGate, @@ -704,13 +834,13 @@ impl PackFlowController { pack_flow_id: context.get_identifier(Self::pack_flow_id(pack_id.to_index())), id: pack_id.to_index(), + is_enabled: false, flow_demand: Ratio::default(), fcv_open_allowed: false, should_open_fcv: false, pack_flow: MassRate::default(), pack_flow_demand: MassRate::default(), pid: PidController::new(0.01, 0.1, 0., 0., 1., 0., 1.), - operation_mode: ACSCActiveComputer::None, fcv_timer_open: Duration::from_secs(0), fcv_failed_open_monitor: DelayedTrueLogicGate::new(Self::FCV_FAILED_OPEN_TIME_LIMIT), @@ -736,10 +866,10 @@ impl PackFlowController { pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), pressurization: &impl CabinAltitude, pressurization_overhead: &impl PressurizationOverheadShared, - operation_mode: ACSCActiveComputer, + is_enabled: bool, ) { // TODO: Add overheat protection - self.operation_mode = operation_mode; + self.is_enabled = is_enabled; self.flow_demand = self.flow_demand_determination(aircraft_state, acs_overhead, pneumatic); self.update_pressure_condition(context, pneumatic); self.fcv_open_allowed = self.fcv_open_allowed_determination( @@ -777,12 +907,9 @@ impl PackFlowController { acs_overhead: &impl AirConditioningOverheadShared, pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), ) -> Ratio { - if matches!(self.operation_mode, ACSCActiveComputer::None) { - // If the computer is unpowered, return previous flow demand - return self.flow_demand; - } else if matches!(self.operation_mode, ACSCActiveComputer::Secondary) { - // If Secondary computer is active flow setting optimization is not available - return Ratio::new::(100.); + if !self.is_enabled { + // If both lanes of the ACSC fail, the PFV closes and the flow demand is 0 + return Ratio::default(); } let mut intermediate_flow: Ratio = acs_overhead.flow_selector_position().into(); // TODO: Add "insufficient performance" based on Pack Mixer Temperature Demand @@ -907,17 +1034,13 @@ impl PackFlow for PackFlowController { impl ControllerSignal for PackFlowController { fn signal(&self) -> Option { - // Only send signal to move the valve if the computer is powered - if !matches!(self.operation_mode, ACSCActiveComputer::None) { - let target_open = Ratio::new::(if self.should_open_fcv { - self.pid.output() - } else { - 0. - }); - Some(PackFlowValveSignal::new(target_open)) + // If both lanes of the ACSC fail, the PFV closes + let target_open = if self.is_enabled && self.should_open_fcv { + Ratio::new::(self.pid.output()) } else { - None - } + Ratio::default() + }; + Some(PackFlowValveSignal::new(target_open)) } } @@ -927,24 +1050,37 @@ impl SimulationElement for PackFlowController { } } -#[derive(Clone, Copy)] struct TrimAirSystemController { - hot_air_is_enabled_id: VariableIdentifier, - hot_air_is_open_id: VariableIdentifier, - + duct_overheat: [bool; ZONES], is_enabled: bool, is_open: bool, + overheat_timer: [Duration; ZONES], + taprv_open_disagrees: bool, + taprv_open_timer: Duration, + taprv_closed_disagrees: bool, + taprv_closed_timer: Duration, + taprv_controller: TrimAirPressureRegulatingValveController, trim_air_valve_controllers: [TrimAirValveController; ZONES], } impl TrimAirSystemController { - fn new(context: &mut InitContext) -> Self { - Self { - hot_air_is_enabled_id: context.get_identifier("HOT_AIR_VALVE_IS_ENABLED".to_owned()), - hot_air_is_open_id: context.get_identifier("HOT_AIR_VALVE_IS_OPEN".to_owned()), + const DUCT_OVERHEAT_SET_LIMIT: f64 = 88.; // Deg C + const DUCT_OVERHEAT_RESET_LIMIT: f64 = 70.; // Deg C + const TAPRV_OPEN_COMMAND_DISAGREE_TIMER: f64 = 30.; // seconds + const TAPRV_CLOSE_COMMAND_DISAGREE_TIMER: f64 = 14.; // seconds + const TIMER_RESET: f64 = 1.2; // seconds + fn new() -> Self { + Self { + duct_overheat: [false; ZONES], is_enabled: false, is_open: false, + overheat_timer: [Duration::default(); ZONES], + taprv_open_disagrees: false, + taprv_open_timer: Duration::default(), + taprv_closed_disagrees: false, + taprv_closed_timer: Duration::default(), + taprv_controller: TrimAirPressureRegulatingValveController::new(), trim_air_valve_controllers: [TrimAirValveController::new(); ZONES], } } @@ -954,72 +1090,184 @@ impl TrimAirSystemController; 2], - operation_mode: ACSCActiveComputer, + is_enabled: bool, + pack_flow_controller: &PackFlowController, pneumatic: &impl PackFlowValveState, trim_air_system: &TrimAirSystem, ) { - self.is_enabled = self - .trim_air_pressure_regulating_valve_status_determination(acs_overhead, operation_mode); - - self.is_open = self.trim_air_pressure_regulating_valve_is_open_determination( + // If both lanes of the ACSC fail, the associated trim air valves close + self.is_enabled = self.trim_air_pressure_regulating_valve_status_determination( + acs_overhead, + trim_air_system.any_trim_air_valve_has_fault(), + is_enabled, pack_flow_controller, pneumatic, - ) && self.is_enabled; + ); + + self.taprv_controller.update(self.is_enabled); + + self.is_open = trim_air_system.trim_air_pressure_regulating_valve_is_open(); for (id, tav_controller) in self.trim_air_valve_controllers.iter_mut().enumerate() { tav_controller.update( context, - self.is_enabled && self.is_open, + self.is_open, trim_air_system.duct_temperature()[id], duct_demand_temperature[id], ) } + + self.duct_overheat = (0..ZONES) + .map(|id| { + self.duct_zone_overheat_monitor( + context, + acs_overhead, + trim_air_system.duct_temperature(), + id, + ) + }) + .collect::>() + .try_into() + .unwrap_or_else(|v: Vec| { + panic!("Expected a Vec of length {} but it was {}", ZONES, v.len()) + }); + + self.taprv_open_disagrees = self.taprv_open_command_disagree_monitor(context); + self.taprv_closed_disagrees = self.taprv_closed_command_disagree_monitor(context); } fn trim_air_pressure_regulating_valve_status_determination( &self, acs_overhead: &impl AirConditioningOverheadShared, - operation_mode: ACSCActiveComputer, - ) -> bool { - // TODO: Add overheat protection - // TODO: If more than one TAV fails, the system should be off - acs_overhead.hot_air_pushbutton_is_on() && operation_mode == ACSCActiveComputer::Primary - } - - fn trim_air_pressure_regulating_valve_is_open_determination( - &self, - pack_flow_controller: &[PackFlowController; 2], + any_tav_has_fault: bool, + is_enabled: bool, + pack_flow_controller: &PackFlowController, pneumatic: &impl PackFlowValveState, ) -> bool { - !pack_flow_controller - .iter() - .any(|pack| pack.pack_start_condition_determination(pneumatic)) + acs_overhead.hot_air_pushbutton_is_on() + && is_enabled + && !pack_flow_controller.pack_start_condition_determination(pneumatic) && ((pneumatic.pack_flow_valve_is_open(1)) || (pneumatic.pack_flow_valve_is_open(2))) + && !self.duct_overheat_monitor() + && !any_tav_has_fault } fn trim_air_valve_controllers(&self, zone_id: usize) -> TrimAirValveController { self.trim_air_valve_controllers[zone_id] } - fn is_enabled(&self) -> bool { - self.is_enabled + fn tarpv_is_open(&self) -> bool { + self.is_open } - fn is_open(&self) -> bool { - self.is_open + fn taprv_controller(&self) -> TrimAirPressureRegulatingValveController { + self.taprv_controller } -} -impl SimulationElement - for TrimAirSystemController -{ - fn write(&self, writer: &mut SimulatorWriter) { - writer.write(&self.hot_air_is_enabled_id, self.is_enabled()); - writer.write(&self.hot_air_is_open_id, self.is_open()); + fn duct_zone_overheat_monitor( + &mut self, + context: &UpdateContext, + acs_overhead: &impl AirConditioningOverheadShared, + duct_temperature: Vec, + zone_id: usize, + ) -> bool { + if duct_temperature[zone_id] + > ThermodynamicTemperature::new::(Self::DUCT_OVERHEAT_SET_LIMIT) + { + if self.overheat_timer[zone_id] > Duration::from_secs_f64(Self::TIMER_RESET) { + true + } else { + self.overheat_timer[zone_id] += context.delta(); + false + } + } else if self.duct_overheat[zone_id] + && ((duct_temperature[zone_id] + > ThermodynamicTemperature::new::(Self::DUCT_OVERHEAT_RESET_LIMIT)) + || (duct_temperature[zone_id] + <= ThermodynamicTemperature::new::( + Self::DUCT_OVERHEAT_RESET_LIMIT, + ) + && acs_overhead.hot_air_pushbutton_is_on())) + { + true + } else if self.duct_overheat[zone_id] + && duct_temperature[zone_id] + <= ThermodynamicTemperature::new::(Self::DUCT_OVERHEAT_RESET_LIMIT) + && !acs_overhead.hot_air_pushbutton_is_on() + { + self.overheat_timer[zone_id] = Duration::default(); + false + } else { + self.duct_overheat[zone_id] + } + } + + fn taprv_open_command_disagree_monitor(&mut self, context: &UpdateContext) -> bool { + if !self.is_enabled { + false + } else if !self.is_open && !self.taprv_open_disagrees { + if self.taprv_open_timer + > Duration::from_secs_f64(Self::TAPRV_OPEN_COMMAND_DISAGREE_TIMER) + { + self.taprv_open_timer = Duration::default(); + true + } else { + self.taprv_open_timer += context.delta(); + false + } + } else if self.is_open && self.taprv_open_disagrees { + if self.taprv_open_timer > Duration::from_secs_f64(Self::TIMER_RESET) { + self.taprv_open_timer = Duration::default(); + false + } else { + self.taprv_open_timer += context.delta(); + true + } + } else { + self.taprv_open_disagrees + } + } + + fn taprv_closed_command_disagree_monitor(&mut self, context: &UpdateContext) -> bool { + if self.is_enabled { + false + } else if self.is_open && !self.taprv_closed_disagrees { + if self.taprv_closed_timer + > Duration::from_secs_f64(Self::TAPRV_CLOSE_COMMAND_DISAGREE_TIMER) + { + self.taprv_closed_timer = Duration::default(); + true + } else { + self.taprv_closed_timer += context.delta(); + false + } + } else if !self.is_open && self.taprv_closed_disagrees { + if self.taprv_closed_timer > Duration::from_secs_f64(Self::TIMER_RESET) { + self.taprv_closed_timer = Duration::default(); + false + } else { + self.taprv_closed_timer += context.delta(); + true + } + } else { + self.taprv_closed_disagrees + } + } + + fn duct_overheat(&self, zone_id: usize) -> bool { + self.duct_overheat[zone_id] + } + + fn duct_overheat_monitor(&self) -> bool { + self.duct_overheat.iter().any(|&overheat| overheat) + } + + fn taprv_disagree_status_monitor(&self) -> bool { + self.taprv_open_disagrees || self.taprv_closed_disagrees } } +#[derive(Default)] pub struct TrimAirValveSignal { target_open_amount: Ratio, } @@ -1035,7 +1283,34 @@ impl PneumaticValveSignal for TrimAirValveSignal { } #[derive(Clone, Copy)] -pub(super) struct TrimAirValveController { +pub struct TrimAirPressureRegulatingValveController { + should_open_taprv: bool, +} + +impl TrimAirPressureRegulatingValveController { + fn new() -> Self { + Self { + should_open_taprv: false, + } + } + + fn update(&mut self, should_open_taprv: bool) { + self.should_open_taprv = should_open_taprv + } +} + +impl ControllerSignal for TrimAirPressureRegulatingValveController { + fn signal(&self) -> Option { + if self.should_open_taprv { + Some(TrimAirValveSignal::new(Ratio::new::(100.))) + } else { + Some(TrimAirValveSignal::new_closed()) + } + } +} + +#[derive(Clone, Copy)] +pub struct TrimAirValveController { tav_open_allowed: bool, pid: PidController, } @@ -1095,13 +1370,8 @@ impl CabinFanController { Self { is_enabled: false } } - fn update( - &mut self, - acs_overhead: &impl AirConditioningOverheadShared, - operation_mode: ACSCActiveComputer, - ) { - self.is_enabled = - acs_overhead.cabin_fans_is_on() && !matches!(operation_mode, ACSCActiveComputer::None); + fn update(&mut self, acs_overhead: &impl AirConditioningOverheadShared) { + self.is_enabled = acs_overhead.cabin_fans_is_on(); } #[cfg(test)] @@ -1486,7 +1756,7 @@ mod acs_controller_tests { fn update( &mut self, context: &UpdateContext, - pack_flow_valve_signals: &impl PackFlowControllers, + pack_flow_valve_signals: [&impl PackFlowControllers; 2], engine_bleed: [&impl EngineCorrectedN1; 2], ) { let apu_bleed_is_on = self.apu_bleed_is_on(); @@ -1499,8 +1769,9 @@ mod acs_controller_tests { self.packs .iter_mut() .zip(self.engine_bleed.iter_mut()) - .for_each(|(pack, engine_bleed)| { - pack.update(context, engine_bleed, pack_flow_valve_signals) + .enumerate() + .for_each(|(id, (pack, engine_bleed))| { + pack.update(context, engine_bleed, pack_flow_valve_signals[id]) }); } @@ -1587,7 +1858,7 @@ mod acs_controller_tests { PneumaticPipe::new( Volume::new::(8.), Pressure::new::(44.), - ThermodynamicTemperature::new::(144.), + ThermodynamicTemperature::new::(200.), ) } else { PneumaticPipe::new( @@ -1604,7 +1875,7 @@ mod acs_controller_tests { PneumaticPipe::new( Volume::new::(16.), Pressure::new::(14.7), - ThermodynamicTemperature::new::(131.), + ThermodynamicTemperature::new::(200.), ) } else { PneumaticPipe::new( @@ -1918,7 +2189,7 @@ mod acs_controller_tests { } struct TestAircraft { - acsc: AirConditioningSystemController<2, 2>, + acsc: [AirConditioningSystemController<2, 2>; 2], acs_overhead: TestAcsOverhead, adirs: TestAdirs, air_conditioning_system: TestAirConditioningSystem, @@ -1940,45 +2211,73 @@ mod acs_controller_tests { powered_ac_source_1: TestElectricitySource, powered_dc_source_2: TestElectricitySource, powered_ac_source_2: TestElectricitySource, + powered_dc_ess_source: TestElectricitySource, dc_1_bus: ElectricalBus, ac_1_bus: ElectricalBus, dc_2_bus: ElectricalBus, ac_2_bus: ElectricalBus, + dc_ess_bus: ElectricalBus, } impl TestAircraft { fn new(context: &mut InitContext) -> Self { let cabin_zones = [ZoneType::Cockpit, ZoneType::Cabin(1)]; Self { - acsc: AirConditioningSystemController::new( - context, - &cabin_zones, - vec![ - ElectricalBusType::DirectCurrent(1), - ElectricalBusType::AlternatingCurrent(1), - ], - vec![ - ElectricalBusType::DirectCurrent(2), - ElectricalBusType::AlternatingCurrent(2), - ], - ), + acsc: [ + AirConditioningSystemController::new( + context, + AcscId::Acsc1(Channel::ChannelOne), + &cabin_zones, + [ + [ + ElectricalBusType::AlternatingCurrent(1), // 103XP + ElectricalBusType::DirectCurrent(1), // 101PP + ], + [ + ElectricalBusType::AlternatingCurrent(2), // 202XP + ElectricalBusType::DirectCurrentEssential, // 4PP + ], + ], + ), + AirConditioningSystemController::new( + context, + AcscId::Acsc2(Channel::ChannelOne), + &cabin_zones, + [ + [ + ElectricalBusType::AlternatingCurrent(1), // 101XP + ElectricalBusType::DirectCurrent(1), // 103PP + ], + [ + ElectricalBusType::AlternatingCurrent(2), // 204XP + ElectricalBusType::DirectCurrent(2), // 206PP + ], + ], + ), + ], acs_overhead: TestAcsOverhead::new(context), adirs: TestAdirs::new(), air_conditioning_system: TestAirConditioningSystem::new(), - cabin_fans: [CabinFan::new(ElectricalBusType::AlternatingCurrent(1)); 2], + cabin_fans: [ + CabinFan::new(1, ElectricalBusType::AlternatingCurrent(1)), + CabinFan::new(2, ElectricalBusType::AlternatingCurrent(2)), + ], engine_1: TestEngine::new(Ratio::default()), engine_2: TestEngine::new(Ratio::default()), engine_fire_push_buttons: TestEngineFirePushButtons::new(), mixer_unit: MixerUnit::new(&cabin_zones), number_of_passengers: 0, - packs: [AirConditioningPack::new(), AirConditioningPack::new()], + packs: [ + AirConditioningPack::new(Pack(1)), + AirConditioningPack::new(Pack(2)), + ], pneumatic: TestPneumatic::new(context), pressurization: TestPressurization::new(), pressurization_overhead: TestPressurizationOverheadPanel::new(context), lgciu1: TestLgciu::new(false), lgciu2: TestLgciu::new(false), cabin_air_simulation: TestCabinAirSimulation::new(context), - trim_air_system: TrimAirSystem::new(context, &cabin_zones), + trim_air_system: TrimAirSystem::new(context, &cabin_zones, &[1]), powered_dc_source_1: TestElectricitySource::powered( context, PotentialOrigin::Battery(1), @@ -1995,10 +2294,15 @@ mod acs_controller_tests { context, PotentialOrigin::EngineGenerator(2), ), + powered_dc_ess_source: TestElectricitySource::powered( + context, + PotentialOrigin::StaticInverter, + ), dc_1_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrent(1)), ac_1_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(1)), dc_2_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrent(2)), ac_2_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(2)), + dc_ess_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrentEssential), } } @@ -2075,6 +2379,10 @@ mod acs_controller_tests { fn power_ac_2_bus(&mut self) { self.powered_ac_source_2.power(); } + + fn unpower_dc_ess_bus(&mut self) { + self.powered_dc_ess_source.unpower(); + } } impl Aircraft for TestAircraft { fn update_before_power_distribution( @@ -2086,30 +2394,38 @@ mod acs_controller_tests { electricity.supplied_by(&self.powered_ac_source_1); electricity.supplied_by(&self.powered_dc_source_2); electricity.supplied_by(&self.powered_ac_source_2); + electricity.supplied_by(&self.powered_dc_ess_source); electricity.flow(&self.powered_dc_source_1, &self.dc_1_bus); electricity.flow(&self.powered_ac_source_1, &self.ac_1_bus); electricity.flow(&self.powered_dc_source_2, &self.dc_2_bus); electricity.flow(&self.powered_ac_source_2, &self.ac_2_bus); + electricity.flow(&self.powered_dc_ess_source, &self.dc_ess_bus) } fn update_after_power_distribution(&mut self, context: &UpdateContext) { let lgciu_gears_compressed = self.lgciu1.compressed() && self.lgciu2.compressed(); - self.acsc.update( + for acsc in self.acsc.iter_mut() { + acsc.update( + context, + &self.adirs, + &self.acs_overhead, + &self.cabin_air_simulation, + [&self.engine_1, &self.engine_2], + &self.engine_fire_push_buttons, + &self.pneumatic, + &self.pressurization, + &self.pressurization_overhead, + [&self.lgciu1, &self.lgciu2], + &self.trim_air_system, + ); + } + + self.pneumatic.update( context, - &self.adirs, - &self.acs_overhead, - &self.cabin_air_simulation, + [&self.acsc[0], &self.acsc[1]], [&self.engine_1, &self.engine_2], - &self.engine_fire_push_buttons, - &self.pneumatic, - &self.pressurization, - &self.pressurization_overhead, - [&self.lgciu1, &self.lgciu2], - &self.trim_air_system, ); - self.pneumatic - .update(context, &self.acsc, [&self.engine_1, &self.engine_2]); self.trim_air_system .mix_packs_air_update(self.pneumatic.packs()); @@ -2122,18 +2438,27 @@ mod acs_controller_tests { [0, self.number_of_passengers / 2], ); - let pack_flow: [MassRate; 2] = [ - self.acsc.individual_pack_flow(Pack(1)), - self.acsc.individual_pack_flow(Pack(2)), + let pack_flow = [0, 1].map(|id| self.acsc[id].individual_pack_flow()); + + let duct_demand_temperature = vec![ + self.acsc[0].duct_demand_temperature()[0], + self.acsc[1].duct_demand_temperature()[1], ]; - let duct_demand_temperature = self.acsc.duct_demand_temperature(); - for (id, pack) in self.packs.iter_mut().enumerate() { - pack.update(pack_flow[id], &duct_demand_temperature) - } + + [0, 1].iter().for_each(|&id| { + self.packs[id].update( + context, + pack_flow[id], + &duct_demand_temperature, + self.acsc[id].both_channels_failure(), + ) + }); + + // Fan monitors by ACSC 2 for fan in self.cabin_fans.iter_mut() { fan.update( &self.cabin_air_simulation, - &self.acsc.cabin_fans_controller(), + &self.acsc[1].cabin_fans_controller(), ); } let mut mixer_intakes: Vec<&dyn OutletAir> = vec![&self.packs[0], &self.packs[1]]; @@ -2142,26 +2467,37 @@ mod acs_controller_tests { } self.mixer_unit.update(mixer_intakes); - self.trim_air_system - .update(context, &self.mixer_unit, &self.acsc); + self.trim_air_system.update( + context, + &self.mixer_unit, + &[ + &self.acsc[0].trim_air_pressure_regulating_valve_controller(), + &self.acsc[1].trim_air_pressure_regulating_valve_controller(), + ], + &[&self.acsc[0], &self.acsc[1]], + ); - self.acs_overhead - .set_pack_pushbutton_fault(self.acsc.pack_fault_determination()); + self.acs_overhead.set_pack_pushbutton_fault([ + self.acsc[0].pack_fault_determination(), + self.acsc[1].pack_fault_determination(), + ]); self.air_conditioning_system.update( self.trim_air_system.duct_temperature(), - self.acsc.individual_pack_flow(Pack(1)) + self.acsc.individual_pack_flow(Pack(2)), + self.acsc[0].individual_pack_flow() + self.acsc[1].individual_pack_flow(), self.trim_air_system.trim_air_outlet_pressure(), ); } } + impl SimulationElement for TestAircraft { fn accept(&mut self, visitor: &mut V) { - self.acsc.accept(visitor); + accept_iterable!(self.acsc, visitor); self.acs_overhead.accept(visitor); self.cabin_air_simulation.accept(visitor); self.pneumatic.accept(visitor); self.pressurization_overhead.accept(visitor); + self.trim_air_system.accept(visitor); accept_iterable!(self.cabin_fans, visitor); visitor.visit(self); @@ -2271,49 +2607,49 @@ mod acs_controller_tests { fn ac_state_is_initialisation(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::Initialisation(_) ) } fn ac_state_is_on_ground(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::OnGround(_) ) } fn ac_state_is_begin_takeoff(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::BeginTakeOff(_) ) } fn ac_state_is_end_takeoff(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::EndTakeOff(_) ) } fn ac_state_is_in_flight(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::InFlight(_) ) } fn ac_state_is_begin_landing(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::BeginLanding(_) ) } fn ac_state_is_end_landing(&self) -> bool { matches!( - self.query(|a| a.acsc.aircraft_state), + self.query(|a| a.acsc[0].aircraft_state), AirConditioningStateManager::EndLanding(_) ) } @@ -2358,6 +2694,11 @@ mod acs_controller_tests { self } + fn unpowered_dc_ess_bus(mut self) -> Self { + self.command(|a| a.unpower_dc_ess_bus()); + self + } + fn hot_air_pb_on(mut self, value: bool) -> Self { self.command(|a| a.acs_overhead.set_hot_air_pb(value)); self @@ -2450,7 +2791,10 @@ mod acs_controller_tests { } fn duct_demand_temperature(&self) -> Vec { - self.query(|a| a.acsc.duct_demand_temperature()) + vec![ + self.query(|a| a.acsc[0].duct_demand_temperature()[0]), + self.query(|a| a.acsc[1].duct_demand_temperature())[1], + ] } fn duct_temperature(&self) -> Vec { @@ -2473,8 +2817,8 @@ mod acs_controller_tests { fn trim_air_system_controller_is_enabled(&self) -> bool { self.query(|a| { - a.acsc.trim_air_system_controller.is_enabled() - && a.acsc.trim_air_system_controller.is_open() + a.acsc[0].trim_air_pressure_regulating_valve_is_open() + && a.acsc[1].trim_air_pressure_regulating_valve_is_open() }) } @@ -2486,17 +2830,25 @@ mod acs_controller_tests { self.query(|a| a.trim_air_system.duct_temperature()[0]) } + fn trim_air_system_outlet_pressure(&self) -> Pressure { + self.query(|a| a.trim_air_system.outlet_air.pressure()) + } + fn trim_air_valves_open_amount(&self) -> [Ratio; 2] { self.query(|a| a.trim_air_system.trim_air_valves_open_amount()) } fn mixer_unit_controller_is_enabled(&self) -> bool { - self.query(|a| a.acsc.cabin_fans_controller.is_enabled()) + self.query(|a| a.acsc[0].cabin_fans_controller.is_enabled()) } fn mixer_unit_outlet_air(&self) -> Air { self.query(|a| a.mixer_unit.outlet_air()) } + + fn trim_air_high_pressure(&self) -> bool { + self.query(|a| a.trim_air_system.trim_air_high_pressure()) + } } impl TestBed for ACSCTestBed { @@ -2758,14 +3110,157 @@ mod acs_controller_tests { } } - mod zone_controller_tests { + mod air_conditioning_system_controller_tests { use super::*; - const A320_ZONE_IDS: [&str; 2] = ["CKPT", "FWD"]; - #[test] - fn duct_demand_temperature_starts_at_24_c_in_all_zones() { - let test_bed = test_bed(); + fn trim_air_achieves_selected_temperature() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]) + .iterate(1000); + + assert!((test_bed.measured_temperature().get::() - 26.).abs() < 1.); + } + + #[test] + fn unpowering_one_lane_has_no_effect() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]) + .unpowered_ac_1_bus() + .iterate(1000); + + assert!((test_bed.measured_temperature().get::() - 26.).abs() < 1.); + } + + #[test] + fn failing_one_lane_has_no_effect() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]); + + test_bed.fail(FailureType::Acsc(AcscId::Acsc1(Channel::ChannelOne))); + test_bed = test_bed.iterate(1000); + + assert!((test_bed.measured_temperature().get::() - 26.).abs() < 1.); + } + + #[test] + fn unpowering_both_lanes_shuts_off_pack() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]) + .unpowered_ac_1_bus() + .unpowered_dc_2_bus() + .iterate(1000); + + assert_eq!(test_bed.trim_air_valves_open_amount()[1], Ratio::default()); + assert!(!test_bed.trim_air_system_controller_is_enabled()); + assert!((test_bed.measured_temperature().get::() - 26.).abs() > 1.); + } + + #[test] + fn failing_both_lanes_shuts_off_pack() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]); + + test_bed.fail(FailureType::Acsc(AcscId::Acsc2(Channel::ChannelOne))); + test_bed.fail(FailureType::Acsc(AcscId::Acsc2(Channel::ChannelTwo))); + test_bed = test_bed.iterate(1000); + + assert_eq!(test_bed.trim_air_valves_open_amount()[1], Ratio::default()); + assert!(!test_bed.trim_air_system_controller_is_enabled()); + assert!((test_bed.measured_temperature().get::() - 26.).abs() > 1.); + } + + #[test] + fn unpowering_opposite_acsc_doesnt_shut_off_pack() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]) + .unpowered_ac_1_bus() + .unpowered_dc_1_bus() + .unpowered_dc_ess_bus() + .iterate(1000); + + assert_ne!(test_bed.pack_flow(), MassRate::default()); + assert!((test_bed.measured_temperature().get::() - 24.).abs() < 1.); + } + + #[test] + fn failing_opposite_acsc_doesnt_shut_off_pack() { + let mut test_bed = test_bed() + .with() + .both_packs_on() + .and() + .engine_idle() + .and() + .command_selected_temperature([ + ThermodynamicTemperature::new::(24.), + ThermodynamicTemperature::new::(26.), + ]); + + test_bed.fail(FailureType::Acsc(AcscId::Acsc1(Channel::ChannelOne))); + test_bed.fail(FailureType::Acsc(AcscId::Acsc1(Channel::ChannelTwo))); + test_bed = test_bed.iterate(1000); + + assert_ne!(test_bed.pack_flow(), MassRate::default()); + assert!((test_bed.measured_temperature().get::() - 24.).abs() < 1.); + } + } + + mod zone_controller_tests { + use super::*; + + const A320_ZONE_IDS: [&str; 2] = ["CKPT", "FWD"]; + + #[test] + fn duct_demand_temperature_starts_at_24_c_in_all_zones() { + let test_bed = test_bed(); for id in 0..A320_ZONE_IDS.len() { assert_eq!( @@ -2972,7 +3467,7 @@ mod acs_controller_tests { } #[test] - fn knobs_dont_affect_duct_temperature_when_primary_unpowered() { + fn knobs_dont_affect_duct_temperature_one_acsc_unpowered() { let mut test_bed = test_bed() .with() .both_packs_on() @@ -2981,6 +3476,7 @@ mod acs_controller_tests { .and() .unpowered_dc_1_bus() .unpowered_ac_1_bus() + .unpowered_dc_ess_bus() .command_selected_temperature( [ThermodynamicTemperature::new::(30.); 2], ); @@ -2991,7 +3487,7 @@ mod acs_controller_tests { } #[test] - fn unpowering_the_system_gives_control_to_packs() { + fn failing_galley_fans_sets_duct_demand_to_15c() { let mut test_bed = test_bed() .with() .both_packs_on() @@ -2999,33 +3495,33 @@ mod acs_controller_tests { .engine_idle() .iterate(2) .and() - .unpowered_dc_1_bus() - .unpowered_ac_1_bus() - .unpowered_dc_2_bus() - .unpowered_ac_2_bus() .command_selected_temperature( [ThermodynamicTemperature::new::(30.); 2], - ); + ) + .iterate(500); - test_bed = test_bed.iterate_with_delta(100, Duration::from_secs(10)); + test_bed.fail(FailureType::GalleyFans); + + test_bed = test_bed.iterate(100); assert!( - (test_bed.duct_demand_temperature()[0].get::() - 20.).abs() < 1. + (test_bed.duct_demand_temperature()[1].get::() - 15.).abs() < 1. ); - assert!( - (test_bed.duct_demand_temperature()[1].get::() - 10.).abs() < 1. + assert_eq!( + (test_bed.trim_air_valves_open_amount()[1]), + Ratio::default() ); } #[test] - fn unpowering_and_repowering_primary_behaves_as_expected() { + fn unpowering_and_repowering_acsc_behaves_as_expected() { let mut test_bed = test_bed() .with() .both_packs_on() .and() .engine_idle() .and() - .unpowered_dc_1_bus() + .unpowered_dc_2_bus() .unpowered_ac_1_bus() .command_selected_temperature( [ThermodynamicTemperature::new::(30.); 2], @@ -3033,7 +3529,7 @@ mod acs_controller_tests { test_bed = test_bed.iterate(1000); assert!((test_bed.duct_temperature()[1].get::() - 24.).abs() < 1.); - test_bed = test_bed.powered_dc_1_bus().powered_ac_1_bus(); + test_bed = test_bed.powered_dc_2_bus().powered_ac_1_bus(); test_bed = test_bed.iterate(1000); assert!(test_bed.duct_temperature()[1].get::() > 24.); } @@ -3328,7 +3824,7 @@ mod acs_controller_tests { } #[test] - fn pack_flow_controller_is_unresponsive_when_unpowered() { + fn pack_flow_is_zero_when_acsc_unpowered() { let mut test_bed = test_bed() .with() .both_packs_on() @@ -3342,14 +3838,14 @@ mod acs_controller_tests { .unpowered_ac_1_bus() .unpowered_dc_2_bus() .unpowered_ac_2_bus(); - test_bed.command_ditching_on(); + test_bed = test_bed.iterate(2); - assert!(test_bed.pack_flow() > MassRate::default()); + assert_eq!(test_bed.pack_flow(), MassRate::default()); } #[test] - fn unpowering_ac_or_dc_unpowers_system() { + fn unpowering_one_acsc_shuts_down_one_pack_only() { let mut test_bed = test_bed() .with() .both_packs_on() @@ -3358,44 +3854,29 @@ mod acs_controller_tests { .iterate(2); assert!(test_bed.pack_flow() > MassRate::default()); - test_bed = test_bed.unpowered_dc_1_bus().unpowered_ac_2_bus(); - test_bed.command_ditching_on(); + let initial_flow = test_bed.pack_flow(); + + test_bed = test_bed.unpowered_dc_2_bus().unpowered_ac_1_bus(); + test_bed = test_bed.iterate(2); + + assert!(test_bed.pack_flow() < initial_flow); assert!(test_bed.pack_flow() > MassRate::default()); test_bed = test_bed - .powered_dc_1_bus() - .unpowered_ac_1_bus() - .unpowered_dc_2_bus() - .powered_ac_2_bus(); + .unpowered_dc_1_bus() + .unpowered_dc_ess_bus() + .powered_dc_2_bus() + .powered_ac_1_bus(); test_bed = test_bed.iterate(2); + assert!(test_bed.pack_flow() < initial_flow); assert!(test_bed.pack_flow() > MassRate::default()); - test_bed = test_bed.powered_ac_1_bus().powered_dc_2_bus().iterate(2); - assert_eq!(test_bed.pack_flow(), MassRate::default(),); - } - - #[test] - fn pack_flow_loses_optimization_when_secondary_computer_active() { - let mut test_bed = test_bed() - .with() - .both_packs_on() - .and() - .engine_idle() - .iterate(40); - - let initial_flow = test_bed.pack_flow(); - test_bed.command_apu_bleed_on(); - test_bed = test_bed.iterate(2); - assert!(test_bed.pack_flow() > initial_flow); - - test_bed = test_bed.unpowered_dc_1_bus().unpowered_ac_1_bus(); - test_bed.command_pack_flow_selector_position(0.); - test_bed = test_bed.iterate(20); - assert!( - (test_bed.pack_flow() - initial_flow).abs() - < MassRate::new::(0.1) - ); + test_bed = test_bed + .unpowered_ac_1_bus() + .unpowered_dc_2_bus() + .iterate(20); + assert_eq!(test_bed.pack_flow(), MassRate::default()); } #[test] @@ -3413,9 +3894,9 @@ mod acs_controller_tests { .unpowered_ac_1_bus() .unpowered_dc_2_bus() .unpowered_ac_2_bus(); - test_bed.command_ditching_on(); + test_bed = test_bed.iterate(2); - assert!(test_bed.pack_flow() > MassRate::default()); + assert_eq!(test_bed.pack_flow(), MassRate::default()); test_bed = test_bed .powered_dc_1_bus() @@ -3423,7 +3904,7 @@ mod acs_controller_tests { .powered_dc_2_bus() .powered_ac_2_bus() .iterate(2); - assert_eq!(test_bed.pack_flow(), MassRate::default()); + assert!(test_bed.pack_flow() > MassRate::default()); } } @@ -3461,21 +3942,24 @@ mod acs_controller_tests { .iterate(32); assert!(test_bed.trim_air_system_controller_is_enabled()); - test_bed = test_bed.hot_air_pb_on(false).and_run(); + test_bed = test_bed.hot_air_pb_on(false).iterate(4); assert!(!test_bed.trim_air_system_controller_is_enabled()); test_bed = test_bed.hot_air_pb_on(true); test_bed.command_pack_1_pb_position(false); - test_bed = test_bed.iterate(2); + test_bed = test_bed.iterate(4); assert!(test_bed.trim_air_system_controller_is_enabled()); // Pack 1 should be in start condition test_bed.command_pack_1_pb_position(true); - test_bed = test_bed.iterate(2); + test_bed = test_bed.iterate(6); assert!(!test_bed.trim_air_system_controller_is_enabled()); - // Secondary circuit - test_bed = test_bed.unpowered_dc_1_bus().unpowered_ac_1_bus().and_run(); + // ACSC 1 unpowered + test_bed = test_bed + .unpowered_dc_1_bus() + .unpowered_ac_1_bus() + .iterate(4); assert!(!test_bed.trim_air_system_controller_is_enabled()); test_bed = test_bed.powered_dc_1_bus().powered_ac_1_bus().iterate(32); @@ -3484,6 +3968,8 @@ mod acs_controller_tests { } mod mixer_unit_tests { + use crate::failures::FailureType; + use super::*; #[test] @@ -3508,12 +3994,15 @@ mod acs_controller_tests { test_bed = test_bed.cab_fans_pb_on(false).and_run(); assert!(!test_bed.mixer_unit_controller_is_enabled()); - // Unpower both circuits + // Unpowering ACSC doesn't affect fans test_bed = test_bed.cab_fans_pb_on(true); - test_bed = test_bed.unpowered_dc_1_bus().unpowered_ac_2_bus().and_run(); - assert!(!test_bed.mixer_unit_controller_is_enabled()); + test_bed = test_bed + .unpowered_dc_1_bus() + .unpowered_ac_1_bus() + .unpowered_dc_2_bus() + .unpowered_ac_2_bus() + .and_run(); - test_bed = test_bed.powered_dc_1_bus().powered_ac_2_bus().and_run(); assert!(test_bed.mixer_unit_controller_is_enabled()); } @@ -3667,13 +4156,42 @@ mod acs_controller_tests { > MassRate::new::(0.1) ); - test_bed = test_bed.unpowered_ac_1_bus().iterate(50); + test_bed = test_bed + .unpowered_ac_1_bus() + .unpowered_ac_2_bus() + .iterate(50); assert!( (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()) < MassRate::new::(0.1) ) } + + #[test] + fn cabin_fans_dont_work_with_fault() { + let mut test_bed = test_bed() + .with() + .cab_fans_pb_on(true) + .and() + .both_packs_on() + .and() + .engine_idle() + .iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()) + > MassRate::new::(0.1) + ); + + test_bed.fail(FailureType::CabinFan(1)); + test_bed.fail(FailureType::CabinFan(2)); + test_bed = test_bed.iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()) + < MassRate::new::(0.1) + ); + } } mod trim_air_tests { @@ -3721,6 +4239,28 @@ mod acs_controller_tests { ); } + #[test] + fn trim_air_pressure_regulating_valve_is_unresponsive_when_failed() { + let mut test_bed = test_bed() + .with() + .hot_air_pb_on(true) + .and() + .engine_idle() + .command_fwd_selected_temperature(ThermodynamicTemperature::new::( + 30., + )) + .iterate(400); + + test_bed.fail(FailureType::HotAir(1)); + test_bed = test_bed.hot_air_pb_on(false).iterate(100); + + assert!((test_bed.trim_air_system_outlet_air(1).flow_rate()) > MassRate::default()); + assert!( + (test_bed.trim_air_system_outlet_air(1).temperature()) + > ThermodynamicTemperature::new::(25.) + ); + } + #[test] fn trim_valves_close_if_selected_temp_below_measured() { let mut test_bed = test_bed() @@ -3743,6 +4283,158 @@ mod acs_controller_tests { ); } + #[test] + fn trim_air_valves_are_unresponsive_when_failed() { + let mut test_bed = test_bed() + .with() + .engine_idle() + .both_packs_on() + .and() + .command_fwd_selected_temperature(ThermodynamicTemperature::new::( + 30., + )); + + test_bed.command_measured_temperature( + [ThermodynamicTemperature::new::(15.); 2], + ); + + test_bed = test_bed.iterate(100); + + assert!((test_bed.trim_air_valves_open_amount()[1]) > Ratio::default()); + + let initial_open = test_bed.trim_air_valves_open_amount()[1]; + + test_bed = test_bed.command_fwd_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(18.)); + + test_bed.fail(FailureType::TrimAirFault(ZoneType::Cabin(1))); + + test_bed.command_measured_temperature( + [ThermodynamicTemperature::new::(30.); 2], + ); + + test_bed = test_bed.iterate(100); + + assert!((test_bed.trim_air_valves_open_amount()[1]) > Ratio::default()); + assert_eq!(test_bed.trim_air_valves_open_amount()[1], initial_open); + } + + #[test] + fn trim_air_system_delivers_overheat_air_if_overheat() { + let mut test_bed = test_bed() + .with() + .hot_air_pb_on(true) + .and() + .engine_idle() + .command_fwd_selected_temperature(ThermodynamicTemperature::new::( + 30., + )) + .iterate(500); + + assert!((test_bed.trim_air_system_outlet_air(1).flow_rate()) > MassRate::default()); + assert!( + (test_bed.trim_air_system_outlet_air(1).temperature()) + > ThermodynamicTemperature::new::(25.) + ); + + test_bed.fail(FailureType::TrimAirOverheat(ZoneType::Cabin(1))); + + test_bed = test_bed.iterate(1); + + assert!( + (test_bed.duct_temperature()[1]) + > ThermodynamicTemperature::new::(88.) + ); + } + + #[test] + fn hot_air_closes_if_overheat() { + let mut test_bed = test_bed() + .with() + .hot_air_pb_on(true) + .and() + .engine_idle() + .command_fwd_selected_temperature(ThermodynamicTemperature::new::( + 30., + )) + .iterate(500); + + test_bed.command_measured_temperature( + [ThermodynamicTemperature::new::(15.); 2], + ); + assert!((test_bed.trim_air_system_outlet_air(1).flow_rate()) > MassRate::default()); + test_bed.fail(FailureType::TrimAirOverheat(ZoneType::Cabin(1))); + + test_bed = test_bed.iterate(500); + + assert!( + (test_bed.trim_air_system_outlet_air(1).flow_rate()) + < MassRate::new::(0.001) + ); + } + + #[test] + fn hot_air_closes_if_one_tav_failed() { + let mut test_bed = test_bed() + .with() + .hot_air_pb_on(true) + .and() + .engine_idle() + .command_fwd_selected_temperature(ThermodynamicTemperature::new::( + 30., + )); + + test_bed.command_measured_temperature([ + ThermodynamicTemperature::new::(25.), + ThermodynamicTemperature::new::(15.), + ]); + + test_bed = test_bed.iterate(500); + + assert!((test_bed.duct_temperature()[1] > test_bed.duct_temperature()[0])); + test_bed.fail(FailureType::TrimAirFault(ZoneType::Cabin(1))); + + test_bed = test_bed.iterate(100); + + assert!( + (test_bed.duct_temperature()[0].get::() + - test_bed.duct_temperature()[1].get::()) + .abs() + < 1. + ); + } + + #[test] + fn trim_increases_pressure_if_overpressure() { + let mut test_bed = test_bed() + .with() + .engine_idle() + .and() + .command_fwd_selected_temperature(ThermodynamicTemperature::new::( + 30., + )); + + test_bed.command_measured_temperature( + [ThermodynamicTemperature::new::(15.); 2], + ); + + test_bed = test_bed.iterate(50); + + assert!( + (test_bed.trim_air_system_outlet_air(1).flow_rate()) + > MassRate::new::(0.01) + ); + assert!((test_bed.trim_air_system_outlet_pressure()) < Pressure::new::(20.)); + + test_bed.fail(FailureType::TrimAirHighPressure); + + test_bed = test_bed.iterate(50); + + assert!((test_bed.trim_air_system_outlet_pressure()) > Pressure::new::(20.)); + assert!(test_bed.trim_air_high_pressure()); + } + #[test] fn trim_valves_react_to_only_one_pack_operative() { let mut test_bed = test_bed() diff --git a/fbw-common/src/wasm/systems/systems/src/air_conditioning/mod.rs b/fbw-common/src/wasm/systems/systems/src/air_conditioning/mod.rs index ba3a4ba1389..a55163a6ed9 100644 --- a/fbw-common/src/wasm/systems/systems/src/air_conditioning/mod.rs +++ b/fbw-common/src/wasm/systems/systems/src/air_conditioning/mod.rs @@ -1,15 +1,14 @@ -use self::acs_controller::{ - AirConditioningSystemController, CabinFansSignal, Pack, TrimAirValveController, -}; +use self::acs_controller::{CabinFansSignal, Pack, TrimAirValveController, TrimAirValveSignal}; use crate::{ + failures::{Failure, FailureType}, pneumatic::{ valve::{DefaultValve, PneumaticExhaust}, ControllablePneumaticValve, PneumaticContainer, PneumaticPipe, PneumaticValveSignal, }, shared::{ - arinc429::Arinc429Word, AverageExt, CabinSimulation, ConsumePower, ControllerSignal, - ElectricalBusType, ElectricalBuses, + arinc429::Arinc429Word, low_pass_filter::LowPassFilter, AverageExt, CabinSimulation, + ConsumePower, ControllerSignal, ElectricalBusType, ElectricalBuses, }, simulation::{ InitContext, SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext, @@ -17,7 +16,7 @@ use crate::{ }, }; -use std::{convert::TryInto, fmt::Display}; +use std::{convert::TryInto, fmt::Display, time::Duration}; use uom::si::{ f64::*, @@ -55,6 +54,10 @@ pub trait PackFlowControllers { fn pack_flow_controller(&self, pack_id: usize) -> &Self::PackFlowControllerSignal; } +pub trait TrimAirControllers { + fn trim_air_valve_controllers(&self, zone_id: usize) -> TrimAirValveController; +} + pub struct PackFlowValveSignal { target_open_amount: Ratio, } @@ -97,6 +100,7 @@ pub trait PressurizationOverheadShared { /// Cabin Zones with double digit IDs are specific to the A380 /// 1X is main deck, 2X is upper deck +#[derive(Clone, Copy, Eq, PartialEq)] pub enum ZoneType { Cockpit, Cabin(u8), @@ -104,7 +108,7 @@ pub enum ZoneType { } impl ZoneType { - fn id(&self) -> usize { + pub fn id(&self) -> usize { match self { ZoneType::Cockpit => 0, ZoneType::Cabin(number) => { @@ -199,6 +203,84 @@ pub trait CabinPressure { fn cabin_pressure(&self) -> Pressure; } +// Future work this can be different types of failure. +enum OperatingChannelFault { + NoFault, + Fault, +} + +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum Channel { + ChannelOne, + ChannelTwo, +} + +impl From for usize { + fn from(value: Channel) -> Self { + match value { + Channel::ChannelOne => 1, + Channel::ChannelTwo => 2, + } + } +} + +impl From for Channel { + fn from(value: usize) -> Self { + match value { + 1 => Channel::ChannelOne, + 2 => Channel::ChannelTwo, + _ => panic!("Operating Channel out of bounds"), + } + } +} + +struct OperatingChannel { + channel_id: Channel, + powered_by: Vec, + is_powered: bool, + failure: Failure, + fault: OperatingChannelFault, +} + +impl OperatingChannel { + fn new(id: usize, failure_type: FailureType, powered_by: &[ElectricalBusType]) -> Self { + Self { + channel_id: id.into(), + powered_by: powered_by.to_vec(), + is_powered: false, + failure: Failure::new(failure_type), + fault: OperatingChannelFault::NoFault, + } + } + + fn update_fault(&mut self) { + self.fault = if !self.is_powered || self.failure.is_active() { + OperatingChannelFault::Fault + } else { + OperatingChannelFault::NoFault + }; + } + + fn has_fault(&self) -> bool { + matches!(self.fault, OperatingChannelFault::Fault) + } + + fn id(&self) -> Channel { + self.channel_id + } +} + +impl SimulationElement for OperatingChannel { + fn accept(&mut self, visitor: &mut T) { + self.failure.accept(visitor); + visitor.visit(self); + } + + fn receive_power(&mut self, buses: &impl ElectricalBuses) { + self.is_powered = self.powered_by.iter().all(|&p| buses.is_powered(p)); + } +} + pub trait PressurizationConstants { const CABIN_VOLUME_CUBIC_METER: f64; const COCKPIT_VOLUME_CUBIC_METER: f64; @@ -224,7 +306,6 @@ pub trait PressurizationConstants { const LOW_DIFFERENTIAL_PRESSURE_WARNING: f64; } -#[derive(Clone, Copy)] /// A320neo fan part number: VD3900-03 pub struct CabinFan { is_on: bool, @@ -232,6 +313,7 @@ pub struct CabinFan { is_powered: bool, powered_by: ElectricalBusType, + failure: Failure, } impl CabinFan { @@ -239,13 +321,14 @@ impl CabinFan { const PRESSURE_RISE_HPA: f64 = 22.; // hPa const FAN_EFFICIENCY: f64 = 0.75; // Ratio - so output matches AMM numbers - pub fn new(powered_by: ElectricalBusType) -> Self { + pub fn new(id: u8, powered_by: ElectricalBusType) -> Self { Self { is_on: false, outlet_air: Air::new(), is_powered: false, powered_by, + failure: Failure::new(FailureType::CabinFan(id as usize)), } } @@ -259,7 +342,10 @@ impl CabinFan { self.outlet_air .set_temperature(cabin_simulation.cabin_temperature().iter().average()); - if !self.is_powered || !matches!(controller.signal(), Some(CabinFansSignal::On)) { + if !self.is_powered + || self.failure.is_active() + || !matches!(controller.signal(), Some(CabinFansSignal::On)) + { self.is_on = false; self.outlet_air .set_pressure(cabin_simulation.cabin_pressure()); @@ -281,6 +367,10 @@ impl CabinFan { / (Air::R * self.outlet_air.temperature().get::()); MassRate::new::(mass_flow) } + + pub fn has_fault(&self) -> bool { + self.failure.is_active() + } } impl OutletAir for CabinFan { @@ -290,6 +380,11 @@ impl OutletAir for CabinFan { } impl SimulationElement for CabinFan { + fn accept(&mut self, visitor: &mut T) { + self.failure.accept(visitor); + visitor.visit(self); + } + fn receive_power(&mut self, buses: &impl ElectricalBuses) { self.is_powered = buses.is_powered(self.powered_by); } @@ -406,26 +501,49 @@ impl OutletAir for MixerUnitOutlet { /// Temporary struct until packs are fully simulated pub struct AirConditioningPack { + pack_id: Pack, + outlet_temperature: LowPassFilter, // Degree Celsius outlet_air: Air, } impl AirConditioningPack { - pub fn new() -> Self { + const PACK_REACTION_TIME: Duration = Duration::from_secs(10); + pub fn new(pack_id: Pack) -> Self { Self { + pack_id, + outlet_temperature: LowPassFilter::new_with_init_value(Self::PACK_REACTION_TIME, 15.), outlet_air: Air::new(), } } /// Takes the minimum duct demand temperature as the pack outlet temperature. This is accurate to real world behaviour but /// this is a placeholder until the packs are modelled - pub fn update(&mut self, pack_flow: MassRate, duct_demand: &[ThermodynamicTemperature]) { + pub fn update( + &mut self, + context: &UpdateContext, + pack_flow: MassRate, + duct_demand: &[ThermodynamicTemperature], + acsc_failure: bool, + ) { self.outlet_air.set_flow_rate(pack_flow); - let min_temp = duct_demand - .iter() - .fold(f64::INFINITY, |acc, &t| acc.min(t.get::())); + let unfiltered_outlet_temperature = if acsc_failure { + if matches!(self.pack_id, Pack(1)) { + 20. + } else { + 10. + } + } else { + duct_demand + .iter() + .fold(f64::INFINITY, |acc, &t| acc.min(t.get::())) + }; + self.outlet_temperature + .update(context.delta(), unfiltered_outlet_temperature); self.outlet_air - .set_temperature(ThermodynamicTemperature::new::(min_temp)); + .set_temperature(ThermodynamicTemperature::new::( + self.outlet_temperature.output(), + )); } } @@ -435,50 +553,45 @@ impl OutletAir for AirConditioningPack { } } -impl Default for AirConditioningPack { - fn default() -> Self { - Self::new() - } -} - pub struct TrimAirSystem { duct_temperature_id: [VariableIdentifier; ZONES], + + trim_air_pressure_regulating_valves: Vec, trim_air_valves: [TrimAirValve; ZONES], // These are not a real components of the system, but a tool to simulate the mixing of air pack_mixer_container: PneumaticPipe, trim_air_mixers: [MixerUnit<1>; ZONES], + + duct_high_pressure: Failure, outlet_air: Air, } impl TrimAirSystem { - pub fn new(context: &mut InitContext, cabin_zone_ids: &[ZoneType; ZONES]) -> Self { - let duct_temperature_id = cabin_zone_ids + pub fn new( + context: &mut InitContext, + cabin_zone_ids: &[ZoneType; ZONES], + taprv_ids: &[usize], + ) -> Self { + let duct_temperature_id = + cabin_zone_ids.map(|id| context.get_identifier(format!("COND_{}_DUCT_TEMP", id))); + let trim_air_pressure_regulating_valves = taprv_ids .iter() - .map(|id| context.get_identifier(format!("COND_{}_DUCT_TEMP", id))) - .collect::>() - .try_into() - .unwrap_or_else(|v: Vec| { - panic!("Expected a Vec of length {} but it was {}", ZONES, v.len()) - }); - - let trim_air_valves = cabin_zone_ids - .iter() - .map(|id| TrimAirValve::new(context, id)) - .collect::>() - .try_into() - .unwrap_or_else(|v: Vec| { - panic!("Expected a Vec of length {} but it was {}", ZONES, v.len()) - }); + .map(|id| TrimAirPressureRegulatingValve::new(*id)) + .collect::>(); Self { duct_temperature_id, - trim_air_valves, + + trim_air_pressure_regulating_valves, + trim_air_valves: cabin_zone_ids.map(|id| TrimAirValve::new(context, &id)), pack_mixer_container: PneumaticPipe::new( Volume::new::(4.), Pressure::new::(14.7), ThermodynamicTemperature::new::(15.), ), trim_air_mixers: [MixerUnit::new(&[ZoneType::Cabin(1)]); ZONES], + + duct_high_pressure: Failure::new(FailureType::TrimAirHighPressure), outlet_air: Air::new(), } } @@ -487,13 +600,37 @@ impl TrimAirSystem { &mut self, context: &UpdateContext, mixer_air: &MixerUnit, - tav_controller: &AirConditioningSystemController, + taprv_controller: &[&impl ControllerSignal], + tav_controller: &[&impl TrimAirControllers], ) { + self.trim_air_pressure_regulating_valves + .iter_mut() + .for_each(|taprv| { + taprv.update( + context, + &mut self.pack_mixer_container, + *taprv_controller + .iter() + .min_by_key(|signal| { + signal + .signal() + .unwrap_or_default() + .target_open_amount() + .get::() as u64 + }) + .unwrap(), + ) + }); + + // Fixme: A380 will need to take both TAPRV for (id, tav) in self.trim_air_valves.iter_mut().enumerate() { tav.update( context, - &mut self.pack_mixer_container, - tav_controller.trim_air_valve_controllers(id), + self.trim_air_pressure_regulating_valves + .iter() + .any(|taprv| taprv.is_open()), + &mut self.trim_air_pressure_regulating_valves[0], + tav_controller[id].trim_air_valve_controllers(id), ); self.trim_air_mixers[id].update(vec![tav, &mixer_air.mixer_unit_individual_outlet(id)]); } @@ -505,8 +642,13 @@ impl TrimAirSystem { .map(|tam| tam.outlet_air.flow_rate()) .sum(); self.outlet_air.set_flow_rate(total_flow); - self.outlet_air - .set_pressure(self.trim_air_outlet_pressure()); + + if self.duct_high_pressure.is_active() { + self.outlet_air.set_pressure(Pressure::new::(22.)); + } else { + self.outlet_air + .set_pressure(self.trim_air_outlet_pressure()); + } } pub fn mix_packs_air_update(&mut self, pack_container: &mut [impl PneumaticContainer; 2]) { @@ -536,6 +678,26 @@ impl TrimAirSystem { .average() } + pub fn trim_air_valve_has_fault(&self, tav_id: usize) -> bool { + self.trim_air_valves[tav_id].trim_air_valve_has_fault() + } + + fn any_trim_air_valve_has_fault(&self) -> bool { + self.trim_air_valves + .iter() + .any(|tav| tav.trim_air_valve_has_fault()) + } + + pub fn trim_air_high_pressure(&self) -> bool { + self.outlet_air.pressure() > Pressure::new::(20.) + } + + fn trim_air_pressure_regulating_valve_is_open(&self) -> bool { + self.trim_air_pressure_regulating_valves + .iter() + .any(|taprv| taprv.is_open()) + } + #[cfg(test)] fn trim_air_valves_open_amount(&self) -> [Ratio; ZONES] { self.trim_air_valves @@ -560,7 +722,9 @@ impl DuctTemperature for TrimAirSystem impl SimulationElement for TrimAirSystem { fn accept(&mut self, visitor: &mut V) { + accept_iterable!(self.trim_air_pressure_regulating_valves, visitor); accept_iterable!(self.trim_air_valves, visitor); + self.duct_high_pressure.accept(visitor); visitor.visit(self); } @@ -572,12 +736,159 @@ impl SimulationElement for TrimAirSyst } } +/// Struct to simulate the travel time of the TAVs and the TAPRV +struct TrimAirValveTravelTime { + valve_open_command: Ratio, + travel_time: Duration, +} + +impl TrimAirValveTravelTime { + fn new(travel_time: Duration) -> Self { + Self { + valve_open_command: Ratio::default(), + travel_time, + } + } + + fn update( + &mut self, + context: &UpdateContext, + valve_open_amount: Ratio, + signal: &impl ControllerSignal, + ) { + self.valve_open_command = valve_open_amount; + if let Some(signal) = signal.signal() { + if self.valve_open_command < signal.target_open_amount() { + self.valve_open_command += + Ratio::new::(self.get_valve_change_for_delta(context)) + .min(signal.target_open_amount() - self.valve_open_command); + } else if self.valve_open_command > signal.target_open_amount() { + self.valve_open_command -= + Ratio::new::(self.get_valve_change_for_delta(context)) + .min(self.valve_open_command - signal.target_open_amount()); + } + } + } + + fn get_valve_change_for_delta(&self, context: &UpdateContext) -> f64 { + 100. * (context.delta_as_secs_f64() / self.travel_time.as_secs_f64()) + } +} + +impl ControllerSignal for TrimAirValveTravelTime { + fn signal(&self) -> Option { + if self.valve_open_command > Ratio::default() { + Some(TrimAirValveSignal::new(self.valve_open_command)) + } else { + Some(TrimAirValveSignal::new_closed()) + } + } +} + +struct TrimAirPressureRegulatingValve { + trim_air_pressure_regulating_valve: DefaultValve, + taprv_travel_time: TrimAirValveTravelTime, + downstream: PneumaticPipe, + exhaust: PneumaticExhaust, + failure: Failure, +} + +impl TrimAirPressureRegulatingValve { + fn new(id: usize) -> Self { + Self { + trim_air_pressure_regulating_valve: DefaultValve::new_closed(), + taprv_travel_time: TrimAirValveTravelTime::new(Duration::from_secs(3)), + downstream: PneumaticPipe::new( + Volume::new::(4.), + Pressure::new::(14.7), + ThermodynamicTemperature::new::(15.), + ), + exhaust: PneumaticExhaust::new(0.1, 0.1, Pressure::default()), + failure: Failure::new(FailureType::HotAir(id)), + } + } + + fn update( + &mut self, + context: &UpdateContext, + from: &mut impl PneumaticContainer, + signal: &impl ControllerSignal, + ) { + // When a failure is active or there is no signal coming from the controller the TAPRV is unresponsive + self.taprv_travel_time.update( + context, + self.trim_air_pressure_regulating_valve.open_amount(), + signal, + ); + + if !self.failure.is_active() { + self.trim_air_pressure_regulating_valve + .update_open_amount(&self.taprv_travel_time); + } + + self.trim_air_pressure_regulating_valve.update_move_fluid( + context, + from, + &mut self.downstream, + ); + self.exhaust + .update_move_fluid(context, &mut self.downstream); + } + + fn is_open(&self) -> bool { + self.trim_air_pressure_regulating_valve.open_amount() > Ratio::new::(0.01) + } +} + +impl PneumaticContainer for TrimAirPressureRegulatingValve { + fn pressure(&self) -> Pressure { + self.downstream.pressure() + } + + fn volume(&self) -> Volume { + self.downstream.volume() + } + + fn temperature(&self) -> ThermodynamicTemperature { + self.downstream.temperature() + } + + fn mass(&self) -> Mass { + self.downstream.mass() + } + + fn change_fluid_amount( + &mut self, + fluid_amount: Mass, + fluid_temperature: ThermodynamicTemperature, + fluid_pressure: Pressure, + ) { + self.downstream + .change_fluid_amount(fluid_amount, fluid_temperature, fluid_pressure); + } + + fn update_temperature(&mut self, temperature_change: TemperatureInterval) { + self.downstream.update_temperature(temperature_change); + } +} + +impl SimulationElement for TrimAirPressureRegulatingValve { + fn accept(&mut self, visitor: &mut T) { + self.failure.accept(visitor); + visitor.visit(self); + } +} + struct TrimAirValve { trim_air_valve_id: VariableIdentifier, + trim_air_valve: DefaultValve, + trim_air_valve_travel_time: TrimAirValveTravelTime, trim_air_container: PneumaticPipe, exhaust: PneumaticExhaust, outlet_air: Air, + failure: Failure, + overheat: Failure, } impl TrimAirValve { @@ -587,7 +898,9 @@ impl TrimAirValve { Self { trim_air_valve_id: context .get_identifier(format!("COND_{}_TRIM_AIR_VALVE_POSITION", zone_id)), + trim_air_valve: DefaultValve::new_closed(), + trim_air_valve_travel_time: TrimAirValveTravelTime::new(Duration::from_secs(5)), trim_air_container: PneumaticPipe::new( Volume::new::(0.03), // Based on references Pressure::new::(14.7 + Self::PRESSURE_DIFFERENCE_WITH_CABIN_PSI), @@ -595,26 +908,47 @@ impl TrimAirValve { ), exhaust: PneumaticExhaust::new(5., 1., Pressure::new::(0.)), outlet_air: Air::new(), + failure: Failure::new(FailureType::TrimAirFault(*zone_id)), + overheat: Failure::new(FailureType::TrimAirOverheat(*zone_id)), } } fn update( &mut self, context: &UpdateContext, + trim_air_pressure_regulating_valve_open: bool, from: &mut impl PneumaticContainer, tav_controller: TrimAirValveController, ) { - self.trim_air_valve.update_open_amount(&tav_controller); + self.trim_air_valve_travel_time.update( + context, + self.trim_air_valve_open_amount(), + &tav_controller, + ); + + if !self.failure.is_active() { + self.trim_air_valve + .update_open_amount(&self.trim_air_valve_travel_time); + } self.trim_air_valve .update_move_fluid(context, from, &mut self.trim_air_container); self.exhaust .update_move_fluid(context, &mut self.trim_air_container); - self.outlet_air.set_temperature(from.temperature()); + if self.overheat.is_active() && trim_air_pressure_regulating_valve_open { + // When forcing overheat we inject high pressure high temperature air + self.outlet_air + .set_temperature(ThermodynamicTemperature::new::(200.)); + self.outlet_air + .set_flow_rate(MassRate::new::(0.8)); + } else { + self.outlet_air.set_temperature(from.temperature()); + self.outlet_air + .set_flow_rate(self.trim_air_valve_air_flow()); + } + self.outlet_air .set_pressure(self.trim_air_container.pressure()); - self.outlet_air - .set_flow_rate(self.trim_air_valve_air_flow()); } fn trim_air_valve_open_amount(&self) -> Ratio { @@ -624,6 +958,10 @@ impl TrimAirValve { fn trim_air_valve_air_flow(&self) -> MassRate { self.trim_air_valve.fluid_flow() } + + fn trim_air_valve_has_fault(&self) -> bool { + self.failure.is_active() + } } impl OutletAir for TrimAirValve { @@ -636,6 +974,12 @@ impl SimulationElement for TrimAirValve { fn write(&self, writer: &mut SimulatorWriter) { writer.write(&self.trim_air_valve_id, self.trim_air_valve_open_amount()); } + + fn accept(&mut self, visitor: &mut T) { + self.failure.accept(visitor); + self.overheat.accept(visitor); + visitor.visit(self); + } } #[derive(Clone, Copy)] @@ -693,6 +1037,12 @@ impl Air { } } +impl OutletAir for Air { + fn outlet_air(&self) -> Air { + *self + } +} + impl Default for Air { fn default() -> Self { Self::new() diff --git a/fbw-common/src/wasm/systems/systems/src/failures/mod.rs b/fbw-common/src/wasm/systems/systems/src/failures/mod.rs index e85aa6f5c47..5af4833d6f7 100644 --- a/fbw-common/src/wasm/systems/systems/src/failures/mod.rs +++ b/fbw-common/src/wasm/systems/systems/src/failures/mod.rs @@ -1,3 +1,4 @@ +use crate::air_conditioning::{acs_controller::AcscId, ZoneType}; use crate::shared::{ AirbusElectricPumpId, AirbusEngineDrivenPumpId, ElectricalBusType, GearActuatorId, HydraulicColor, LgciuId, ProximityDetectorId, @@ -6,6 +7,13 @@ use crate::simulation::SimulationElement; #[derive(Clone, Copy, PartialEq, Eq)] pub enum FailureType { + Acsc(AcscId), + CabinFan(usize), + HotAir(usize), + TrimAirOverheat(ZoneType), + TrimAirFault(ZoneType), + TrimAirHighPressure, + GalleyFans, Generator(usize), ApuGenerator(usize), TransformerRectifier(usize),