From 581edbca8c25bad8562274fef1370bbcbf9545c8 Mon Sep 17 00:00:00 2001 From: Miquel Juhe <60938089+mjuhe@users.noreply.github.com> Date: Sat, 21 Oct 2023 00:19:01 +0100 Subject: [PATCH] feat(cond): a380 temperature control and ventilation (#8086) * feat: trim air drive device and tcs application * feat: connect tadd and cpiom to air cond system * feat: improve local controller channels * feat: use a380 volume for the cabin zones * feat: ventilation control module * feat: cpiom b ventilation control system app * feat: cargo bulk ventilation * feat: forward cargo temperature control and tests * style: small refactor and style improvements * refactor: variable trim air system volume * feat: system outputs * fix: power source for HP fans * fix: wrong vcm updating aft fans * fix: merge conflicts * fix: review comments * refactor: small final style updates * refactor: move local controllers to aircraft crate * fix: second round of comments * style: add comment to clarify duct_demand_temperature fn * fix: comment typo --- .../a320_systems/src/air_conditioning.rs | 52 +- fbw-a380x/docs/a380-simvars.md | 53 + .../src/air_conditioning/cpiom_b.rs | 326 ++- .../full_digital_agu_controller.rs | 105 +- .../air_conditioning/local_controllers/mod.rs | 3 + .../trim_air_drive_device.rs | 175 ++ .../ventilation_control_module.rs | 249 +++ .../a380_systems/src/air_conditioning/mod.rs | 1818 +++++++++++++++-- .../systems/a380_systems/src/hydraulic/mod.rs | 13 +- .../src/wasm/systems/a380_systems/src/lib.rs | 1 + .../systems/a380_systems/src/pneumatic.rs | 18 +- .../src/air_conditioning/acs_controller.rs | 220 +- .../systems/src/air_conditioning/cabin_air.rs | 106 +- .../cabin_pressure_controller.rs | 7 +- .../systems/src/air_conditioning/mod.rs | 320 ++- .../systems/src/hydraulic/cargo_doors.rs | 2 +- .../wasm/systems/systems/src/shared/mod.rs | 5 + 17 files changed, 3057 insertions(+), 416 deletions(-) rename {fbw-common/src/wasm/systems/systems/src/air_conditioning => fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers}/full_digital_agu_controller.rs (81%) create mode 100644 fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/mod.rs create mode 100644 fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/trim_air_drive_device.rs create mode 100644 fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/ventilation_control_module.rs 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 26d2df13102..a6ce6e2278f 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 @@ -8,7 +8,7 @@ use systems::{ AdirsToAirCondInterface, Air, AirConditioningOverheadShared, AirConditioningPack, CabinFan, Channel, DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, OverheadFlowSelector, PackFlowControllers, PressurizationConstants, PressurizationOverheadShared, TrimAirSystem, - ZoneType, + VcmShared, ZoneType, }, overhead::{ AutoManFaultPushButton, NormalOnPushButton, OnOffFaultPushButton, OnOffPushButton, @@ -33,7 +33,7 @@ use systems::{ use std::time::Duration; use uom::si::{ f64::*, pressure::hectopascal, ratio::percent, thermodynamic_temperature::degree_celsius, - velocity::knot, + velocity::knot, volume::cubic_meter, volume_rate::liter_per_second, }; use crate::payload::A320Pax; @@ -177,7 +177,7 @@ impl A320Cabin { fn update( &mut self, context: &UpdateContext, - air_conditioning_system: &(impl OutletAir + DuctTemperature), + air_conditioning_system: &(impl OutletAir + DuctTemperature + VcmShared), lgciu: [&impl LgciuWeightOnWheels; 2], number_of_passengers: &impl NumberOfPassengers, pressurization: &A320PressurizationSystem, @@ -252,6 +252,8 @@ pub struct A320AirConditioningSystem { } impl A320AirConditioningSystem { + const CAB_FAN_DESIGN_FLOW_RATE_L_S: f64 = 325.; // litres/sec + pub(crate) fn new(context: &mut InitContext, cabin_zones: &[ZoneType; 3]) -> Self { Self { acs_interface: [ @@ -299,15 +301,29 @@ impl A320AirConditioningSystem { ), ], cabin_fans: [ - CabinFan::new(1, ElectricalBusType::AlternatingCurrent(1)), - CabinFan::new(2, ElectricalBusType::AlternatingCurrent(2)), + CabinFan::new( + 1, + VolumeRate::new::(Self::CAB_FAN_DESIGN_FLOW_RATE_L_S), + ElectricalBusType::AlternatingCurrent(1), + ), + CabinFan::new( + 2, + VolumeRate::new::(Self::CAB_FAN_DESIGN_FLOW_RATE_L_S), + ElectricalBusType::AlternatingCurrent(2), + ), ], mixer_unit: MixerUnit::new(cabin_zones), packs: [ - AirConditioningPack::new(Pack(1)), - AirConditioningPack::new(Pack(2)), + AirConditioningPack::new(context, Pack(1)), + AirConditioningPack::new(context, Pack(2)), ], - trim_air_system: TrimAirSystem::new(context, cabin_zones, &[1]), + trim_air_system: TrimAirSystem::new( + context, + cabin_zones, + &[1], + Volume::new::(4.), + Volume::new::(0.03), + ), air_conditioning_overhead: A320AirConditioningSystemOverhead::new(context, cabin_zones), } @@ -424,9 +440,9 @@ impl A320AirConditioningSystem { 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].trim_air_pressure_regulating_valve_controller(), + self.acsc[1].trim_air_pressure_regulating_valve_controller(), ], &[&self.acsc[0], &self.acsc[1], &self.acsc[1]], ); @@ -481,15 +497,21 @@ impl OutletAir for A320AirConditioningSystem { outlet_air.set_temperature(self.duct_temperature().iter().average()); outlet_air + + // TODO: This should use self.trim_air_system.outlet_air() } } +// This is not used in the A320 +impl VcmShared for A320AirConditioningSystem {} + impl SimulationElement for A320AirConditioningSystem { fn accept(&mut self, visitor: &mut V) { accept_iterable!(self.acs_interface, visitor); accept_iterable!(self.acsc, visitor); self.trim_air_system.accept(visitor); accept_iterable!(self.cabin_fans, visitor); + accept_iterable!(self.packs, visitor); self.air_conditioning_overhead.accept(visitor); @@ -550,7 +572,7 @@ impl AirConditioningSystemInterfaceUnit { 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()); + .set_bit(23, acs_overhead.hot_air_pushbutton_is_on(1)); 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()); @@ -637,7 +659,7 @@ impl AirConditioningOverheadShared self.pack_pbs.iter().map(|pack| pack.is_on()).collect() } - fn hot_air_pushbutton_is_on(&self) -> bool { + fn hot_air_pushbutton_is_on(&self, _hot_air_id: usize) -> bool { self.hot_air_pb.is_on() } @@ -828,8 +850,10 @@ struct A320PressurizationConstants; impl PressurizationConstants for A320PressurizationConstants { // Volume data from A320 AIRCRAFT CHARACTERISTICS - AIRPORT AND MAINTENANCE PLANNING - const CABIN_VOLUME_CUBIC_METER: f64 = 139.; // m3 + const CABIN_ZONE_VOLUME_CUBIC_METER: f64 = 139.; // m3 const COCKPIT_VOLUME_CUBIC_METER: f64 = 9.; // m3 + const FWD_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 0.; // m3 Not used in A320 + const BULK_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 0.; // m3 Not used in A320 const PRESSURIZED_FUSELAGE_VOLUME_CUBIC_METER: f64 = 330.; // m3 const CABIN_LEAKAGE_AREA: f64 = 0.0003; // m2 const OUTFLOW_VALVE_SIZE: f64 = 0.05; // m2 diff --git a/fbw-a380x/docs/a380-simvars.md b/fbw-a380x/docs/a380-simvars.md index dd42480ce5f..a8c8f62ad9f 100644 --- a/fbw-a380x/docs/a380-simvars.md +++ b/fbw-a380x/docs/a380-simvars.md @@ -92,6 +92,8 @@ - UPPER_DECK_5 - UPPER_DECK_6 - UPPER_DECK_7 + - CARGO_FWD + - CARGO_BULK - A32NX_COND_{id}_DUCT_TEMP - Degree Celsius @@ -99,12 +101,63 @@ - {id} - Same as A32NX_COND_{id}_TEMP +- A32NX_COND_PACK_{id}_IS_OPERATING + - Bool + - True when the pack operates normally (at least one FCV is open) + - {id} 1 or 2 + +- A32NX_COND_PACK_{id}_OUTLET_TEMPERATURE + - Degree Celsius + - Outlet temperature of the packs + - {id} 1 or 2 + - A32NX_COND_{id}_TRIM_AIR_VALVE_POSITION - Percentage - Percentage opening of each trim air valve (hot air) - {id} - Same as A32NX_COND_{id}_TEMP +- A32NX_COND_HOT_AIR_VALVE_{id}_IS_ENABLED + - Bool + - True if the hot air valve {1 or 2} is enabled + +- A32NX_COND_HOT_AIR_VALVE_{id}_IS_OPEN + - Bool + - True if the hot air valve {1 or 2} is open + +- A32NX_COND_TADD_CHANNEL_FAILURE + - Number + - 0 if no failure + - 1 or 2 if single channel failure (for failed channel id) + - 3 if dual channel failure + +- A32NX_VENT_PRIMARY_FANS_ENABLED + - Bool + - True if the primary (high pressure) fans are enabled and operating normally + +- A32NX_VENT_{id}_EXTRACTION_FAN_ON + - Bool + - True when the the extraction fans of the fwd/bulk cargo compartment operate normally + - {id} + - FWD + - BULK + +- A32NX_VENT_{id}_ISOLATION_VALVE_OPEN + - Bool + - True when the isolation valves of the fwd/bulk cargo compartment are open + - {id} + - FWD + - BULK + +- A32NX_VENT_{id}_VCM_CHANNEL_FAILURE + - Number + - 0 if no failure + - 1 or 2 if single channel failure (for failed channel id) + - 3 if dual channel failure + - {id} + - FWD + - AFT + - A32NX_OVHD_COND_{id}_SELECTOR_KNOB - Number (0 to 300) - Rotation amount of the overhead temperature selectors for the cockpit and the cabin diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/cpiom_b.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/cpiom_b.rs index efc2428d1c5..6f3da9f1928 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/cpiom_b.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/cpiom_b.rs @@ -2,13 +2,14 @@ use std::time::Duration; use systems::{ air_conditioning::{ - acs_controller::{AirConditioningStateManager, Pack}, - AdirsToAirCondInterface, AirConditioningOverheadShared, OverheadFlowSelector, PackFlow, + acs_controller::{AcscId, AirConditioningStateManager, Pack, ZoneController}, + AdirsToAirCondInterface, AirConditioningOverheadShared, BulkHeaterSignal, CabinFansSignal, + Channel, DuctTemperature, OverheadFlowSelector, PackFlow, VcmShared, ZoneType, }, integrated_modular_avionics::core_processing_input_output_module::CoreProcessingInputOutputModule, shared::{ - CabinAltitude, EngineCorrectedN1, EngineStartState, LgciuWeightOnWheels, - PackFlowValveState, PneumaticBleed, + CabinAltitude, CabinSimulation, CargoDoorLocked, ControllerSignal, EngineCorrectedN1, + EngineStartState, LgciuWeightOnWheels, PackFlowValveState, PneumaticBleed, }, simulation::{ InitContext, SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext, @@ -16,29 +17,32 @@ use systems::{ }, }; +use super::local_controllers::trim_air_drive_device::TaddShared; + use uom::si::{ f64::*, length::foot, mass_rate::kilogram_per_second, ratio::{percent, ratio}, + thermodynamic_temperature::degree_celsius, }; pub(super) struct CoreProcessingInputOutputModuleB { cpiom_are_active: [bool; 4], ags_app: AirGenerationSystemApplication, - // temperature_control_system_app: TemperatureControlSystemApplication, - // ventilation_control_system_app: VentilationControlSystemApplication, + tcs_app: TemperatureControlSystemApplication, + vcs_app: VentilationControlSystemApplication, // cabin_pressure_control_system_app: CabinPressureControllSystemApplication, // avionics_ventilation_system_app: AvionicsVentilationSystemApplication, } impl CoreProcessingInputOutputModuleB { - pub(super) fn new(context: &mut InitContext) -> Self { + pub(super) fn new(context: &mut InitContext, cabin_zones: &[ZoneType; 18]) -> Self { Self { cpiom_are_active: [false; 4], ags_app: AirGenerationSystemApplication::new(context), - // temperature_control_system_app: TemperatureControlSystemApplication::new(), - // ventilation_control_system_app: VentilationControlSystemApplication::new(), + tcs_app: TemperatureControlSystemApplication::new(context, cabin_zones), + vcs_app: VentilationControlSystemApplication::new(context), // cabin_pressure_control_system_app: CabinPressureControllSystemApplication::new(), // avionics_ventilation_system_app: AvionicsVentilationSystemApplication::new(), } @@ -49,12 +53,15 @@ impl CoreProcessingInputOutputModuleB { context: &UpdateContext, adirs: &impl AdirsToAirCondInterface, acs_overhead: &impl AirConditioningOverheadShared, + cabin_temperature: &impl CabinSimulation, + cargo_door_open: &impl CargoDoorLocked, cpiom_b: [&CoreProcessingInputOutputModule; 4], engines: &[&impl EngineCorrectedN1], lgciu: [&impl LgciuWeightOnWheels; 2], number_of_passengers: usize, pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), pressurization: &impl CabinAltitude, + local_controllers: &(impl TaddShared + VcmShared), ) { self.cpiom_are_active = cpiom_b.map(|cpiom| cpiom.is_available()); @@ -70,8 +77,37 @@ impl CoreProcessingInputOutputModuleB { pneumatic, pressurization, ); + self.tcs_app.update( + context, + acs_overhead, + cabin_temperature, + self.cpiom_are_active.iter().any(|c| *c), + pressurization, + local_controllers, + ); + self.vcs_app.update( + acs_overhead, + self.cpiom_are_active[1] || self.cpiom_are_active[3], + cabin_temperature, + cargo_door_open, + lgciu, + local_controllers, + &self.ags_app, + ); } } + + pub(super) fn should_close_taprv(&self) -> [bool; 2] { + self.tcs_app.should_close_taprv() + } + + pub(super) fn hp_recirculation_fans_signal(&self) -> &impl ControllerSignal { + &self.vcs_app + } + + pub(super) fn bulk_heater_on_signal(&self) -> &impl ControllerSignal { + &self.vcs_app + } } impl PackFlow for CoreProcessingInputOutputModuleB { @@ -90,9 +126,17 @@ impl PackFlow for CoreProcessingInputOutputModuleB { } } +impl DuctTemperature for CoreProcessingInputOutputModuleB { + fn duct_demand_temperature(&self) -> Vec { + self.tcs_app.duct_demand_temperature() + } +} + impl SimulationElement for CoreProcessingInputOutputModuleB { fn accept(&mut self, visitor: &mut T) { self.ags_app.accept(visitor); + self.tcs_app.accept(visitor); + self.vcs_app.accept(visitor); visitor.visit(self); } @@ -101,12 +145,14 @@ impl SimulationElement for CoreProcessingInputOutputModuleB { /// Determines the pack flow demand and sends it to the FDAC for actuation of the valves struct AirGenerationSystemApplication { pack_flow_id: [VariableIdentifier; 4], + pack_operational_id: [VariableIdentifier; 2], aircraft_state: AirConditioningStateManager, fcv_timer_open: [Duration; 2], // One for each pack flow_demand_ratio: [Ratio; 2], // One for each pack flow_ratio: [Ratio; 4], // One for each FCV pack_flow_demand: [MassRate; 2], + pack_operating: [bool; 2], // One for each pack } impl AirGenerationSystemApplication { @@ -125,23 +171,20 @@ impl AirGenerationSystemApplication { fn new(context: &mut InitContext) -> Self { Self { pack_flow_id: Self::pack_flow_id(context), + pack_operational_id: [1, 2] + .map(|pack| context.get_identifier(format!("COND_PACK_{}_IS_OPERATING", pack))), aircraft_state: AirConditioningStateManager::new(), fcv_timer_open: [Duration::from_secs(0); 2], flow_demand_ratio: [Ratio::default(); 2], flow_ratio: [Ratio::default(); 4], pack_flow_demand: [MassRate::default(); 2], + pack_operating: [false; 2], } } fn pack_flow_id(context: &mut InitContext) -> [VariableIdentifier; 4] { - (1..=4) - .map(|fcv| context.get_identifier(format!("COND_PACK_FLOW_{}", fcv))) - .collect::>() - .try_into() - .unwrap_or_else(|v: Vec<_>| { - panic!("Expected a Vec of length {} but it was {}", 4, v.len()) - }) + [1, 2, 3, 4].map(|fcv| context.get_identifier(format!("COND_PACK_FLOW_{}", fcv))) } fn update( @@ -181,6 +224,12 @@ impl AirGenerationSystemApplication { self.update_timer(context, pneumatic); self.flow_ratio = self.actual_flow_percentage_calculation(pneumatic, pressurization); + + self.pack_operating = [[0, 1], [2, 3]].map(|flow_id| { + flow_id + .iter() + .any(|&f| self.flow_ratio[f] > Ratio::default()) + }); } fn ground_speed(&self, adirs: &impl AdirsToAirCondInterface) -> Option { @@ -208,7 +257,6 @@ impl AirGenerationSystemApplication { pack: Pack, pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), ) -> Ratio { - // TODO: Needs to account for the 4 positions of the A380 Selector let mut intermediate_flow: Ratio = acs_overhead.flow_selector_position().into(); // TODO: Add "insufficient performance" based on Pack Mixer Temperature Demand if self.pack_start_condition_determination(pack, pneumatic) { @@ -311,9 +359,253 @@ impl PackFlow for AirGenerationSystemApplication { impl SimulationElement for AirGenerationSystemApplication { fn write(&self, writer: &mut SimulatorWriter) { + self.pack_operational_id + .iter() + .zip(self.pack_operating) + .for_each(|(id, operating)| writer.write(id, operating)); self.pack_flow_id .iter() .zip(self.flow_ratio) .for_each(|(id, flow)| writer.write(id, flow)); } } + +struct TemperatureControlSystemApplication { + hot_air_is_enabled_id: [VariableIdentifier; 2], + hot_air_is_open_id: [VariableIdentifier; 2], + + zone_controllers: [ZoneController; 18], + hot_air_is_enabled: [bool; 2], + hot_air_is_open: [bool; 2], +} +impl TemperatureControlSystemApplication { + fn new(context: &mut InitContext, cabin_zones: &[ZoneType; 18]) -> Self { + let hot_air_variable_identifiers = Self::hot_air_id_init(context); + Self { + hot_air_is_enabled_id: hot_air_variable_identifiers[0], + hot_air_is_open_id: hot_air_variable_identifiers[1], + zone_controllers: cabin_zones.map(ZoneController::new), + hot_air_is_enabled: [false; 2], + hot_air_is_open: [false; 2], + } + } + + fn hot_air_id_init(context: &mut InitContext) -> [[VariableIdentifier; 2]; 2] { + [ + [1, 2].map(|id| format!("COND_HOT_AIR_VALVE_{}_IS_ENABLED", id)), + [1, 2].map(|id| format!("COND_HOT_AIR_VALVE_{}_IS_OPEN", id)), + ] + .map(|id_vec| id_vec.map(|st| context.get_identifier(st))) + } + + fn update( + &mut self, + context: &UpdateContext, + acs_overhead: &impl AirConditioningOverheadShared, + cabin_temperature: &impl CabinSimulation, + cpiom_b_powered: bool, + pressurization: &impl CabinAltitude, + trim_air_drive_device: &impl TaddShared, + ) { + for zone in self.zone_controllers.iter_mut() { + // Acsc is irrelevant for the A380 so we set it to 1 + zone.update( + context, + AcscId::Acsc1(Channel::ChannelOne), + acs_overhead, + cpiom_b_powered, + cabin_temperature.cabin_temperature(), + pressurization, + ); + } + + self.hot_air_is_enabled = [ + trim_air_drive_device.hot_air_is_enabled(1), + trim_air_drive_device.hot_air_is_enabled(2), + ]; + self.hot_air_is_open = [ + trim_air_drive_device.trim_air_pressure_regulating_valve_is_open(1), + trim_air_drive_device.trim_air_pressure_regulating_valve_is_open(2), + ]; + } + + fn should_close_taprv(&self) -> [bool; 2] { + // This signal is used when there is an overheat or leak detection + // At the moment we hard code it to false until failures are implemented + [false; 2] + } +} + +impl DuctTemperature for TemperatureControlSystemApplication { + fn duct_demand_temperature(&self) -> Vec { + self.zone_controllers + .iter() + .flat_map(|controller| controller.duct_demand_temperature()) + .collect() + } +} + +impl SimulationElement for TemperatureControlSystemApplication { + fn write(&self, writer: &mut SimulatorWriter) { + self.hot_air_is_enabled_id + .iter() + .zip(self.hot_air_is_enabled) + .for_each(|(id, is_enabled)| writer.write(id, is_enabled)); + self.hot_air_is_open_id + .iter() + .zip(self.hot_air_is_open) + .for_each(|(id, is_open)| writer.write(id, is_open)); + } +} + +struct VentilationControlSystemApplication { + fwd_extraction_fan_id: VariableIdentifier, + fwd_isolation_valve_id: VariableIdentifier, + bulk_extraction_fan_id: VariableIdentifier, + bulk_isolation_valve_id: VariableIdentifier, + primary_fans_enabled_id: VariableIdentifier, + + fwd_extraction_fan_is_on: bool, + fwd_isolation_valve_is_open: bool, + bulk_control_is_powered: bool, + bulk_extraction_fan_is_on: bool, + bulk_isolation_valve_is_open: bool, + hp_cabin_fans_are_enabled: bool, + hp_cabin_fans_flow_demand: MassRate, + should_switch_on_bulk_heater: bool, +} + +impl VentilationControlSystemApplication { + // This value is an assumption. Total mixed air per cabin occupant (A320 AMM): 9.9 g/s -> (for 517 occupants) 5.1183 kg/s + const TOTAL_MIXED_AIR_DEMAND: f64 = 5.1183; // kg/s + const NUMBER_OF_FANS: f64 = 4.; + + fn new(context: &mut InitContext) -> Self { + Self { + fwd_extraction_fan_id: context.get_identifier("VENT_FWD_EXTRACTION_FAN_ON".to_owned()), + fwd_isolation_valve_id: context + .get_identifier("VENT_FWD_ISOLATION_VALVE_OPEN".to_owned()), + bulk_extraction_fan_id: context + .get_identifier("VENT_BULK_EXTRACTION_FAN_ON".to_owned()), + bulk_isolation_valve_id: context + .get_identifier("VENT_BULK_ISOLATION_VALVE_OPEN".to_owned()), + primary_fans_enabled_id: context.get_identifier("VENT_PRIMARY_FANS_ENABLED".to_owned()), + + fwd_extraction_fan_is_on: false, + fwd_isolation_valve_is_open: false, + bulk_control_is_powered: false, + bulk_extraction_fan_is_on: false, + bulk_isolation_valve_is_open: false, + hp_cabin_fans_are_enabled: false, + hp_cabin_fans_flow_demand: MassRate::default(), + should_switch_on_bulk_heater: false, + } + } + + fn update( + &mut self, + acs_overhead: &impl AirConditioningOverheadShared, + bulk_control_is_powered: bool, + cabin_temperature: &impl CabinSimulation, + cargo_door_open: &impl CargoDoorLocked, + lgciu: [&impl LgciuWeightOnWheels; 2], + vcm_shared: &impl VcmShared, + pack_flow_demand: &impl PackFlow, + ) { + self.fwd_extraction_fan_is_on = vcm_shared.fwd_extraction_fan_is_on(); + self.fwd_isolation_valve_is_open = vcm_shared.fwd_isolation_valves_open_allowed(); + self.bulk_control_is_powered = bulk_control_is_powered; + self.bulk_extraction_fan_is_on = vcm_shared.bulk_extraction_fan_is_on(); + self.bulk_isolation_valve_is_open = + self.bulk_control_is_powered && vcm_shared.bulk_isolation_valves_open_allowed(); + self.hp_cabin_fans_are_enabled = vcm_shared.hp_cabin_fans_are_enabled(); + // The recirculation airflow demand is linked to the fresh airflow demand in order to keep the total airflow constant + self.hp_cabin_fans_flow_demand = self.recirculation_flow_determination( + acs_overhead, + pack_flow_demand.pack_flow_demand(Pack(1)) + pack_flow_demand.pack_flow_demand(Pack(2)), + ); + self.should_switch_on_bulk_heater = self.bulk_heater_on_determination( + acs_overhead, + cabin_temperature, + cargo_door_open, + lgciu, + vcm_shared, + ); + } + + fn recirculation_flow_determination( + &self, + acs_overhead: &impl AirConditioningOverheadShared, + pack_flow_demand: MassRate, + ) -> MassRate { + MassRate::new::( + Self::TOTAL_MIXED_AIR_DEMAND + * Ratio::from(acs_overhead.flow_selector_position()).get::() + - pack_flow_demand.get::(), + ) + } + + fn bulk_heater_on_determination( + &self, + acs_overhead: &impl AirConditioningOverheadShared, + cabin_temperature: &impl CabinSimulation, + cargo_door_open: &impl CargoDoorLocked, + lgciu: [&impl LgciuWeightOnWheels; 2], + vcm_shared: &impl VcmShared, + ) -> bool { + let bulk_heater_on_allowed = (cargo_door_open.aft_cargo_door_locked() + || !lgciu.iter().all(|a| a.left_and_right_gear_compressed(true))) + && vcm_shared.bulk_duct_heater_on_allowed(); + let temperature_difference = cabin_temperature.cabin_temperature()[ZoneType::Cargo(2).id()] + .get::() + - acs_overhead + .selected_cargo_temperature(ZoneType::Cargo(2)) + .get::(); + (temperature_difference < -1. + || (self.should_switch_on_bulk_heater && temperature_difference < 1.)) + && bulk_heater_on_allowed + } +} + +impl ControllerSignal for VentilationControlSystemApplication { + fn signal(&self) -> Option { + if !self.bulk_control_is_powered { + None + } else if self.hp_cabin_fans_are_enabled { + Some(CabinFansSignal::On(Some( + self.hp_cabin_fans_flow_demand / Self::NUMBER_OF_FANS, + ))) + } else { + None + } + } +} + +impl ControllerSignal for VentilationControlSystemApplication { + fn signal(&self) -> Option { + if self.should_switch_on_bulk_heater && self.bulk_control_is_powered { + Some(BulkHeaterSignal::On) + } else { + Some(BulkHeaterSignal::Off) + } + } +} + +impl SimulationElement for VentilationControlSystemApplication { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write(&self.fwd_extraction_fan_id, self.fwd_extraction_fan_is_on); + writer.write( + &self.fwd_isolation_valve_id, + self.fwd_isolation_valve_is_open, + ); + writer.write(&self.bulk_extraction_fan_id, self.bulk_extraction_fan_is_on); + writer.write( + &self.bulk_isolation_valve_id, + self.bulk_isolation_valve_is_open, + ); + writer.write( + &self.primary_fans_enabled_id, + self.hp_cabin_fans_are_enabled, + ); + } +} diff --git a/fbw-common/src/wasm/systems/systems/src/air_conditioning/full_digital_agu_controller.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/full_digital_agu_controller.rs similarity index 81% rename from fbw-common/src/wasm/systems/systems/src/air_conditioning/full_digital_agu_controller.rs rename to fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/full_digital_agu_controller.rs index 6f41cf832e6..9f050814aa4 100644 --- a/fbw-common/src/wasm/systems/systems/src/air_conditioning/full_digital_agu_controller.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/full_digital_agu_controller.rs @@ -1,16 +1,15 @@ -use crate::{ +use systems::{ + air_conditioning::{ + AirConditioningOverheadShared, OperatingChannel, PackFlow, PackFlowControllers, + PackFlowValveSignal, PressurizationOverheadShared, + }, pneumatic::{EngineState, PneumaticValveSignal}, shared::{ - pid::PidController, ControllerSignal, ElectricalBusType, ElectricalBuses, - EngineBleedPushbutton, EngineCorrectedN1, EngineFirePushButtons, EngineStartState, - PackFlowValveState, PneumaticBleed, + pid::PidController, ControllerSignal, ElectricalBusType, EngineBleedPushbutton, + EngineCorrectedN1, EngineFirePushButtons, EngineStartState, PackFlowValveState, + PneumaticBleed, }, - simulation::{SimulationElement, UpdateContext}, -}; - -use super::{ - AirConditioningOverheadShared, PackFlow, PackFlowControllers, PackFlowValveSignal, - PressurizationOverheadShared, + simulation::{SimulationElement, SimulationElementVisitor, UpdateContext}, }; use uom::si::{ @@ -19,18 +18,10 @@ use uom::si::{ ratio::{percent, ratio}, }; -// Future work this can be different types of failure. Dead code for now since `Fault` is never constructed -#[allow(dead_code)] -enum OperatingChannelFault { - NoFault, - Fault, -} - #[derive(Debug)] enum FdacFault { OneChannelFault, BothChannelsFault, - PowerLoss, } #[derive(Clone, Copy)] @@ -38,48 +29,22 @@ enum FcvFault { PositionDisagree, //More to be added } -enum OperatingChannel { - FDACChannelOne(OperatingChannelFault), - FDACChannelTwo(OperatingChannelFault), -} - -impl OperatingChannel { - fn has_fault(&self) -> bool { - matches!( - self, - OperatingChannel::FDACChannelOne(OperatingChannelFault::Fault) - | OperatingChannel::FDACChannelTwo(OperatingChannelFault::Fault) - ) - } - - fn switch(&mut self) { - // At the moment switching channels always clears the fault in the second channel - // TODO: This needs to be improved so we can have dual channel failures - *self = if matches!(self, OperatingChannel::FDACChannelOne(_)) { - OperatingChannel::FDACChannelTwo(OperatingChannelFault::NoFault) - } else { - OperatingChannel::FDACChannelOne(OperatingChannelFault::NoFault) - }; - } -} pub struct FullDigitalAGUController { active_channel: OperatingChannel, + stand_by_channel: OperatingChannel, flow_control: FDACFlowControl, // agu_control - powered_by: Vec, - is_powered: bool, fault: Option, } impl FullDigitalAGUController { - pub fn new(fdac_id: usize, powered_by: Vec) -> Self { + pub fn new(fdac_id: usize, powered_by: [ElectricalBusType; 2]) -> Self { Self { - active_channel: OperatingChannel::FDACChannelOne(OperatingChannelFault::NoFault), + active_channel: OperatingChannel::new(1, None, &[powered_by[0]]), + stand_by_channel: OperatingChannel::new(2, None, &[powered_by[1]]), flow_control: FDACFlowControl::new(fdac_id), // agu_control - powered_by, - is_powered: false, fault: None, } } @@ -98,10 +63,9 @@ impl FullDigitalAGUController { ) { self.fault_determination(); - if !matches!( - self.fault, - Some(FdacFault::PowerLoss) | Some(FdacFault::BothChannelsFault) - ) { + if !matches!(self.fault, Some(FdacFault::BothChannelsFault)) + && !self.active_channel.has_fault() + { self.flow_control.update( context, acs_overhead, @@ -117,20 +81,26 @@ impl FullDigitalAGUController { } fn fault_determination(&mut self) { - self.fault = { - if !self.is_powered { - Some(FdacFault::PowerLoss) - } else if self.active_channel.has_fault() { - self.active_channel.switch(); - if self.active_channel.has_fault() { - Some(FdacFault::BothChannelsFault) - } else { - Some(FdacFault::OneChannelFault) + self.active_channel.update_fault(); + self.stand_by_channel.update_fault(); + + self.fault = match ( + self.active_channel.has_fault(), + self.stand_by_channel.has_fault(), + ) { + (true, true) => Some(FdacFault::BothChannelsFault), + (false, false) => None, + (ac, _) => { + if ac { + self.switch_active_channel(); } - } else { - None + Some(FdacFault::OneChannelFault) } - } + }; + } + + fn switch_active_channel(&mut self) { + std::mem::swap(&mut self.stand_by_channel, &mut self.active_channel); } pub fn fcv_status_determination(&self, fcv_id: usize) -> bool { @@ -153,8 +123,11 @@ impl PackFlowControllers for FullDigitalAGUController SimulationElement for FullDigitalAGUController { - fn receive_power(&mut self, buses: &impl ElectricalBuses) { - self.is_powered = self.powered_by.iter().any(|&p| buses.is_powered(p)); + fn accept(&mut self, visitor: &mut T) { + self.active_channel.accept(visitor); + self.stand_by_channel.accept(visitor); + + visitor.visit(self); } } diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/mod.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/mod.rs new file mode 100644 index 00000000000..c00516ee2be --- /dev/null +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/mod.rs @@ -0,0 +1,3 @@ +pub mod full_digital_agu_controller; +pub mod trim_air_drive_device; +pub mod ventilation_control_module; diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/trim_air_drive_device.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/trim_air_drive_device.rs new file mode 100644 index 00000000000..691ae1daf8a --- /dev/null +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/trim_air_drive_device.rs @@ -0,0 +1,175 @@ +use systems::{ + air_conditioning::{ + acs_controller::{TrimAirPressureRegulatingValveController, TrimAirValveController}, + AirConditioningOverheadShared, DuctTemperature, OperatingChannel, TrimAirControllers, + TrimAirSystem, + }, + shared::{ElectricalBusType, EngineStartState, PackFlowValveState, PneumaticBleed}, + simulation::{ + InitContext, SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext, + VariableIdentifier, Write, + }, +}; + +#[derive(Debug)] +enum TaddFault { + OneChannelFault, + BothChannelsFault, +} + +pub trait TaddShared { + fn hot_air_is_enabled(&self, hot_air_id: usize) -> bool; + fn trim_air_pressure_regulating_valve_is_open(&self, taprv_id: usize) -> bool; +} + +pub struct TrimAirDriveDevice { + tadd_channel_failure_id: VariableIdentifier, + + active_channel: OperatingChannel, + stand_by_channel: OperatingChannel, + hot_air_is_enabled: [bool; 2], + hot_air_is_open: [bool; 2], + taprv_controllers: [TrimAirPressureRegulatingValveController; 2], + trim_air_valve_controllers: [TrimAirValveController; ZONES], + + fault: Option, +} + +impl TrimAirDriveDevice { + pub fn new(context: &mut InitContext, powered_by: [ElectricalBusType; 2]) -> Self { + Self { + tadd_channel_failure_id: context.get_identifier("COND_TADD_CHANNEL_FAILURE".to_owned()), + + active_channel: OperatingChannel::new(1, None, &[powered_by[0]]), + stand_by_channel: OperatingChannel::new(2, None, &[powered_by[1]]), + hot_air_is_enabled: [false; 2], + hot_air_is_open: [false; 2], + taprv_controllers: [TrimAirPressureRegulatingValveController::new(); 2], + trim_air_valve_controllers: [TrimAirValveController::new(); ZONES], + + fault: None, + } + } + + pub fn update( + &mut self, + context: &UpdateContext, + acs_overhead: &impl AirConditioningOverheadShared, + duct_demand_temperature: &impl DuctTemperature, + duct_temperature: &impl DuctTemperature, + pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), + should_close_taprv: [bool; 2], + trim_air_system: &TrimAirSystem, + ) { + self.fault_determination(); + + self.hot_air_is_enabled = [1, 2].map(|id| { + self.trim_air_pressure_regulating_valve_status_determination( + acs_overhead, + should_close_taprv[id - 1], + id, + pneumatic, + ) + }); + + self.taprv_controllers + .iter_mut() + .enumerate() + .for_each(|(id, controller)| controller.update(self.hot_air_is_enabled[id])); + + self.hot_air_is_open = + [1, 2].map(|id| trim_air_system.trim_air_pressure_regulating_valve_is_open(id)); + + if !matches!(self.fault, Some(TaddFault::BothChannelsFault)) + && !self.active_channel.has_fault() + { + for (id, tav_controller) in self.trim_air_valve_controllers.iter_mut().enumerate() { + tav_controller.update( + context, + self.hot_air_is_open.iter().any(|&hot_air| hot_air), + duct_temperature.duct_temperature()[id], + duct_demand_temperature.duct_demand_temperature()[id], + ) + } + } + } + + fn fault_determination(&mut self) { + self.active_channel.update_fault(); + self.stand_by_channel.update_fault(); + + self.fault = match ( + self.active_channel.has_fault(), + self.stand_by_channel.has_fault(), + ) { + (true, true) => Some(TaddFault::BothChannelsFault), + (false, false) => None, + (ac, _) => { + if ac { + self.switch_active_channel(); + } + Some(TaddFault::OneChannelFault) + } + }; + } + + fn switch_active_channel(&mut self) { + std::mem::swap(&mut self.stand_by_channel, &mut self.active_channel); + } + + fn trim_air_pressure_regulating_valve_status_determination( + &self, + acs_overhead: &impl AirConditioningOverheadShared, + should_close_taprv: bool, + hot_air_id: usize, + pneumatic: &impl PackFlowValveState, + ) -> bool { + acs_overhead.hot_air_pushbutton_is_on(hot_air_id) + && !self.active_channel.has_fault() + && (pneumatic.pack_flow_valve_is_open(1) || pneumatic.pack_flow_valve_is_open(2)) + && !should_close_taprv + // && !self.duct_overheat_monitor() + // && !any_tav_has_fault + } + + pub fn taprv_controller(&self) -> [TrimAirPressureRegulatingValveController; 2] { + self.taprv_controllers + } +} + +impl TaddShared for TrimAirDriveDevice { + fn hot_air_is_enabled(&self, hot_air_id: usize) -> bool { + self.hot_air_is_enabled[hot_air_id - 1] + } + fn trim_air_pressure_regulating_valve_is_open(&self, taprv_id: usize) -> bool { + self.hot_air_is_open[taprv_id - 1] + } +} + +impl TrimAirControllers + for TrimAirDriveDevice +{ + fn trim_air_valve_controllers(&self, zone_id: usize) -> TrimAirValveController { + self.trim_air_valve_controllers[zone_id] + } +} + +impl SimulationElement + for TrimAirDriveDevice +{ + fn write(&self, writer: &mut SimulatorWriter) { + let failure_count = match self.fault { + None => 0, + Some(TaddFault::OneChannelFault) => self.stand_by_channel.id().into(), + Some(TaddFault::BothChannelsFault) => 3, + }; + writer.write(&self.tadd_channel_failure_id, failure_count); + } + + fn accept(&mut self, visitor: &mut T) { + self.active_channel.accept(visitor); + self.stand_by_channel.accept(visitor); + + visitor.visit(self); + } +} diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/ventilation_control_module.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/ventilation_control_module.rs new file mode 100644 index 00000000000..4e79c4d936e --- /dev/null +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/air_conditioning/local_controllers/ventilation_control_module.rs @@ -0,0 +1,249 @@ +use std::fmt::Display; + +use systems::{ + air_conditioning::{ + AirConditioningOverheadShared, CabinFansSignal, OperatingChannel, + PressurizationOverheadShared, VcmShared, + }, + shared::{ControllerSignal, ElectricalBusType}, + simulation::{ + InitContext, SimulationElement, SimulationElementVisitor, SimulatorWriter, + VariableIdentifier, Write, + }, +}; + +#[derive(Debug)] +enum VcmFault { + OneChannelFault, + BothChannelsFault, +} + +#[derive(Clone, Copy, Debug)] +pub enum VcmId { + Fwd, + Aft, +} + +impl Display for VcmId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VcmId::Fwd => write!(f, "FWD"), + VcmId::Aft => write!(f, "AFT"), + } + } +} + +pub struct VentilationControlModule { + vcm_channel_failure_id: VariableIdentifier, + + id: VcmId, + active_channel: OperatingChannel, + stand_by_channel: OperatingChannel, + hp_cabin_fans_are_enabled: bool, + + // These are not separate systems in the aircraft + fcvcs: ForwardCargoVentilationControlSystem, + bvcs: BulkVentilationControlSystem, + + fault: Option, +} + +impl VentilationControlModule { + pub fn new(context: &mut InitContext, id: VcmId, powered_by: [ElectricalBusType; 2]) -> Self { + Self { + vcm_channel_failure_id: context + .get_identifier(format!("VENT_{}_VCM_CHANNEL_FAILURE", id)), + + id, + active_channel: OperatingChannel::new(1, None, &[powered_by[0]]), + stand_by_channel: OperatingChannel::new(2, None, &[powered_by[1]]), + hp_cabin_fans_are_enabled: false, + + fcvcs: ForwardCargoVentilationControlSystem::new(), + bvcs: BulkVentilationControlSystem::new(), + + fault: None, + } + } + + pub fn update( + &mut self, + acs_overhead: &impl AirConditioningOverheadShared, + pressurization_overhead: &impl PressurizationOverheadShared, + ) { + self.fault_determination(); + + self.hp_cabin_fans_are_enabled = !self.active_channel.has_fault() + && acs_overhead.cabin_fans_is_on() + && !pressurization_overhead.ditching_is_on(); + + if !self.active_channel.has_fault() { + if matches!(self.id, VcmId::Aft) { + self.bvcs.update( + self.active_channel.has_fault(), + acs_overhead, + pressurization_overhead, + ); + } else { + self.fcvcs.update( + self.active_channel.has_fault(), + acs_overhead, + pressurization_overhead, + ) + } + } + } + + fn fault_determination(&mut self) { + self.active_channel.update_fault(); + self.stand_by_channel.update_fault(); + + self.fault = match ( + self.active_channel.has_fault(), + self.stand_by_channel.has_fault(), + ) { + (true, true) => Some(VcmFault::BothChannelsFault), + (false, false) => None, + (ac, _) => { + if ac { + self.switch_active_channel(); + } + Some(VcmFault::OneChannelFault) + } + }; + } + + fn switch_active_channel(&mut self) { + std::mem::swap(&mut self.stand_by_channel, &mut self.active_channel); + } + + pub fn id(&self) -> VcmId { + self.id + } +} + +impl VcmShared for VentilationControlModule { + fn hp_cabin_fans_are_enabled(&self) -> bool { + self.hp_cabin_fans_are_enabled + } + fn fwd_extraction_fan_is_on(&self) -> bool { + self.fcvcs.fwd_extraction_fan_is_on() + } + fn fwd_isolation_valves_open_allowed(&self) -> bool { + self.fcvcs.fwd_isolation_valves_open_allowed() + } + fn bulk_duct_heater_on_allowed(&self) -> bool { + self.bvcs.duct_heater_on_allowed() + } + fn bulk_extraction_fan_is_on(&self) -> bool { + self.bvcs.bulk_extraction_fan_is_on() + } + fn bulk_isolation_valves_open_allowed(&self) -> bool { + self.bvcs.bulk_isolation_valves_open_allowed() + } +} + +impl ControllerSignal for VentilationControlModule { + fn signal(&self) -> Option { + if self.hp_cabin_fans_are_enabled { + Some(CabinFansSignal::On(None)) + } else { + Some(CabinFansSignal::Off) + } + } +} + +impl SimulationElement for VentilationControlModule { + fn write(&self, writer: &mut SimulatorWriter) { + let failure_count = match self.fault { + None => 0, + Some(VcmFault::OneChannelFault) => self.stand_by_channel.id().into(), + Some(VcmFault::BothChannelsFault) => 3, + }; + writer.write(&self.vcm_channel_failure_id, failure_count); + } + + fn accept(&mut self, visitor: &mut T) { + self.active_channel.accept(visitor); + self.stand_by_channel.accept(visitor); + + visitor.visit(self); + } +} + +struct ForwardCargoVentilationControlSystem { + extraction_fan_is_on: bool, + isolation_valves_open_allowed: bool, +} + +impl ForwardCargoVentilationControlSystem { + fn new() -> Self { + Self { + extraction_fan_is_on: false, + isolation_valves_open_allowed: false, + } + } + + fn update( + &mut self, + active_channel_has_fault: bool, + acs_overhead: &impl AirConditioningOverheadShared, + pressurization_overhead: &impl PressurizationOverheadShared, + ) { + // TODO: Add failures and smoke detection + self.isolation_valves_open_allowed = acs_overhead.fwd_cargo_isolation_valve_is_on() + && !pressurization_overhead.ditching_is_on() + && !active_channel_has_fault; + self.extraction_fan_is_on = + self.isolation_valves_open_allowed && !pressurization_overhead.ditching_is_on(); + } + + fn fwd_extraction_fan_is_on(&self) -> bool { + self.extraction_fan_is_on + } + fn fwd_isolation_valves_open_allowed(&self) -> bool { + self.isolation_valves_open_allowed + } +} + +struct BulkVentilationControlSystem { + duct_heater_on_allowed: bool, + extraction_fan_is_on: bool, + isolation_valves_open_allowed: bool, +} + +impl BulkVentilationControlSystem { + fn new() -> Self { + Self { + duct_heater_on_allowed: false, + isolation_valves_open_allowed: false, + extraction_fan_is_on: false, + } + } + + fn update( + &mut self, + active_channel_has_fault: bool, + acs_overhead: &impl AirConditioningOverheadShared, + pressurization_overhead: &impl PressurizationOverheadShared, + ) { + // TODO: Add failures and smoke detection + self.isolation_valves_open_allowed = acs_overhead.bulk_isolation_valve_is_on() + && !pressurization_overhead.ditching_is_on() + && !active_channel_has_fault; + self.extraction_fan_is_on = + self.isolation_valves_open_allowed && !pressurization_overhead.ditching_is_on(); + self.duct_heater_on_allowed = + acs_overhead.bulk_cargo_heater_is_on() && self.extraction_fan_is_on; + } + + fn duct_heater_on_allowed(&self) -> bool { + self.duct_heater_on_allowed + } + fn bulk_extraction_fan_is_on(&self) -> bool { + self.extraction_fan_is_on + } + fn bulk_isolation_valves_open_allowed(&self) -> bool { + self.isolation_valves_open_allowed + } +} 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 241d70ea80c..a501aa5397b 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,14 @@ use systems::{ accept_iterable, air_conditioning::{ - acs_controller::{AcscId, AirConditioningSystemController, Pack}, + acs_controller::Pack, cabin_air::CabinAirSimulation, cabin_pressure_controller::CabinPressureController, - full_digital_agu_controller::FullDigitalAGUController, pressure_valve::{OutflowValve, SafetyValve}, - AdirsToAirCondInterface, Air, AirConditioningOverheadShared, AirConditioningPack, CabinFan, - Channel, DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, OverheadFlowSelector, - PackFlow, PackFlowControllers, PressurizationConstants, PressurizationOverheadShared, - TrimAirSystem, ZoneType, + AdirsToAirCondInterface, Air, AirConditioningOverheadShared, AirConditioningPack, + AirHeater, CabinFan, DuctTemperature, MixerUnit, OutflowValveSignal, OutletAir, + OverheadFlowSelector, PackFlow, PackFlowControllers, PressurizationConstants, + PressurizationOverheadShared, TrimAirSystem, VcmShared, ZoneType, }, overhead::{ AutoManFaultPushButton, NormalOnPushButton, OnOffFaultPushButton, OnOffPushButton, @@ -17,10 +16,10 @@ use systems::{ }, pneumatic::PneumaticContainer, shared::{ - random_number, update_iterator::MaxStepLoop, AverageExt, CabinAltitude, CabinSimulation, - ControllerSignal, ElectricalBusType, EngineBleedPushbutton, EngineCorrectedN1, - EngineFirePushButtons, EngineStartState, LgciuWeightOnWheels, PackFlowValveState, - PneumaticBleed, + random_number, update_iterator::MaxStepLoop, CabinAltitude, CabinSimulation, + CargoDoorLocked, ControllerSignal, ElectricalBusType, EngineBleedPushbutton, + EngineCorrectedN1, EngineFirePushButtons, EngineStartState, LgciuWeightOnWheels, + PackFlowValveState, PneumaticBleed, }, simulation::{ InitContext, Read, SimulationElement, SimulationElementVisitor, SimulatorReader, @@ -31,14 +30,22 @@ use systems::{ use std::time::Duration; use uom::si::{ f64::*, pressure::hectopascal, ratio::percent, thermodynamic_temperature::degree_celsius, - velocity::knot, + velocity::knot, volume::cubic_meter, volume_rate::liter_per_second, }; use crate::avionics_data_communication_network::CoreProcessingInputOutputModuleShared; -use self::cpiom_b::CoreProcessingInputOutputModuleB; +use self::{ + cpiom_b::CoreProcessingInputOutputModuleB, + local_controllers::{ + full_digital_agu_controller::FullDigitalAGUController, + trim_air_drive_device::{TaddShared, TrimAirDriveDevice}, + ventilation_control_module::{VcmId, VentilationControlModule}, + }, +}; mod cpiom_b; +mod local_controllers; pub(super) struct A380AirConditioning { a380_cabin: A380Cabin, @@ -71,8 +78,8 @@ impl A380AirConditioning { ZoneType::Cabin(25), // UPPER_DECK_5 ZoneType::Cabin(26), // UPPER_DECK_6 ZoneType::Cabin(27), // UPPER_DECK_7 - ZoneType::Cargo(1), // FWD - ZoneType::Cargo(2), // BULK + ZoneType::Cargo(1), // CARGO_FWD + ZoneType::Cargo(2), // CARGO_BULK ]; Self { @@ -80,7 +87,7 @@ impl A380AirConditioning { a380_air_conditioning_system: A380AirConditioningSystem::new(context, &cabin_zones), a320_pressurization_system: A320PressurizationSystem::new(context), - cpiom_b: CoreProcessingInputOutputModuleB::new(context), + cpiom_b: CoreProcessingInputOutputModuleB::new(context, &cabin_zones), pressurization_updater: MaxStepLoop::new(Self::PRESSURIZATION_SIM_MAX_TIME_STEP), } @@ -90,6 +97,7 @@ impl A380AirConditioning { &mut self, context: &UpdateContext, adirs: &impl AdirsToAirCondInterface, + cargo_door_open: &impl CargoDoorLocked, cpiom_b: &impl CoreProcessingInputOutputModuleShared, engines: [&impl EngineCorrectedN1; 4], engine_fire_push_buttons: &impl EngineFirePushButtons, @@ -108,17 +116,19 @@ impl A380AirConditioning { adirs, self.a380_air_conditioning_system .air_conditioning_overhead(), + &self.a380_cabin, + cargo_door_open, cpiom, &engines, lgciu, self.a380_cabin.number_of_passengers(), pneumatic, &self.a320_pressurization_system, + &self.a380_air_conditioning_system, ); self.a380_air_conditioning_system.update( context, - adirs, &self.a380_cabin, &self.cpiom_b, engines, @@ -126,9 +136,7 @@ impl A380AirConditioning { self.a380_cabin.number_of_open_doors(), pneumatic, pneumatic_overhead, - &self.a320_pressurization_system, pressurization_overhead, - lgciu, ); // This is here due to the ADIRS updating at a different rate than the pressurization system @@ -210,7 +218,7 @@ struct A380Cabin { fwd_door_is_open: bool, rear_door_is_open: bool, number_of_passengers: [u8; 18], - cabin_air_simulation: CabinAirSimulation, + cabin_air_simulation: CabinAirSimulation, } impl A380Cabin { @@ -235,7 +243,7 @@ impl A380Cabin { fn update( &mut self, context: &UpdateContext, - air_conditioning_system: &(impl OutletAir + DuctTemperature), + air_conditioning_system: &(impl OutletAir + DuctTemperature + VcmShared), lgciu: [&impl LgciuWeightOnWheels; 2], pressurization: &A320PressurizationSystem, ) { @@ -303,9 +311,13 @@ impl SimulationElement for A380Cabin { } pub(super) struct A380AirConditioningSystem { - acsc: AirConditioningSystemController<18, 4>, + // Local controllers fdac: [FullDigitalAGUController<4>; 2], - cabin_fans: [CabinFan; 2], + tadd: TrimAirDriveDevice<18, 4>, + vcm: [VentilationControlModule; 2], + + cabin_fans: [CabinFan; 4], + cargo_air_heater: AirHeater, mixer_unit: MixerUnit<18>, // Temporary structure until packs are simulated packs: [AirConditioningPack; 2], @@ -315,49 +327,79 @@ pub(super) struct A380AirConditioningSystem { } impl A380AirConditioningSystem { + const CAB_FAN_DESIGN_FLOW_RATE_L_S: f64 = 550.; // litres/sec + fn new(context: &mut InitContext, cabin_zones: &[ZoneType; 18]) -> Self { Self { - 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 - ], - ], - ), fdac: [ FullDigitalAGUController::new( 1, - vec![ + [ ElectricalBusType::AlternatingCurrentEssential, // 403XP ElectricalBusType::AlternatingCurrent(2), // 117XP ], ), FullDigitalAGUController::new( 2, - vec![ + [ ElectricalBusType::AlternatingCurrentEssential, // 403XP ElectricalBusType::AlternatingCurrent(4), // 204XP ], ), ], - cabin_fans: [ - CabinFan::new(1, ElectricalBusType::AlternatingCurrent(1)), - CabinFan::new(2, ElectricalBusType::AlternatingCurrent(1)), + tadd: TrimAirDriveDevice::new( + context, + [ + ElectricalBusType::AlternatingCurrent(2), // 117XP + ElectricalBusType::AlternatingCurrent(4), // 206XP + ], + ), + vcm: [ + VentilationControlModule::new( + context, + VcmId::Fwd, + [ + ElectricalBusType::DirectCurrent(1), // 411PP + ElectricalBusType::DirectCurrentEssential, // 109PP + ], + ), + VentilationControlModule::new( + context, + VcmId::Aft, + [ + ElectricalBusType::DirectCurrent(2), // 214PP + ElectricalBusType::DirectCurrentEssential, // 109PP + ], + ), ], + + cabin_fans: [ + 1, // Left Hand - 100XP1 + 2, // Left Hand - 100XP2 + 3, // Right Hand - 200XP3 + 4, // Right Hand - 200XP4 + ] + .map(|id| { + CabinFan::new( + id, + VolumeRate::new::(Self::CAB_FAN_DESIGN_FLOW_RATE_L_S), + ElectricalBusType::AlternatingCurrent(id), + ) + }), + + cargo_air_heater: AirHeater::new(ElectricalBusType::AlternatingCurrent(2)), // 200XP4 mixer_unit: MixerUnit::new(cabin_zones), packs: [ - AirConditioningPack::new(Pack(1)), - AirConditioningPack::new(Pack(2)), + AirConditioningPack::new(context, Pack(1)), + AirConditioningPack::new(context, Pack(2)), ], - trim_air_system: TrimAirSystem::new(context, cabin_zones, &[1]), + trim_air_system: TrimAirSystem::new( + context, + cabin_zones, + &[1, 2], + Volume::new::(7.), + Volume::new::(0.2), + ), air_conditioning_overhead: A380AirConditioningSystemOverhead::new(context), } @@ -366,32 +408,51 @@ impl A380AirConditioningSystem { fn update( &mut self, context: &UpdateContext, - adirs: &impl AdirsToAirCondInterface, cabin_simulation: &impl CabinSimulation, - cpiom_b: &impl PackFlow, + cpiom_b: &CoreProcessingInputOutputModuleB, engines: [&impl EngineCorrectedN1; 4], engine_fire_push_buttons: &impl EngineFirePushButtons, number_of_open_doors: u8, pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), pneumatic_overhead: &impl EngineBleedPushbutton<4>, - pressurization: &impl CabinAltitude, pressurization_overhead: &A380PressurizationOverheadPanel, - lgciu: [&impl LgciuWeightOnWheels; 2], ) { - self.acsc.update( + self.update_local_controllers( context, - adirs, - &self.air_conditioning_overhead, - cabin_simulation, + cpiom_b, engines, engine_fire_push_buttons, + number_of_open_doors, pneumatic, - pressurization, + pneumatic_overhead, pressurization_overhead, - lgciu, - &self.trim_air_system, ); + self.update_fans(cabin_simulation, cpiom_b); + + self.update_packs(context, cpiom_b); + + self.update_mixer_unit(); + + self.update_trim_air_system(context); + + self.update_cargo_heater(cabin_simulation, cpiom_b); + + self.air_conditioning_overhead + .set_pack_pushbutton_fault(self.pack_fault_determination()); + } + + fn update_local_controllers( + &mut self, + context: &UpdateContext, + cpiom_b: &CoreProcessingInputOutputModuleB, + engines: [&impl EngineCorrectedN1; 4], + engine_fire_push_buttons: &impl EngineFirePushButtons, + number_of_open_doors: u8, + pneumatic: &(impl EngineStartState + PackFlowValveState + PneumaticBleed), + pneumatic_overhead: &impl EngineBleedPushbutton<4>, + pressurization_overhead: &A380PressurizationOverheadPanel, + ) { self.fdac.iter_mut().for_each(|controller| { controller.update( context, @@ -406,36 +467,72 @@ impl A380AirConditioningSystem { ) }); - for fan in self.cabin_fans.iter_mut() { - fan.update(cabin_simulation, &self.acsc.cabin_fans_controller()) - } + self.tadd.update( + context, + &self.air_conditioning_overhead, + cpiom_b, + &self.trim_air_system, + pneumatic, + cpiom_b.should_close_taprv(), + &self.trim_air_system, + ); + + self.vcm.iter_mut().for_each(|module| { + module.update(&self.air_conditioning_overhead, pressurization_overhead) + }); + } - 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() { + fn update_packs( + &mut self, + context: &UpdateContext, + cpiom_b: &CoreProcessingInputOutputModuleB, + ) { + for (pack, pack_flow) in self + .packs + .iter_mut() + .zip(self.fdac.iter().map(|fdac| fdac.pack_flow())) + { + // TODO: Failures pack.update( context, - pack_flow[id], - &duct_demand_temperature, - self.acsc.both_channels_failure(), + pack_flow, + &cpiom_b.duct_demand_temperature(), + false, ) } + } + 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); + } + fn update_trim_air_system(&mut self, context: &UpdateContext) { self.trim_air_system.update( context, &self.mixer_unit, - &[&self.acsc.trim_air_pressure_regulating_valve_controller(); 18], - &[&self.acsc; 18], + [ + self.tadd.taprv_controller()[0], + self.tadd.taprv_controller()[1], + ], + &[&self.tadd; 18], ); + } - self.air_conditioning_overhead - .set_pack_pushbutton_fault(self.pack_fault_determination()); + fn update_cargo_heater( + &mut self, + cabin_simulation: &impl CabinSimulation, + cpiom_b: &CoreProcessingInputOutputModuleB, + ) { + // For the bulk cargo, air flows from the LD and is warmed up by an electric heater + self.cargo_air_heater.update( + cabin_simulation, + &self.trim_air_system, + cpiom_b.bulk_heater_on_signal(), + ); } fn pack_fault_determination(&self) -> [bool; 2] { @@ -445,6 +542,36 @@ impl A380AirConditioningSystem { ] } + fn update_fans( + &mut self, + cabin_simulation: &impl CabinSimulation, + cpiom_b: &CoreProcessingInputOutputModuleB, + ) { + // The VCM FWD controls all LH recirculation fans and the VCM AFT controls all RH recirculation. + // The signal to update the fans comes from the CPIOM when the selector is in AUTO and from the VCM in the other positions + for (id, fan) in self.cabin_fans.iter_mut().enumerate() { + if cpiom_b.hp_recirculation_fans_signal().signal().is_some() { + fan.update(cabin_simulation, cpiom_b.hp_recirculation_fans_signal()); + } else if id < 2 { + fan.update( + cabin_simulation, + self.vcm + .iter() + .find(|module| matches!(module.id(), VcmId::Fwd)) + .expect("The Ventilation Control Module failed to find the required module for the recirculation fans"), + ) + } else { + fan.update( + cabin_simulation, + self.vcm + .iter() + .find(|module| matches!(module.id(), VcmId::Aft)) + .expect("The Ventilation Control Module failed to find the required module for the recirculation fans"), + ) + } + } + } + fn mix_packs_air_update(&mut self, pack_container: &mut [impl PneumaticContainer; 2]) { self.trim_air_system.mix_packs_air_update(pack_container); } @@ -465,28 +592,66 @@ impl PackFlowControllers for A380AirConditioningSystem { impl DuctTemperature for A380AirConditioningSystem { fn duct_temperature(&self) -> Vec { - self.trim_air_system.duct_temperature() + // The bulk cargo zone of the A380 is fed with recirculated air from the cabin flowing through the heater + let mut duct_temp_vec = self.trim_air_system.duct_temperature(); + duct_temp_vec[ZoneType::Cargo(2).id()] = self.cargo_air_heater.outlet_air().temperature(); + duct_temp_vec } } 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() + 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()); + self.trim_air_system.outlet_air() + } +} + +impl TaddShared for A380AirConditioningSystem { + fn hot_air_is_enabled(&self, hot_air_id: usize) -> bool { + self.tadd.hot_air_is_enabled(hot_air_id) + } + fn trim_air_pressure_regulating_valve_is_open(&self, taprv_id: usize) -> bool { + self.tadd + .trim_air_pressure_regulating_valve_is_open(taprv_id) + } +} - outlet_air +impl VcmShared for A380AirConditioningSystem { + fn hp_cabin_fans_are_enabled(&self) -> bool { + // If one of the VCMs is not returning cabin fans enabled we return false here + // This will force the VCMs to take control of the fans instead of the CPIOM B + self.vcm + .iter() + .all(|module| module.hp_cabin_fans_are_enabled()) + } + fn fwd_extraction_fan_is_on(&self) -> bool { + // The Fwd VCM controls the forward ventilation + self.vcm[0].fwd_extraction_fan_is_on() + } + fn fwd_isolation_valves_open_allowed(&self) -> bool { + self.vcm[0].fwd_isolation_valves_open_allowed() + } + fn bulk_duct_heater_on_allowed(&self) -> bool { + // The Aft VCM controls the bulk ventilation and heating + self.vcm[1].bulk_duct_heater_on_allowed() + } + fn bulk_extraction_fan_is_on(&self) -> bool { + self.vcm[1].bulk_extraction_fan_is_on() + } + fn bulk_isolation_valves_open_allowed(&self) -> bool { + self.vcm[1].bulk_isolation_valves_open_allowed() } } impl SimulationElement for A380AirConditioningSystem { fn accept(&mut self, visitor: &mut V) { - self.acsc.accept(visitor); accept_iterable!(self.fdac, visitor); + self.tadd.accept(visitor); + accept_iterable!(self.vcm, visitor); + self.trim_air_system.accept(visitor); accept_iterable!(self.cabin_fans, visitor); + accept_iterable!(self.packs, visitor); + self.cargo_air_heater.accept(visitor); self.air_conditioning_overhead.accept(visitor); @@ -526,8 +691,8 @@ impl A380AirConditioningSystemOverhead { OnOffFaultPushButton::new_on(context, "COND_HOT_AIR_2"), ], temperature_selectors: [ - ValueKnob::new_with_value(context, "COND_CKPT_SELECTOR", 24.), - ValueKnob::new_with_value(context, "COND_CABIN_SELECTOR", 24.), + ValueKnob::new_with_value(context, "COND_CKPT_SELECTOR", 150.), + ValueKnob::new_with_value(context, "COND_CABIN_SELECTOR", 150.), ], ram_air_pb: OnOffPushButton::new_off(context, "COND_RAM_AIR"), pack_pbs: [ @@ -544,8 +709,8 @@ impl A380AirConditioningSystemOverhead { OnOffFaultPushButton::new_on(context, "CARGO_AIR_ISOL_VALVES_BULK"), ], cargo_temperature_regulators: [ - ValueKnob::new_with_value(context, "CARGO_AIR_FWD_SELECTOR", 15.), - ValueKnob::new_with_value(context, "CARGO_AIR_BULK_SELECTOR", 15.), + ValueKnob::new_with_value(context, "CARGO_AIR_FWD_SELECTOR", 150.), + ValueKnob::new_with_value(context, "CARGO_AIR_BULK_SELECTOR", 150.), ], cargo_heater_pb: OnOffFaultPushButton::new_on(context, "CARGO_AIR_HEATER"), } @@ -562,7 +727,7 @@ impl A380AirConditioningSystemOverhead { impl AirConditioningOverheadShared for A380AirConditioningSystemOverhead { fn selected_cabin_temperature(&self, zone_id: usize) -> ThermodynamicTemperature { // The A380 has 16 cabin zones but only one knob - let knob = if zone_id > 1 { + let knob = if zone_id > 0 { &self.temperature_selectors[1] } else { &self.temperature_selectors[0] @@ -571,13 +736,22 @@ impl AirConditioningOverheadShared for A380AirConditioningSystemOverhead { ThermodynamicTemperature::new::(knob.value() * 0.04 + 18.) } + fn selected_cargo_temperature(&self, zone_id: ZoneType) -> ThermodynamicTemperature { + let knob = if matches!(zone_id, ZoneType::Cargo(1)) { + &self.cargo_temperature_regulators[0] + } else { + &self.cargo_temperature_regulators[1] + }; + // Map from knob range 0-300 to 5-25 degrees C + ThermodynamicTemperature::new::(knob.value() * 0.0667 + 5.) + } + fn pack_pushbuttons_state(&self) -> Vec { self.pack_pbs.iter().map(|pack| pack.is_on()).collect() } - fn hot_air_pushbutton_is_on(&self) -> bool { - // FIXME: Temporary solution until A380 air cond is implemented - self.hot_air_pbs[0].is_on() || self.hot_air_pbs[1].is_on() + fn hot_air_pushbutton_is_on(&self, hot_air_id: usize) -> bool { + self.hot_air_pbs[hot_air_id - 1].is_on() } fn cabin_fans_is_on(&self) -> bool { @@ -587,6 +761,18 @@ impl AirConditioningOverheadShared for A380AirConditioningSystemOverhead { fn flow_selector_position(&self) -> OverheadFlowSelector { self.flow_selector } + + fn fwd_cargo_isolation_valve_is_on(&self) -> bool { + self.isol_valves_pbs[0].is_on() + } + + fn bulk_isolation_valve_is_on(&self) -> bool { + self.isol_valves_pbs[1].is_on() + } + + fn bulk_cargo_heater_is_on(&self) -> bool { + self.cargo_heater_pb.is_on() + } } impl SimulationElement for A380AirConditioningSystemOverhead { @@ -620,7 +806,7 @@ impl SimulationElement for A380AirConditioningSystemOverhead { struct A320PressurizationSystem { active_cpc_sys_id: VariableIdentifier, - cpc: [CabinPressureController; 2], + cpc: [CabinPressureController; 2], outflow_valve: [OutflowValve; 1], // Array to prepare for more than 1 outflow valve in A380 safety_valve: SafetyValve, residual_pressure_controller: ResidualPressureController, @@ -769,12 +955,17 @@ impl SimulationElement for A320PressurizationSystem { } } -struct A320PressurizationConstants; - -impl PressurizationConstants for A320PressurizationConstants { - // Volume data from A320 AIRCRAFT CHARACTERISTICS - AIRPORT AND MAINTENANCE PLANNING - const CABIN_VOLUME_CUBIC_METER: f64 = 139.; // m3 - const COCKPIT_VOLUME_CUBIC_METER: f64 = 9.; // m3 +struct A380PressurizationConstants; + +impl PressurizationConstants for A380PressurizationConstants { + // Volume data from A380 AIRCRAFT CHARACTERISTICS - AIRPORT AND MAINTENANCE PLANNING + // Not all cabin zones have the exact same volume. Main deck 775 m3, upper deck 530 m3. + // For now we average it as an approximation + const CABIN_ZONE_VOLUME_CUBIC_METER: f64 = 86.3; // m3 + const COCKPIT_VOLUME_CUBIC_METER: f64 = 12.; // m3 + const FWD_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 131.; // m3 + const BULK_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 17.3; // m3 + // TODO Pressurization volume 2100 m3 const PRESSURIZED_FUSELAGE_VOLUME_CUBIC_METER: f64 = 330.; // m3 const CABIN_LEAKAGE_AREA: f64 = 0.0003; // m2 const OUTFLOW_VALVE_SIZE: f64 = 0.05; // m2 @@ -916,6 +1107,7 @@ mod tests { use super::*; use ntest::assert_about_eq; use systems::{ + air_conditioning::PackFlow, electrical::{test::TestElectricitySource, ElectricalBus, Electricity}, integrated_modular_avionics::core_processing_input_output_module::CoreProcessingInputOutputModule, overhead::AutoOffFaultPushButton, @@ -936,6 +1128,7 @@ mod tests { }; use uom::si::{ length::{foot, meter}, + mass_rate::kilogram_per_second, pressure::{hectopascal, psi}, thermodynamic_temperature::degree_celsius, velocity::{foot_per_minute, meter_per_second}, @@ -1170,6 +1363,10 @@ mod tests { fn set_cross_bleed_valve_open(&mut self) { self.cross_bleed_valve = DefaultValve::new_open(); } + + fn packs(&mut self) -> &mut [TestPneumaticPackComplex; 2] { + &mut self.packs + } } impl PneumaticBleed for TestPneumatic { @@ -1247,7 +1444,7 @@ mod tests { PneumaticPipe::new( Volume::new::(8.), Pressure::new::(44.), - ThermodynamicTemperature::new::(144.), + ThermodynamicTemperature::new::(200.), ) } else { PneumaticPipe::new( @@ -1263,7 +1460,7 @@ mod tests { PneumaticPipe::new( Volume::new::(16.), Pressure::new::(14.7), - ThermodynamicTemperature::new::(131.), + ThermodynamicTemperature::new::(200.), ) } else { PneumaticPipe::new( @@ -1333,7 +1530,7 @@ mod tests { Pressure::new::(14.7), ThermodynamicTemperature::new::(15.), ), - exhaust: PneumaticExhaust::new(0.3, 0.3, Pressure::new::(0.)), + exhaust: PneumaticExhaust::new(0.3, 0.3, Pressure::default()), left_pack_flow_valve: ElectroPneumaticValve::new( ElectricalBusType::DirectCurrentEssential, ), @@ -1461,6 +1658,28 @@ mod tests { } } + struct TestDsms { + aft_cargo_door_open: bool, + } + impl TestDsms { + fn new() -> Self { + Self { + aft_cargo_door_open: false, + } + } + fn open_aft_cargo_door(&mut self, open: bool) { + self.aft_cargo_door_open = open; + } + } + impl CargoDoorLocked for TestDsms { + fn aft_cargo_door_locked(&self) -> bool { + !self.aft_cargo_door_open + } + fn fwd_cargo_door_locked(&self) -> bool { + true + } + } + struct TestLgciu { compressed: bool, } @@ -1504,6 +1723,7 @@ mod tests { a380_cabin_air: A380AirConditioning, adcn: TestAdcn, adirs: TestAdirs, + dsms: TestDsms, engine_1: TestEngine, engine_2: TestEngine, engine_3: TestEngine, @@ -1515,15 +1735,18 @@ mod tests { lgciu1: TestLgciu, lgciu2: TestLgciu, powered_dc_source_1: TestElectricitySource, + powered_dc_source_ess: TestElectricitySource, powered_ac_source_ess: TestElectricitySource, powered_ac_source_1: TestElectricitySource, powered_dc_source_2: TestElectricitySource, powered_ac_source_2: TestElectricitySource, + powered_ac_source_3: TestElectricitySource, powered_ac_source_4: TestElectricitySource, dc_1_bus: ElectricalBus, ac_1_bus: ElectricalBus, dc_2_bus: ElectricalBus, ac_2_bus: ElectricalBus, + ac_3_bus: ElectricalBus, ac_4_bus: ElectricalBus, ac_ess_bus: ElectricalBus, dc_ess_bus: ElectricalBus, @@ -1543,6 +1766,7 @@ mod tests { a380_cabin_air: A380AirConditioning::new(context), adcn: TestAdcn::new(context), adirs: TestAdirs::new(), + dsms: TestDsms::new(), engine_1: TestEngine::new(Ratio::default()), engine_2: TestEngine::new(Ratio::default()), engine_3: TestEngine::new(Ratio::default()), @@ -1557,6 +1781,10 @@ mod tests { context, PotentialOrigin::Battery(1), ), + powered_dc_source_ess: TestElectricitySource::powered( + context, + PotentialOrigin::EmergencyGenerator, + ), powered_ac_source_ess: TestElectricitySource::powered( context, PotentialOrigin::EmergencyGenerator, @@ -1571,16 +1799,21 @@ mod tests { ), powered_ac_source_2: TestElectricitySource::powered( context, - PotentialOrigin::EngineGenerator(4), + PotentialOrigin::EngineGenerator(2), + ), + powered_ac_source_3: TestElectricitySource::powered( + context, + PotentialOrigin::EngineGenerator(3), ), powered_ac_source_4: TestElectricitySource::powered( context, - PotentialOrigin::EngineGenerator(2), + PotentialOrigin::EngineGenerator(4), ), 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)), + ac_3_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(3)), ac_4_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(4)), ac_ess_bus: ElectricalBus::new( context, @@ -1642,6 +1875,34 @@ mod tests { self.powered_ac_source_ess.power(); } + fn unpower_dc_1_bus(&mut self) { + self.powered_dc_source_1.unpower(); + } + + fn unpower_dc_ess_bus(&mut self) { + self.powered_dc_source_ess.unpower(); + } + + fn power_dc_ess_bus(&mut self) { + self.powered_dc_source_ess.power(); + } + + fn power_dc_1_bus(&mut self) { + self.powered_dc_source_1.power(); + } + + fn unpower_dc_2_bus(&mut self) { + self.powered_dc_source_2.unpower(); + } + + fn power_dc_2_bus(&mut self) { + self.powered_dc_source_2.power(); + } + + fn unpower_ac_1_bus(&mut self) { + self.powered_ac_source_1.unpower(); + } + fn unpower_ac_2_bus(&mut self) { self.powered_ac_source_2.unpower(); } @@ -1650,14 +1911,18 @@ mod tests { self.powered_ac_source_2.power(); } - fn unpower_ac_4_bus(&mut self) { - self.powered_ac_source_4.unpower(); + fn unpower_ac_3_bus(&mut self) { + self.powered_ac_source_3.unpower(); } fn power_ac_4_bus(&mut self) { self.powered_ac_source_4.power(); } + fn unpower_ac_4_bus(&mut self) { + self.powered_ac_source_4.unpower(); + } + fn set_pressure_based_on_vs(&mut self, alt_diff: Length) { // We find the atmospheric pressure that would give us the desired v/s let init_pressure_ratio: f64 = @@ -1686,16 +1951,20 @@ mod tests { electricity.supplied_by(&self.powered_dc_source_1); electricity.supplied_by(&self.powered_ac_source_1); electricity.supplied_by(&self.powered_dc_source_2); + electricity.supplied_by(&self.powered_dc_source_ess); electricity.supplied_by(&self.powered_ac_source_2); + electricity.supplied_by(&self.powered_ac_source_3); electricity.supplied_by(&self.powered_ac_source_4); electricity.supplied_by(&self.powered_ac_source_ess); + electricity.supplied_by(&self.powered_dc_source_ess); 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_ac_source_3, &self.ac_3_bus); electricity.flow(&self.powered_ac_source_4, &self.ac_4_bus); electricity.flow(&self.powered_ac_source_ess, &self.ac_ess_bus); - electricity.flow(&self.powered_dc_source_1, &self.dc_ess_bus); + electricity.flow(&self.powered_dc_source_ess, &self.dc_ess_bus); electricity.flow(&self.powered_dc_source_1, &self.dc_bat_bus); } fn update_after_power_distribution(&mut self, context: &UpdateContext) { @@ -1704,9 +1973,12 @@ mod tests { &self.a380_cabin_air, [&self.engine_1, &self.engine_2], ); + self.a380_cabin_air + .mix_packs_air_update(self.pneumatic.packs()); self.a380_cabin_air.update( context, &self.adirs, + &self.dsms, &self.adcn, [ &self.engine_1, @@ -1752,9 +2024,6 @@ mod tests { test_bed.set_indicated_altitude(Length::default()); test_bed.indicated_airspeed(Velocity::new::(250.)); test_bed.set_ambient_temperature(ThermodynamicTemperature::new::(24.)); - test_bed.command_measured_temperature( - [ThermodynamicTemperature::new::(24.); 2], - ); test_bed.command_pack_flow_selector_position(0); test_bed.command_engine_n1(Ratio::new::(30.)); @@ -1928,18 +2197,38 @@ mod tests { self } - fn unpowered_ac_2_bus(mut self) -> Self { - self.command(|a| a.unpower_ac_2_bus()); + fn unpowered_dc_ess_bus(mut self) -> Self { + self.command(|a| a.unpower_dc_ess_bus()); self } - fn powered_ac_2_bus(mut self) -> Self { - self.command(|a| a.power_ac_2_bus()); + fn powered_dc_ess_bus(mut self) -> Self { + self.command(|a| a.power_dc_ess_bus()); self } - fn unpowered_ac_4_bus(mut self) -> Self { - self.command(|a| a.unpower_ac_4_bus()); + fn unpowered_dc_1_bus(mut self) -> Self { + self.command(|a| a.unpower_dc_1_bus()); + self + } + + fn powered_dc_1_bus(mut self) -> Self { + self.command(|a| a.power_dc_1_bus()); + self + } + + fn unpowered_dc_2_bus(mut self) -> Self { + self.command(|a| a.unpower_dc_2_bus()); + self + } + + fn powered_dc_2_bus(mut self) -> Self { + self.command(|a| a.power_dc_2_bus()); + self + } + + fn powered_ac_2_bus(mut self) -> Self { + self.command(|a| a.power_ac_2_bus()); self } @@ -1948,15 +2237,38 @@ mod tests { self } + fn unpowered_ac_1_bus(mut self) -> Self { + self.command(|a| a.unpower_ac_1_bus()); + self + } + + fn unpowered_ac_2_bus(mut self) -> Self { + self.command(|a| a.unpower_ac_2_bus()); + self + } + + fn unpowered_ac_3_bus(mut self) -> Self { + self.command(|a| a.unpower_ac_3_bus()); + self + } + + fn unpowered_ac_4_bus(mut self) -> Self { + self.command(|a| a.unpower_ac_4_bus()); + self + } + fn set_vertical_speed(&mut self, vertical_speed: Velocity) { self.vertical_speed = vertical_speed; } - fn command_measured_temperature(&mut self, temp_array: [ThermodynamicTemperature; 2]) { - for (temp, id) in temp_array.iter().zip(["CKPT", "FWD"].iter()) { - let zone_measured_temp_id = format!("COND_{}_TEMP", &id); - self.write_by_name(&zone_measured_temp_id, temp.get::()); - } + fn command_measured_temperature(mut self, temp: ThermodynamicTemperature) -> Self { + self.command(|a| { + a.a380_cabin_air + .a380_cabin + .cabin_air_simulation + .command_cabin_temperature(temp) + }); + self } fn command_pack_flow_selector_position(&mut self, value: u8) { @@ -2010,6 +2322,72 @@ mod tests { self } + fn command_cab_fans_pb_on(mut self, on_off: bool) -> Self { + self.write_by_name("OVHD_VENT_CAB_FANS_PB_IS_ON", on_off); + self + } + + fn command_hot_air_pb_on(mut self, on_off: bool, pb_id: usize) -> Self { + self.write_by_name( + format!("OVHD_COND_HOT_AIR_{}_PB_IS_ON", pb_id).as_str(), + on_off, + ); + self + } + + fn hot_air_pbs_on(mut self) -> Self { + self = self.command_hot_air_pb_on(true, 1); + self = self.command_hot_air_pb_on(true, 2); + self + } + + fn hot_air_pbs_off(mut self) -> Self { + self = self.command_hot_air_pb_on(false, 1); + self = self.command_hot_air_pb_on(false, 2); + self + } + + fn command_fwd_isolation_valves_pb_on(mut self, on_off: bool) -> Self { + self.write_by_name("OVHD_CARGO_AIR_ISOL_VALVES_FWD_PB_IS_ON", on_off); + self + } + + fn command_bulk_isolation_valves_pb_on(mut self, on_off: bool) -> Self { + self.write_by_name("OVHD_CARGO_AIR_ISOL_VALVES_BULK_PB_IS_ON", on_off); + self + } + + fn command_bulk_heater_pb_on(mut self, on_off: bool) -> Self { + self.write_by_name("OVHD_CARGO_AIR_HEATER_PB_IS_ON", on_off); + self + } + + fn command_selected_temperature(mut self, temperature: ThermodynamicTemperature) -> Self { + let knob_value: f64 = (temperature.get::() - 18.) / 0.04; + self.write_by_name("OVHD_COND_CKPT_SELECTOR_KNOB", knob_value); + self.write_by_name("OVHD_COND_CABIN_SELECTOR_KNOB", knob_value); + self + } + + fn command_cabin_selected_temperature( + mut self, + temperature: ThermodynamicTemperature, + ) -> Self { + let knob_value: f64 = (temperature.get::() - 18.) / 0.04; + self.write_by_name("OVHD_COND_CABIN_SELECTOR_KNOB", knob_value); + self + } + + fn command_cargo_selected_temperature( + mut self, + temperature: ThermodynamicTemperature, + ) -> Self { + let knob_value: f64 = (temperature.get::() - 5.) / 0.0667; + self.write_by_name("OVHD_CARGO_AIR_FWD_SELECTOR_KNOB", knob_value); + self.write_by_name("OVHD_CARGO_AIR_BULK_SELECTOR_KNOB", knob_value); + self + } + fn command_man_vs_switch_position(mut self, position: usize) -> Self { if position == 0 { self.write_by_name("OVHD_PRESS_MAN_VS_CTL_SWITCH", 0); @@ -2026,6 +2404,13 @@ mod tests { self } + fn command_open_aft_cargo_door(mut self, open: bool) -> Self { + self.command(|a| { + a.dsms.open_aft_cargo_door(open); + }); + self + } + fn command_number_of_passengers(mut self, number_of_passengers: usize) -> Self { self.command(|a| { a.a380_cabin_air @@ -2171,7 +2556,11 @@ mod tests { self.query(|a| a.a380_cabin_air.a320_pressurization_system.cpc[0].landing_elevation()) } - fn duct_temperature(&self) -> Vec { + fn duct_demand_temperature(&self) -> Vec { + self.query(|a| a.a380_cabin_air.cpiom_b.duct_demand_temperature()) + } + + fn duct_temperature(&self) -> Vec { self.query(|a| { a.a380_cabin_air .a380_air_conditioning_system @@ -2179,6 +2568,18 @@ mod tests { }) } + fn measured_temperature(&mut self) -> ThermodynamicTemperature { + self.read_by_name("COND_MAIN_DECK_1_TEMP") + } + + fn fwd_cargo_measured_temperature(&mut self) -> ThermodynamicTemperature { + self.read_by_name("COND_CARGO_FWD_TEMP") + } + + fn bulk_cargo_measured_temperature(&mut self) -> ThermodynamicTemperature { + self.read_by_name("COND_CARGO_BULK_TEMP") + } + fn is_mode_sel_pb_auto(&mut self) -> bool { self.read_by_name("OVHD_PRESS_MODE_SEL_PB_IS_AUTO") } @@ -2202,11 +2603,120 @@ mod tests { }) } + fn recirculated_air_flow(&self) -> MassRate { + MassRate::new::(self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .cabin_fans + .iter() + .map(|fan| fan.outlet_air().flow_rate().get::()) + .sum() + })) + } + fn pack_flow_by_pack(&self, pack_id: usize) -> MassRate { self.query(|a| { a.a380_cabin_air.a380_air_conditioning_system.fdac[pack_id - 1].pack_flow() }) } + + fn trim_air_valves_open_amount(&self) -> Ratio { + self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .trim_air_system + .trim_air_valves_open_amount()[1] + }) + } + + fn hot_air_is_enabled(&self) -> bool { + self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .tadd + .hot_air_is_enabled(1) + && a.a380_cabin_air + .a380_air_conditioning_system + .tadd + .hot_air_is_enabled(2) + }) + } + + fn hp_cabin_fans_are_enabled(&self) -> bool { + self.query(|a| { + a.a380_cabin_air.a380_air_conditioning_system.vcm[0].hp_cabin_fans_are_enabled() + && a.a380_cabin_air.a380_air_conditioning_system.vcm[1] + .hp_cabin_fans_are_enabled() + }) + } + + fn fwd_extraction_fan_is_on(&self) -> bool { + self.query(|a| { + a.a380_cabin_air.a380_air_conditioning_system.vcm[0].fwd_extraction_fan_is_on() + }) + } + + fn fwd_isolation_valves_are_open(&self) -> bool { + self.query(|a| { + a.a380_cabin_air.a380_air_conditioning_system.vcm[0] + .fwd_isolation_valves_open_allowed() + }) + } + + fn bulk_extraction_fan_is_on(&self) -> bool { + self.query(|a| { + a.a380_cabin_air.a380_air_conditioning_system.vcm[1].bulk_extraction_fan_is_on() + }) + } + + fn bulk_isolation_valves_are_open(&self) -> bool { + self.query(|a| { + a.a380_cabin_air.a380_air_conditioning_system.vcm[1] + .bulk_isolation_valves_open_allowed() + }) + } + + fn bulk_duct_heater_on_allowed(&self) -> bool { + self.query(|a| { + a.a380_cabin_air.a380_air_conditioning_system.vcm[1].bulk_duct_heater_on_allowed() + }) + } + + fn bulk_duct_heater_is_on(&self) -> bool { + self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .cargo_air_heater + .is_on() + }) + } + + fn mixer_unit_outlet_air(&self) -> Air { + self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .mixer_unit + .outlet_air() + }) + } + + fn trim_air_system_outlet_air(&self, id: usize) -> Air { + self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .trim_air_system + .trim_air_system_valve_outlet_air(id) + }) + } + + fn trim_air_outlet_temperature(&self) -> ThermodynamicTemperature { + self.query(|a| { + a.a380_cabin_air + .a380_air_conditioning_system + .trim_air_system + .duct_temperature()[1] + }) + } } impl TestBed for CabinAirTestBed { type Aircraft = TestAircraft; @@ -2974,7 +3484,7 @@ mod tests { assert!( test_bed.cabin_delta_p() < Pressure::new::( - A320PressurizationConstants::EXCESSIVE_RESIDUAL_PRESSURE_WARNING + A380PressurizationConstants::EXCESSIVE_RESIDUAL_PRESSURE_WARNING ) ); } @@ -2983,6 +3493,7 @@ mod tests { use super::*; #[test] + #[ignore] fn altitude_calculation_uses_local_altimeter() { let mut test_bed = test_bed() .on_ground() @@ -3006,6 +3517,7 @@ mod tests { } #[test] + #[ignore] fn altitude_calculation_uses_standard_if_no_altimeter_data() { let mut test_bed = test_bed() .on_ground() @@ -3037,6 +3549,7 @@ mod tests { } #[test] + #[ignore] fn altitude_calculation_uses_standard_if_man_mode_is_on() { let mut test_bed = test_bed() .on_ground() @@ -3068,6 +3581,7 @@ mod tests { } #[test] + #[ignore] fn altitude_calculation_uses_local_altimeter_when_not_at_sea_level() { let mut test_bed = test_bed() .on_ground() @@ -3103,6 +3617,7 @@ mod tests { } #[test] + #[ignore] fn altitude_calculation_uses_local_altimeter_during_climb() { let mut test_bed = test_bed() .on_ground() @@ -3143,6 +3658,7 @@ mod tests { } #[test] + #[ignore] fn altitude_calculation_uses_isa_altimeter_when_over_5000_ft_from_airport() { let mut test_bed = test_bed() .on_ground() @@ -3187,38 +3703,6 @@ mod tests { mod a380_air_conditioning_tests { use super::*; - const A380_ZONE_IDS: [&str; 18] = [ - "CKPT", - "MAIN_DECK_1", - "MAIN_DECK_2", - "MAIN_DECK_3", - "MAIN_DECK_4", - "MAIN_DECK_5", - "MAIN_DECK_6", - "MAIN_DECK_7", - "MAIN_DECK_8", - "UPPER_DECK_1", - "UPPER_DECK_2", - "UPPER_DECK_3", - "UPPER_DECK_4", - "UPPER_DECK_5", - "UPPER_DECK_6", - "UPPER_DECK_7", - "CARGO_FWD", - "CARGO_BULK", - ]; - - #[test] - fn duct_temperature_starts_at_24_c_in_all_zones() { - let test_bed = test_bed(); - - for id in 0..A380_ZONE_IDS.len() { - assert_eq!( - test_bed.duct_temperature()[id], - ThermodynamicTemperature::new::(24.) - ); - } - } mod pack_flow_controller_tests { use super::*; @@ -3529,7 +4013,7 @@ mod tests { } #[test] - fn pack_flow_valve_is_unresponsive_when_unpowered() { + fn pack_flow_valve_is_unresponsive_when_fdac_unpowered() { let mut test_bed = test_bed() .with() .command_packs_on_off(true) @@ -3580,6 +4064,25 @@ mod tests { ); } + #[test] + fn unpowering_one_channel_doesnt_unpower_system() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(4); + + assert!(test_bed.pack_flow() > MassRate::default()); + + test_bed = test_bed + .unpowered_ac_ess_bus() + .command_ditching_pb_on() + .iterate(4); + + assert_eq!(test_bed.pack_flow(), MassRate::default()); + } + #[test] fn pack_flow_controller_signals_resets_after_power_reset() { let mut test_bed = test_bed() @@ -3607,5 +4110,1022 @@ mod tests { assert_eq!(test_bed.pack_flow(), MassRate::default()); } } + + mod zone_controller_tests { + use super::*; + + const A380_ZONE_IDS: [&str; 16] = [ + "CKPT", + "MAIN_DECK_1", + "MAIN_DECK_2", + "MAIN_DECK_3", + "MAIN_DECK_4", + "MAIN_DECK_5", + "MAIN_DECK_6", + "MAIN_DECK_7", + "MAIN_DECK_8", + "UPPER_DECK_1", + "UPPER_DECK_2", + "UPPER_DECK_3", + "UPPER_DECK_4", + "UPPER_DECK_5", + "UPPER_DECK_6", + "UPPER_DECK_7", + ]; + + #[test] + fn duct_temperature_starts_at_24_c_in_all_zones() { + let test_bed = test_bed(); + + for id in 0..A380_ZONE_IDS.len() { + assert_eq!( + test_bed.duct_demand_temperature()[id], + ThermodynamicTemperature::new::(24.) + ); + assert_eq!( + test_bed.duct_temperature()[id], + ThermodynamicTemperature::new::(24.) + ); + } + } + + #[test] + fn duct_temp_starts_and_stays_at_24_c_with_no_input() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(false) + .and() + .command_cab_fans_pb_on(false) + .command_selected_temperature(ThermodynamicTemperature::new::( + 24., + )); + + test_bed + .set_ambient_temperature(ThermodynamicTemperature::new::(24.)); + + test_bed = test_bed.iterate(2); + + for id in 0..A380_ZONE_IDS.len() { + assert_eq!( + test_bed.duct_temperature()[id], + ThermodynamicTemperature::new::(24.) + ); + } + + test_bed = test_bed.iterate(200); + + for id in 0..A380_ZONE_IDS.len() { + assert_eq!( + test_bed.duct_temperature()[id], + ThermodynamicTemperature::new::(24.) + ); + } + } + + #[test] + fn system_maintains_24_in_cabin_with_no_inputs() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 24., + )) + .iterate(1000); + + assert!((test_bed.measured_temperature().get::() - 24.).abs() < 1.); + } + + #[test] + fn duct_temperature_is_cabin_temp_when_no_flow() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(false) + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 18., + )) + .iterate(1000); + + assert!( + (test_bed.duct_temperature()[1].get::() + - test_bed.measured_temperature().get::()) + .abs() + < 1. + ); + } + + #[test] + fn increasing_selected_temp_increases_duct_demand_temp() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 30., + )); + + let initial_temperature = test_bed.duct_demand_temperature()[1]; + test_bed = test_bed.iterate_with_delta(100, Duration::from_secs(10)); + + assert!(test_bed.duct_demand_temperature()[1] > initial_temperature); + } + + #[test] + fn increasing_measured_temp_reduces_duct_demand_temp() { + let test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .run_and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 24., + )) + .iterate_with_delta(100, Duration::from_secs(10)) + .then() + .command_measured_temperature(ThermodynamicTemperature::new::( + 30., + )) + .iterate(4); + + assert!( + test_bed.duct_demand_temperature()[1] + < ThermodynamicTemperature::new::(24.) + ); + } + + #[test] + fn duct_demand_temp_reaches_equilibrium() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .run_and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 26., + )) + .iterate(100); + + let mut previous_temp = test_bed.duct_demand_temperature()[1]; + test_bed.run(); + let initial_temp_diff = test_bed.duct_demand_temperature()[1] + .get::() + - previous_temp.get::(); + test_bed = test_bed.iterate(100); + previous_temp = test_bed.duct_demand_temperature()[1]; + test_bed.run(); + let final_temp_diff = test_bed.duct_demand_temperature()[1].get::() + - previous_temp.get::(); + + assert!(initial_temp_diff.abs() > final_temp_diff.abs()); + } + + #[test] + fn duct_temp_increases_with_altitude() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 24., + )) + .iterate(100); + + let initial_temperature = test_bed.duct_temperature()[1]; + + test_bed = test_bed + .command_aircraft_climb(Length::new::(0.), Length::new::(30000.)); + + assert!(test_bed.duct_temperature()[1] > initial_temperature); + } + + #[test] + fn duct_demand_limit_changes_with_measured_temperature() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 10., + )) + .command_measured_temperature(ThermodynamicTemperature::new::( + 24., + )) + .iterate_with_delta(200, Duration::from_secs(1)); + + assert!( + (test_bed.duct_demand_temperature()[1].get::() - 8.).abs() < 1. + ); + + test_bed = test_bed + .command_measured_temperature(ThermodynamicTemperature::new::( + 27., + )) + .and_run(); + + assert!( + (test_bed.duct_demand_temperature()[1].get::() - 5.).abs() < 1. + ); + + test_bed = test_bed + .command_measured_temperature(ThermodynamicTemperature::new::( + 29., + )) + .and_run(); + + assert!( + (test_bed.duct_demand_temperature()[1].get::() - 2.).abs() < 1. + ); + } + + #[test] + fn knobs_dont_affect_duct_temperature_when_cpiom_unpowered() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .and() + .unpowered_dc_1_bus() + .unpowered_dc_2_bus() + .unpowered_dc_ess_bus() + .command_selected_temperature(ThermodynamicTemperature::new::( + 30., + )); + + test_bed = test_bed.iterate(1000); + + assert!((test_bed.duct_temperature()[1].get::() - 24.).abs() < 1.); + } + } + + mod trim_air_drive_device_tests { + use super::*; + + #[test] + fn hot_air_starts_disabled() { + let test_bed = test_bed(); + + assert!(!test_bed.hot_air_is_enabled()); + } + + #[test] + fn hot_air_enables_when_all_conditions_met() { + let test_bed = test_bed() + .with() + .command_packs_on_off(true) + .hot_air_pbs_on() + .and() + .engines_idle() + .iterate(32); + + assert!(test_bed.hot_air_is_enabled()); + } + + #[test] + fn hot_air_stays_disabled_if_one_condition_is_not_met() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .hot_air_pbs_on() + .and() + .engines_idle() + .iterate(32); + assert!(test_bed.hot_air_is_enabled()); + + test_bed = test_bed.hot_air_pbs_off().iterate(4); + assert!(!test_bed.hot_air_is_enabled()); + + test_bed = test_bed.hot_air_pbs_on().iterate(4); + assert!(test_bed.hot_air_is_enabled()); + + // Tadd is unpowered + test_bed = test_bed + .unpowered_ac_2_bus() + .unpowered_ac_4_bus() + .iterate(4); + assert!(!test_bed.hot_air_is_enabled()); + + test_bed = test_bed.powered_ac_2_bus().powered_ac_4_bus().iterate(32); + assert!(test_bed.hot_air_is_enabled()); + } + + #[test] + fn unpowering_the_tadd_closes_trim_air_pressure_regulating_valves() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(20) + .and() + .unpowered_ac_2_bus() + .unpowered_ac_4_bus() + .command_selected_temperature(ThermodynamicTemperature::new::( + 30., + )); + + test_bed = test_bed.iterate_with_delta(100, Duration::from_secs(10)); + + assert!(test_bed.trim_air_valves_open_amount() < Ratio::new::(1.)) + } + } + + mod mixer_unit_tests { + use uom::si::mass_rate::kilogram_per_second; + + use super::*; + + #[test] + fn hp_cabin_fans_start_disabled() { + let test_bed = test_bed(); + + assert!(!test_bed.hp_cabin_fans_are_enabled()); + } + + #[test] + fn hp_cabin_fans_enable_when_all_conditions_met() { + let test_bed = test_bed().with().command_cab_fans_pb_on(true).iterate(4); + + assert!(test_bed.hp_cabin_fans_are_enabled()); + } + + #[test] + fn cabin_fan_controller_stays_disabled_if_one_condition_is_not_met() { + let mut test_bed = test_bed().with().command_cab_fans_pb_on(true).iterate(4); + assert!(test_bed.hp_cabin_fans_are_enabled()); + + test_bed = test_bed.command_cab_fans_pb_on(false).iterate(4); + assert!(!test_bed.hp_cabin_fans_are_enabled()); + + // Unpower both channels of both vcm's + test_bed = test_bed.command_cab_fans_pb_on(true); + test_bed = test_bed + .unpowered_dc_1_bus() + .unpowered_dc_2_bus() + .unpowered_dc_ess_bus() + .iterate(4); + assert!(!test_bed.hp_cabin_fans_are_enabled()); + + test_bed = test_bed + .powered_dc_1_bus() + .powered_dc_2_bus() + .powered_dc_ess_bus() + .iterate(4); + assert!(test_bed.hp_cabin_fans_are_enabled()); + + test_bed = test_bed.command_ditching_pb_on().iterate(4); + assert!(!test_bed.hp_cabin_fans_are_enabled()); + } + + #[test] + fn mixer_unit_outlet_air_doesnt_move_without_inlets() { + let test_bed = test_bed() + .with() + .command_cab_fans_pb_on(false) + .and() + .command_packs_on_off(false) + .and_run(); + + assert_eq!( + test_bed.mixer_unit_outlet_air().flow_rate(), + MassRate::default(), + ); + } + + #[test] + fn mixer_unit_outlet_is_same_as_packs_without_cab_fans() { + let test_bed = test_bed() + .with() + .command_cab_fans_pb_on(false) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(50); + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()).abs() + < MassRate::new::(0.1) + ) + } + + #[test] + fn changing_pack_flow_changes_mixer_unit_outlet() { + let mut test_bed = test_bed() + .with() + .command_cab_fans_pb_on(false) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(50); + + let initial_flow = test_bed.mixer_unit_outlet_air().flow_rate(); + test_bed.command_pack_flow_selector_position(3); + test_bed = test_bed.iterate(50); + + assert!(test_bed.mixer_unit_outlet_air().flow_rate() > initial_flow); + } + + #[test] + fn mixer_unit_outlet_is_same_as_fan_without_packs() { + let test_bed = test_bed() + .with() + .command_cab_fans_pb_on(true) + .and() + .command_packs_on_off(false) + .and() + .engines_idle() + .iterate(50); + + assert!( + test_bed.mixer_unit_outlet_air().flow_rate() + > MassRate::new::(0.) + ); + assert_ne!( + test_bed.mixer_unit_outlet_air().flow_rate(), + test_bed.pack_flow() + ); + } + + #[test] + fn mixer_unit_outlet_adds_packs_and_fans() { + let test_bed = test_bed() + .with() + .command_cab_fans_pb_on(true) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() + - (test_bed.recirculated_air_flow() + test_bed.pack_flow())) + .abs() + < MassRate::new::(0.1) + ) + } + + #[test] + fn mixer_unit_flow_outputs_match_amm() { + // No data available for A380 so we use the volume of air per pax from A320 as estimation + // Total mixed air per cabin occupant: 9.9 g/s -> (for 517 occupants) 5.1183 + let test_bed = test_bed() + .with() + .command_cab_fans_pb_on(true) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() + - MassRate::new::(5.1183)) + .abs() + < MassRate::new::(0.1) + ) + } + + #[test] + fn mixer_unit_flow_outputs_match_amm_at_different_pack_flows() { + // This tests checks that the cabin fans recirculation speed changes according to the pack flow + let mut test_bed = test_bed() + .with() + .command_cab_fans_pb_on(true) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(50); + + test_bed.command_pack_flow_selector_position(1); + test_bed = test_bed.iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() + - (MassRate::new::(5.1183) * 0.8)) + .abs() + < MassRate::new::(0.1) + ); + + test_bed.command_pack_flow_selector_position(2); + test_bed = test_bed.iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() + - MassRate::new::(5.1183)) + .abs() + < MassRate::new::(0.1) + ); + + test_bed.command_pack_flow_selector_position(3); + test_bed = test_bed.iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() + - (MassRate::new::(5.1183) * 1.2)) + .abs() + < MassRate::new::(0.1) + ); + } + + #[test] + fn mixer_unit_flow_outputs_dont_match_amm_if_cpiom_unpowered() { + // This tests checks that the control of the fans goes to the vcm if the cpiom is unpowered + let mut test_bed = test_bed() + .with() + .command_cab_fans_pb_on(true) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .unpowered_dc_2_bus() + .unpowered_dc_ess_bus() + .iterate(50); + + test_bed.command_pack_flow_selector_position(1); + test_bed = test_bed.iterate(50); + + assert!( + !((test_bed.mixer_unit_outlet_air().flow_rate() + - (MassRate::new::(5.1183) * 0.8)) + .abs() + < MassRate::new::(0.1)) + ); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()).abs() + > MassRate::new::(0.1) + ); + + test_bed = test_bed.powered_dc_2_bus().powered_dc_ess_bus(); + test_bed = test_bed.iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() + - (MassRate::new::(5.1183) * 0.8)) + .abs() + < MassRate::new::(0.1) + ); + } + + #[test] + fn mixer_unit_mixes_air_temperatures() { + let test_bed = test_bed() + .with() + .engines_idle() + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 18., + )) + .iterate(50); + + assert!( + (test_bed + .mixer_unit_outlet_air() + .temperature() + .get::() + - test_bed.duct_demand_temperature()[1].get::()) + > 4. + ) + } + + #[test] + fn cabin_fans_dont_work_without_power() { + let mut test_bed = test_bed() + .with() + .command_cab_fans_pb_on(true) + .and() + .command_packs_on_off(true) + .and() + .engines_idle() + .iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()) + > MassRate::new::(0.1) + ); + + test_bed = test_bed + .unpowered_ac_1_bus() + .unpowered_ac_2_bus() + .unpowered_ac_3_bus() + .unpowered_ac_4_bus() + .iterate(50); + + assert!( + (test_bed.mixer_unit_outlet_air().flow_rate() - test_bed.pack_flow()) + < MassRate::new::(0.1) + ) + } + } + + mod trim_air_tests { + use super::*; + + #[test] + fn trim_air_system_delivers_mixer_air_temp_if_no_hot_air() { + let test_bed = test_bed() + .with() + .hot_air_pbs_off() + .and() + .engines_idle() + .iterate(50); + assert!( + (test_bed + .trim_air_outlet_temperature() + .get::() + - test_bed + .mixer_unit_outlet_air() + .temperature() + .get::()) + .abs() + < 1. + ) + } + + #[test] + fn trim_air_system_delivers_hot_air_if_on() { + let test_bed = test_bed() + .with() + .hot_air_pbs_on() + .and() + .engines_idle() + .command_cabin_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(30.)) + .iterate(500); + + // If both zones get the temperature raised at the same time the packs deliver hotter air and the + // effect of hot air valves is negligible + 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 test_bed = test_bed() + .with() + .engines_idle() + .and() + .command_selected_temperature(ThermodynamicTemperature::new::( + 18., + )) + .then() + .command_measured_temperature(ThermodynamicTemperature::new::( + 30., + )) + .iterate(100); + + assert!( + (test_bed.trim_air_system_outlet_air(1).flow_rate()) + < MassRate::new::(0.01) + ); + } + + #[test] + fn trim_valves_react_to_only_one_pack_operative() { + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .command_selected_temperature(ThermodynamicTemperature::new::( + 24., + )) + .command_cabin_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(22.)) + .iterate(200); + + let initial_open = test_bed.trim_air_valves_open_amount(); + + test_bed = test_bed.command_one_pack_on(1).iterate(50); + + assert!(test_bed.trim_air_valves_open_amount() > initial_open); + } + + #[test] + fn when_engine_in_start_condition_air_is_recirculated() { + // This test is redundant but it's to target a specific condition that was failing in sim + let mut test_bed = test_bed() + .with() + .command_packs_on_off(true) + .and() + .engines_idle() + .command_selected_temperature(ThermodynamicTemperature::new::( + 18., + )) + .iterate(100) + .then() + .command_engine_in_start_mode() + .iterate(4); + + assert_eq!(test_bed.pack_flow(), MassRate::default()); + assert!( + (test_bed + .trim_air_outlet_temperature() + .get::() + - test_bed + .mixer_unit_outlet_air() + .temperature() + .get::()) + .abs() + < 1. + ); + assert!( + (test_bed.duct_temperature()[1].get::() + - test_bed.measured_temperature().get::()) + .abs() + < 1. + ); + } + } + + mod cargo_ventilation_tests { + use super::*; + + #[test] + fn all_fans_and_isolation_valves_start_disabled() { + let test_bed = test_bed(); + + assert!(!test_bed.fwd_extraction_fan_is_on()); + assert!(!test_bed.fwd_isolation_valves_are_open()); + assert!(!test_bed.bulk_extraction_fan_is_on()); + assert!(!test_bed.bulk_isolation_valves_are_open()); + assert!(!test_bed.bulk_duct_heater_is_on()); + } + + #[test] + fn fwd_isolation_and_fans_are_on_when_conditions_met() { + let test_bed = test_bed() + .command_fwd_isolation_valves_pb_on(true) + .iterate(5); + + assert!(test_bed.fwd_extraction_fan_is_on()); + assert!(test_bed.fwd_isolation_valves_are_open()); + } + + #[test] + fn fwd_isolation_and_fans_are_off_when_conditions_not_met() { + let mut test_bed = test_bed() + .command_fwd_isolation_valves_pb_on(false) + .iterate(5); + + assert!(!test_bed.fwd_extraction_fan_is_on()); + assert!(!test_bed.fwd_isolation_valves_are_open()); + + test_bed = test_bed + .command_fwd_isolation_valves_pb_on(true) + .command_ditching_pb_on() + .iterate(5); + + assert!(!test_bed.fwd_extraction_fan_is_on()); + assert!(!test_bed.fwd_isolation_valves_are_open()); + } + + #[test] + fn bulk_isolation_and_fans_are_on_when_conditions_met() { + let test_bed = test_bed() + .command_bulk_isolation_valves_pb_on(true) + .iterate(5); + + assert!(test_bed.bulk_extraction_fan_is_on()); + assert!(test_bed.bulk_isolation_valves_are_open()); + } + + #[test] + fn bulk_isolation_and_fans_are_off_when_conditions_not_met() { + let mut test_bed = test_bed() + .command_bulk_isolation_valves_pb_on(false) + .iterate(5); + + assert!(!test_bed.bulk_extraction_fan_is_on()); + assert!(!test_bed.bulk_isolation_valves_are_open()); + + test_bed = test_bed + .command_bulk_isolation_valves_pb_on(true) + .command_ditching_pb_on() + .iterate(5); + + assert!(!test_bed.bulk_extraction_fan_is_on()); + assert!(!test_bed.bulk_isolation_valves_are_open()); + } + + #[test] + fn bulk_heater_allowed_on_when_conditions_met() { + let mut test_bed = test_bed() + .command_bulk_isolation_valves_pb_on(true) + .command_bulk_heater_pb_on(true) + .iterate(5); + + assert!(test_bed.bulk_duct_heater_on_allowed()); + + test_bed = test_bed.command_bulk_heater_pb_on(false).iterate(5); + + assert!(!test_bed.bulk_duct_heater_on_allowed()); + } + + #[test] + fn bulk_heater_turns_on_when_conditions_met() { + let mut test_bed = test_bed() + .and_run() + .command_measured_temperature(ThermodynamicTemperature::new::( + 10., + )) + .iterate(5); + + assert!(test_bed.bulk_duct_heater_is_on()); + + test_bed = test_bed + .command_measured_temperature(ThermodynamicTemperature::new::( + 15.5, + )) + .iterate(5); + + assert!(test_bed.bulk_duct_heater_is_on()); + + test_bed = test_bed + .command_measured_temperature(ThermodynamicTemperature::new::( + 17., + )) + .iterate(5); + + assert!(!test_bed.bulk_duct_heater_is_on()); + } + + #[test] + fn bulk_heater_switches_off_when_on_ground_and_door_open() { + let mut test_bed = test_bed() + .on_ground() + .command_open_aft_cargo_door(true) + .command_measured_temperature(ThermodynamicTemperature::new::( + 10., + )) + .iterate(5); + + assert!(!test_bed.bulk_duct_heater_is_on()); + + test_bed = test_bed + .command_open_aft_cargo_door(false) + .command_measured_temperature(ThermodynamicTemperature::new::( + 10., + )) + .iterate(5); + + assert!(test_bed.bulk_duct_heater_is_on()); + } + + #[test] + fn bulk_heater_warms_up_the_zone() { + let mut test_bed = test_bed() + .on_ground() + .ambient_temperature_of(ThermodynamicTemperature::new::(-30.)) + .command_measured_temperature(ThermodynamicTemperature::new::( + 5., + )) + .command_cargo_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(20.)) + .command_bulk_heater_pb_on(false) + .iterate(100); + + assert!(!test_bed.bulk_duct_heater_is_on()); + assert!(test_bed.measured_temperature().get::() > 15.); + assert!( + test_bed + .bulk_cargo_measured_temperature() + .get::() + < 15. + ); + + test_bed = test_bed.command_bulk_heater_pb_on(true).iterate(200); + + assert!(test_bed.bulk_duct_heater_is_on()); + assert!(test_bed.measured_temperature().get::() > 15.); + assert!( + test_bed + .bulk_cargo_measured_temperature() + .get::() + > 15. + ); + } + + #[test] + fn bulk_heater_stops_when_temperature_achieved() { + let mut test_bed = test_bed() + .on_ground() + .ambient_temperature_of(ThermodynamicTemperature::new::(0.)) + .command_measured_temperature(ThermodynamicTemperature::new::( + 5., + )) + .command_cargo_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(20.)) + .command_bulk_heater_pb_on(true) + .iterate(500); + + assert!(!test_bed.bulk_duct_heater_is_on()); + assert!(test_bed.measured_temperature().get::() > 15.); + assert!( + (test_bed + .bulk_cargo_measured_temperature() + .get::() + - 20.) + .abs() + < 2. + ); + } + + #[test] + fn fwd_cargo_uses_tav_to_warm_up_zone() { + let mut test_bed = test_bed() + .on_ground() + .ambient_temperature_of(ThermodynamicTemperature::new::(-30.)) + .command_measured_temperature(ThermodynamicTemperature::new::( + 5., + )) + .command_cargo_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(15.)) + .iterate(500); + + assert!(test_bed.measured_temperature().get::() > 20.); + assert!( + test_bed + .fwd_cargo_measured_temperature() + .get::() + < 20. + ); + } + + #[test] + fn fwd_cargo_lowers_pack_outlet_to_cool_zone() { + let mut test_bed = test_bed() + .on_ground() + .ambient_temperature_of(ThermodynamicTemperature::new::(30.)) + .command_measured_temperature(ThermodynamicTemperature::new::( + 24., + )) + .command_cargo_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(15.)) + .iterate(500); + + assert!(test_bed.measured_temperature().get::() > 20.); + assert!( + (test_bed + .fwd_cargo_measured_temperature() + .get::() + - 15.) + .abs() + < 2. + ); + } + + #[test] + fn air_stops_when_isol_valves_closed() { + let mut test_bed = test_bed() + .on_ground() + .ambient_temperature_of(ThermodynamicTemperature::new::(0.)) + .command_measured_temperature(ThermodynamicTemperature::new::( + 5., + )) + .command_cargo_selected_temperature(ThermodynamicTemperature::new::< + degree_celsius, + >(20.)) + .command_bulk_isolation_valves_pb_on(false) + .command_fwd_isolation_valves_pb_on(false) + .iterate(500); + + assert!(!test_bed.bulk_duct_heater_is_on()); + assert!(test_bed.measured_temperature().get::() > 15.); + assert!( + test_bed + .bulk_cargo_measured_temperature() + .get::() + < 15. + ); + assert!( + test_bed + .fwd_cargo_measured_temperature() + .get::() + < 15. + ); + } + } } } diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs index 33165032c52..c7cda9d8ab3 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs @@ -51,7 +51,7 @@ use systems::{ overhead::{AutoOffFaultPushButton, AutoOnFaultPushButton}, shared::{ interpolation, random_from_range, update_iterator::MaxStepLoop, AdirsDiscreteOutputs, - AirbusElectricPumpId, AirbusEngineDrivenPumpId, DelayedFalseLogicGate, + AirbusElectricPumpId, AirbusEngineDrivenPumpId, CargoDoorLocked, DelayedFalseLogicGate, DelayedPulseTrueLogicGate, DelayedTrueLogicGate, ElectricalBusType, ElectricalBuses, EngineFirePushButtons, GearWheel, HydraulicColor, LandingGearHandle, LgciuInterface, LgciuWeightOnWheels, ReservoirAirPressure, SectionPressure, SurfacesPositions, @@ -2802,6 +2802,7 @@ impl A380Hydraulic { &self.gear_system } } + impl SurfacesPositions for A380Hydraulic { fn left_ailerons_positions(&self) -> &[f64] { self.left_aileron.positions() @@ -2827,6 +2828,16 @@ impl SurfacesPositions for A380Hydraulic { self.flap_system.right_position() } } + +impl CargoDoorLocked for A380Hydraulic { + fn fwd_cargo_door_locked(&self) -> bool { + self.forward_cargo_door.is_locked() + } + fn aft_cargo_door_locked(&self) -> bool { + self.aft_cargo_door.is_locked() + } +} + impl SimulationElement for A380Hydraulic { fn accept(&mut self, visitor: &mut T) { self.engine_driven_pump_1a.accept(visitor); diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/lib.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/lib.rs index cb4fde010f3..092f1d5e7dd 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/lib.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/lib.rs @@ -262,6 +262,7 @@ impl Aircraft for A380 { self.air_conditioning.update( context, &self.adirs, + &self.hydraulic, &self.adcn, [ &self.engine_1, diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/pneumatic.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/pneumatic.rs index 70fdf914f33..2e4c121fb31 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/pneumatic.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/pneumatic.rs @@ -1444,8 +1444,8 @@ mod tests { }, shared::{ arinc429::{Arinc429Word, SignStatus}, - ApuBleedAirValveSignal, ControllerSignal, ElectricalBusType, ElectricalBuses, - EmergencyElectricalState, EngineBleedPushbutton, EngineCorrectedN1, + ApuBleedAirValveSignal, CargoDoorLocked, ControllerSignal, ElectricalBusType, + ElectricalBuses, EmergencyElectricalState, EngineBleedPushbutton, EngineCorrectedN1, EngineFirePushButtons, EngineStartState, HydraulicColor, InternationalStandardAtmosphere, LgciuWeightOnWheels, MachNumber, PackFlowValveState, PneumaticBleed, PneumaticValve, PotentialOrigin, @@ -1474,6 +1474,7 @@ mod tests { air_conditioning: A380AirConditioning, adcn: A380AvionicsDataCommunicationNetwork, adirs: TestAdirs, + dsms: TestDsms, pressurization_overhead: A380PressurizationOverheadPanel, } impl TestAirConditioning { @@ -1482,6 +1483,7 @@ mod tests { air_conditioning: A380AirConditioning::new(context), adcn: A380AvionicsDataCommunicationNetwork::new(context), adirs: TestAdirs::new(), + dsms: TestDsms {}, pressurization_overhead: A380PressurizationOverheadPanel::new(context), } } @@ -1497,6 +1499,7 @@ mod tests { self.air_conditioning.update( context, &self.adirs, + &self.dsms, &self.adcn, engines, engine_fire_push_buttons, @@ -1549,6 +1552,17 @@ mod tests { } } + struct TestDsms {} + + impl CargoDoorLocked for TestDsms { + fn aft_cargo_door_locked(&self) -> bool { + true + } + fn fwd_cargo_door_locked(&self) -> bool { + true + } + } + struct TestLgciu { compressed: bool, } 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 c4d9a3ed3ac..a6b4a56b95a 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 @@ -13,9 +13,9 @@ use crate::{ }; use super::{ - AdirsToAirCondInterface, AirConditioningOverheadShared, Channel, DuctTemperature, - OperatingChannel, OverheadFlowSelector, PackFlow, PackFlowControllers, PackFlowValveSignal, - PressurizationOverheadShared, TrimAirControllers, TrimAirSystem, ZoneType, + AdirsToAirCondInterface, AirConditioningOverheadShared, CabinFansSignal, Channel, + DuctTemperature, OperatingChannel, OverheadFlowSelector, PackFlow, PackFlowControllers, + PackFlowValveSignal, PressurizationOverheadShared, TrimAirControllers, TrimAirSystem, ZoneType, }; use std::{fmt::Display, time::Duration}; @@ -67,7 +67,7 @@ pub struct AirConditioningSystemController>, + zone_controller: Vec, pack_flow_controller: PackFlowController, trim_air_system_controller: TrimAirSystemController, cabin_fans_controller: CabinFanController, @@ -96,8 +96,8 @@ impl AirConditioningSystemController AirConditioningSystemController Vec> { + ) -> 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])] + vec![ZoneController::new(cabin_zone_ids[0])] } else { cabin_zone_ids[1..] .iter() - .map(ZoneController::new) - .collect::>>() + .map(|zone| ZoneController::new(*zone)) + .collect::>() } } @@ -185,17 +185,18 @@ impl AirConditioningSystemController Some(AcscFault::BothChannelsFault), + (false, false) => None, + (ac, _) => { + if ac { + self.switch_active_channel(); + } Some(AcscFault::OneChannelFault) } - } else if self.stand_by_channel.has_fault() { - Some(AcscFault::OneChannelFault) - } else { - None }; } @@ -246,22 +247,6 @@ impl AirConditioningSystemController [ThermodynamicTemperature; ZONES] { - let demand_temperature: Vec = self - .zone_controller - .iter() - .map(|zone| zone.duct_demand_temperature()) - .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 { @@ -292,6 +277,35 @@ impl AirConditioningSystemController DuctTemperature + for AirConditioningSystemController +{ + /// This function needs to return a Vector of size ZONES. + /// + /// Because the `ZoneController` is different for each ACSC, we create a vector with "dummy" data for the zones + /// not being calculated by the relevant ACSC. + /// + /// ACSC1 calculates the duct demand temperature of the cockpit. + /// + /// ACSC2 calculates the duct demand temperature of the fwd and aft cabin zones. + fn duct_demand_temperature(&self) -> Vec { + let demand_temperature: Vec = self + .zone_controller + .iter() + .map(|zone| zone.duct_demand_temperature()[0]) + .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.to_vec() + } +} + impl PackFlow for AirConditioningSystemController { @@ -572,8 +586,8 @@ impl AirConditioningState { transition!(EndLanding, OnGround); -struct ZoneController { - zone_id: usize, +pub struct ZoneController { + zone_id: ZoneType, duct_demand_temperature: ThermodynamicTemperature, zone_selected_temperature: ThermodynamicTemperature, pid_controller: PidController, @@ -581,7 +595,7 @@ struct ZoneController { galley_fan_failure: Failure, } -impl ZoneController { +impl ZoneController { const K_ALTITUDE_CORRECTION_DEG_PER_FEET: f64 = 0.0000375; // deg/feet const UPPER_DUCT_TEMP_TRIGGER_HIGH_CELSIUS: f64 = 19.; // C const UPPER_DUCT_TEMP_TRIGGER_LOW_CELSIUS: f64 = 17.; // C @@ -591,13 +605,16 @@ impl ZoneController { const LOWER_DUCT_TEMP_TRIGGER_LOW_CELSIUS: f64 = 26.; // C const LOWER_DUCT_TEMP_LIMIT_LOW_KELVIN: f64 = 275.15; // K const LOWER_DUCT_TEMP_LIMIT_HIGH_KELVIN: f64 = 281.15; // K + const CARGO_DUCT_TEMP_LIMIT_LOW_KELVIN: f64 = 275.15; // K + const CARGO_DUCT_TEMP_LIMIT_HIGH_KELVIN: f64 = 343.15; // K const SETPOINT_TEMP_KELVIN: f64 = 297.15; // K + const CARGO_SETPOINT_TEMP_KELVIN: f64 = 288.15; // K const KI_DUCT_DEMAND_CABIN: f64 = 0.05; const KI_DUCT_DEMAND_COCKPIT: f64 = 0.04; const KP_DUCT_DEMAND_CABIN: f64 = 3.5; const KP_DUCT_DEMAND_COCKPIT: f64 = 2.; - fn new(zone_type: &ZoneType) -> Self { + pub fn new(zone_type: ZoneType) -> Self { let pid_controller = match zone_type { ZoneType::Cockpit => { PidController::new( @@ -610,7 +627,7 @@ impl ZoneController { 1., // Output gain ) } - ZoneType::Cabin(_) | &ZoneType::Cargo(_) => PidController::new( + ZoneType::Cabin(_) => PidController::new( Self::KP_DUCT_DEMAND_CABIN, Self::KI_DUCT_DEMAND_CABIN, 0., @@ -619,9 +636,18 @@ impl ZoneController { Self::SETPOINT_TEMP_KELVIN, 1., ), + ZoneType::Cargo(_) => PidController::new( + Self::KP_DUCT_DEMAND_CABIN, + Self::KI_DUCT_DEMAND_CABIN, + 0., + Self::CARGO_DUCT_TEMP_LIMIT_HIGH_KELVIN, + Self::CARGO_DUCT_TEMP_LIMIT_LOW_KELVIN, + Self::CARGO_SETPOINT_TEMP_KELVIN, + 1., + ), }; Self { - zone_id: zone_type.id(), + zone_id: zone_type, duct_demand_temperature: ThermodynamicTemperature::new::(24.), zone_selected_temperature: ThermodynamicTemperature::new::(24.), pid_controller, @@ -630,7 +656,7 @@ impl ZoneController { } } - fn update( + pub fn update( &mut self, context: &UpdateContext, acsc_id: AcscId, @@ -642,19 +668,21 @@ impl ZoneController { self.zone_selected_temperature = if !is_enabled { // If unpowered or failed, the zone is maintained at fixed temperature ThermodynamicTemperature::new::(24.) + } else if matches!(self.zone_id, ZoneType::Cargo(1)) { + acs_overhead.selected_cargo_temperature(self.zone_id) } else { - acs_overhead.selected_cabin_temperature(self.zone_id) + acs_overhead.selected_cabin_temperature(self.zone_id.id()) }; 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 + // Cabin inlet duct is constant at 15C, cockpit air is unaffected ThermodynamicTemperature::new::(15.) } else { self.calculate_duct_temp_demand( context, pressurization, - zone_measured_temperature[self.zone_id], + zone_measured_temperature[self.zone_id.id()], ) }; } @@ -753,17 +781,19 @@ 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) { +impl DuctTemperature for ZoneController { + fn duct_demand_temperature(&self) -> Vec { + vec![self.duct_demand_temperature] + } +} + +impl SimulationElement for ZoneController { + fn accept(&mut self, visitor: &mut T) { self.galley_fan_failure.accept(visitor); visitor.visit(self); } @@ -1106,7 +1136,7 @@ impl TrimAirSystemController TrimAirSystemController, pneumatic: &impl PackFlowValveState, ) -> bool { - acs_overhead.hot_air_pushbutton_is_on() + acs_overhead.hot_air_pushbutton_is_on(1) && is_enabled && !pack_flow_controller.pack_start_condition_determination(pneumatic) && ((pneumatic.pack_flow_valve_is_open(1)) || (pneumatic.pack_flow_valve_is_open(2))) @@ -1187,13 +1217,13 @@ impl TrimAirSystemController( Self::DUCT_OVERHEAT_RESET_LIMIT, ) - && acs_overhead.hot_air_pushbutton_is_on())) + && acs_overhead.hot_air_pushbutton_is_on(1))) { 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() + && !acs_overhead.hot_air_pushbutton_is_on(1) { self.overheat_timer[zone_id] = Duration::default(); false @@ -1282,19 +1312,19 @@ impl PneumaticValveSignal for TrimAirValveSignal { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct TrimAirPressureRegulatingValveController { should_open_taprv: bool, } impl TrimAirPressureRegulatingValveController { - fn new() -> Self { + pub fn new() -> Self { Self { should_open_taprv: false, } } - fn update(&mut self, should_open_taprv: bool) { + pub fn update(&mut self, should_open_taprv: bool) { self.should_open_taprv = should_open_taprv } } @@ -1316,14 +1346,14 @@ pub struct TrimAirValveController { } impl TrimAirValveController { - fn new() -> Self { + pub fn new() -> Self { Self { tav_open_allowed: false, pid: PidController::new(0.0002, 0.005, 0., 0., 1., 24., 1.), } } - fn update( + pub fn update( &mut self, context: &UpdateContext, open_allowed: bool, @@ -1355,9 +1385,10 @@ impl ControllerSignal for TrimAirValveController { } } -pub enum CabinFansSignal { - On, - Off, +impl Default for TrimAirValveController { + fn default() -> Self { + Self::new() + } } #[derive(Clone, Copy)] @@ -1383,7 +1414,7 @@ impl CabinFanController { impl ControllerSignal for CabinFanController { fn signal(&self) -> Option { if self.is_enabled { - Some(CabinFansSignal::On) + Some(CabinFansSignal::On(None)) } else { Some(CabinFansSignal::Off) } @@ -1396,7 +1427,7 @@ mod acs_controller_tests { use crate::{ air_conditioning::{ cabin_air::CabinAirSimulation, Air, AirConditioningPack, CabinFan, MixerUnit, - OutletAir, PressurizationConstants, + OutletAir, PressurizationConstants, VcmShared, }, electrical::{test::TestElectricitySource, ElectricalBus, Electricity}, overhead::{ @@ -1425,6 +1456,7 @@ mod acs_controller_tests { thermodynamic_temperature::degree_celsius, velocity::knot, volume::cubic_meter, + volume_rate::liter_per_second, }; struct TestAcsOverhead { @@ -1487,7 +1519,7 @@ mod acs_controller_tests { self.pack_pbs.iter().map(|pack| pack.is_on()).collect() } - fn hot_air_pushbutton_is_on(&self) -> bool { + fn hot_air_pushbutton_is_on(&self, _hot_air_id: usize) -> bool { self.hot_air_pb } @@ -2051,14 +2083,14 @@ mod acs_controller_tests { } struct TestAirConditioningSystem { - duct_temperature: Vec, + duct_temperature: [ThermodynamicTemperature; 2], outlet_air: Air, } impl TestAirConditioningSystem { fn new() -> Self { Self { - duct_temperature: vec![ThermodynamicTemperature::new::(24.); 2], + duct_temperature: [ThermodynamicTemperature::new::(24.); 2], outlet_air: Air::new(), } } @@ -2069,21 +2101,23 @@ mod acs_controller_tests { pack_flow: MassRate, trim_air_pressure: Pressure, ) { - self.duct_temperature = duct_temperature; + self.duct_temperature = duct_temperature + .try_into() + .expect("slice with incorrect length"); self.outlet_air.set_flow_rate(pack_flow); self.outlet_air.set_pressure(trim_air_pressure); self.outlet_air .set_temperature(self.duct_temperature.iter().average()); } - fn duct_temperature(&self) -> Vec { - self.duct_temperature.to_vec() + fn duct_temperature(&self) -> &[ThermodynamicTemperature] { + &self.duct_temperature } } impl DuctTemperature for TestAirConditioningSystem { fn duct_temperature(&self) -> Vec { - self.duct_temperature() + self.duct_temperature().to_vec() } } @@ -2093,6 +2127,8 @@ mod acs_controller_tests { } } + impl VcmShared for TestAirConditioningSystem {} + struct TestCabinAirSimulation { cabin_air_simulation: CabinAirSimulation, test_cabin_temperature: Option>, @@ -2112,7 +2148,7 @@ mod acs_controller_tests { fn update( &mut self, context: &UpdateContext, - air_conditioning_system: &(impl OutletAir + DuctTemperature), + air_conditioning_system: &(impl OutletAir + DuctTemperature + VcmShared), outflow_valve_open_amount: Ratio, safety_valve_open_amount: Ratio, lgciu_gear_compressed: bool, @@ -2164,8 +2200,10 @@ mod acs_controller_tests { struct TestConstants; impl PressurizationConstants for TestConstants { - const CABIN_VOLUME_CUBIC_METER: f64 = 139.; // m3 + const CABIN_ZONE_VOLUME_CUBIC_METER: f64 = 139.; // m3 const COCKPIT_VOLUME_CUBIC_METER: f64 = 9.; // m3 + const FWD_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 89.4; // m3 + const BULK_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 14.3; // m3 const PRESSURIZED_FUSELAGE_VOLUME_CUBIC_METER: f64 = 330.; // m3 const CABIN_LEAKAGE_AREA: f64 = 0.0003; // m2 const OUTFLOW_VALVE_SIZE: f64 = 0.05; // m2 @@ -2219,6 +2257,8 @@ mod acs_controller_tests { dc_ess_bus: ElectricalBus, } impl TestAircraft { + const CAB_FAN_DESIGN_FLOW_RATE_L_S: f64 = 325.; // litres/sec + fn new(context: &mut InitContext) -> Self { let cabin_zones = [ZoneType::Cockpit, ZoneType::Cabin(1)]; @@ -2259,8 +2299,16 @@ mod acs_controller_tests { adirs: TestAdirs::new(), air_conditioning_system: TestAirConditioningSystem::new(), cabin_fans: [ - CabinFan::new(1, ElectricalBusType::AlternatingCurrent(1)), - CabinFan::new(2, ElectricalBusType::AlternatingCurrent(2)), + CabinFan::new( + 1, + VolumeRate::new::(Self::CAB_FAN_DESIGN_FLOW_RATE_L_S), + ElectricalBusType::AlternatingCurrent(1), + ), + CabinFan::new( + 2, + VolumeRate::new::(Self::CAB_FAN_DESIGN_FLOW_RATE_L_S), + ElectricalBusType::AlternatingCurrent(2), + ), ], engine_1: TestEngine::new(Ratio::default()), engine_2: TestEngine::new(Ratio::default()), @@ -2268,8 +2316,8 @@ mod acs_controller_tests { mixer_unit: MixerUnit::new(&cabin_zones), number_of_passengers: 0, packs: [ - AirConditioningPack::new(Pack(1)), - AirConditioningPack::new(Pack(2)), + AirConditioningPack::new(context, Pack(1)), + AirConditioningPack::new(context, Pack(2)), ], pneumatic: TestPneumatic::new(context), pressurization: TestPressurization::new(), @@ -2277,7 +2325,13 @@ mod acs_controller_tests { lgciu1: TestLgciu::new(false), lgciu2: TestLgciu::new(false), cabin_air_simulation: TestCabinAirSimulation::new(context), - trim_air_system: TrimAirSystem::new(context, &cabin_zones, &[1]), + trim_air_system: TrimAirSystem::new( + context, + &cabin_zones, + &[1], + Volume::new::(4.), + Volume::new::(0.03), + ), powered_dc_source_1: TestElectricitySource::powered( context, PotentialOrigin::Battery(1), @@ -2470,9 +2524,9 @@ mod acs_controller_tests { 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].trim_air_pressure_regulating_valve_controller(), + self.acsc[1].trim_air_pressure_regulating_valve_controller(), ], &[&self.acsc[0], &self.acsc[1]], ); @@ -2799,6 +2853,7 @@ mod acs_controller_tests { fn duct_temperature(&self) -> Vec { self.query(|a| a.trim_air_system.duct_temperature()) + .to_vec() } fn pack_flow(&self) -> MassRate { @@ -4115,6 +4170,7 @@ mod acs_controller_tests { assert!( (test_bed.mixer_unit_outlet_air().flow_rate() - MassRate::new::(1.683)) + .abs() < MassRate::new::(0.1) ) } diff --git a/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_air.rs b/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_air.rs index 70f5556d937..fe5d7c8c6c9 100644 --- a/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_air.rs +++ b/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_air.rs @@ -1,4 +1,4 @@ -use super::{Air, DuctTemperature, OutletAir, PressurizationConstants, ZoneType}; +use super::{Air, DuctTemperature, OutletAir, PressurizationConstants, VcmShared, ZoneType}; use crate::{ shared::{AverageExt, CabinSimulation}, simulation::{ @@ -34,6 +34,7 @@ pub struct CabinAirSimulation { air_in: Air, air_out: Air, internal_air: Air, + cargo_air_in: Air, cabin_zones: [CabinZone; ZONES], @@ -55,6 +56,7 @@ impl CabinAirSimulation CabinAirSimulation CabinAirSimulation CabinAirSimulation ThermodynamicTemperature { if lgciu_gear_compressed { // If the aircraft is on the ground the cabin starts at the same temperature as ambient - self.cabin_zones - .iter_mut() - .for_each(|zone| zone.set_zone_air_temperature(context.ambient_temperature())); + self.command_cabin_temperature(context.ambient_temperature()); context.ambient_temperature() } else { // If the aircraft is flying we assume the temperature has been stabilized at 24 degrees - self.cabin_zones.iter_mut().for_each(|zone| { - zone.set_zone_air_temperature(ThermodynamicTemperature::new::(24.)) - }); + self.command_cabin_temperature(ThermodynamicTemperature::new::(24.)); ThermodynamicTemperature::new::(24.) } } + fn flow_rate_per_cubic_meter(&self) -> MassRate { + self.air_in.flow_rate() + / (C::COCKPIT_VOLUME_CUBIC_METER + + C::CABIN_ZONE_VOLUME_CUBIC_METER + * self + .cabin_zones + .iter() + .filter(|zone| matches!(&zone.zone_id(), &ZoneType::Cabin(_))) + .count() as f64 + + C::FWD_CARGO_ZONE_VOLUME_CUBIC_METER + * self + .cabin_zones + .iter() + .any(|zone| matches!(&zone.zone_id(), &ZoneType::Cargo(1))) + as usize as f64) + // The bulk cargo is fed with air from the cabin + } + + fn flow_rate_determination(&self, vcm_shared: &impl VcmShared) -> Vec { + let mut flow_rate_per_cubic_meter = Vec::new(); + for zone in self.cabin_zones.iter() { + flow_rate_per_cubic_meter.push( + if matches!(zone.zone_id(), ZoneType::Cargo(1)) + && !vcm_shared.fwd_extraction_fan_is_on() + || matches!(zone.zone_id(), ZoneType::Cargo(2)) + && !vcm_shared.bulk_extraction_fan_is_on() + { + MassRate::default() + } else { + self.flow_rate_per_cubic_meter() + }, + ) + } + flow_rate_per_cubic_meter + } + fn calculate_cabin_flow_in(&self, context: &UpdateContext, flow_in: MassRate) -> MassRate { // Placeholder until packs are modelled to prevent sudden changes in flow const INTERNAL_FLOW_RATE_CHANGE: f64 = 0.1; @@ -306,6 +345,12 @@ impl CabinAirSimulation(pressure_change_mass + pressure_change_temperature) } + + pub fn command_cabin_temperature(&mut self, temperature: ThermodynamicTemperature) { + self.cabin_zones + .iter_mut() + .for_each(|zone| zone.set_zone_air_temperature(temperature)); + } } impl CabinSimulation @@ -339,7 +384,7 @@ impl SimulationElement pub struct CabinZone { zone_identifier: VariableIdentifier, - zone_id: usize, + zone_id: ZoneType, zone_air: ZoneAir, zone_volume: Volume, passengers: u8, @@ -349,16 +394,27 @@ pub struct CabinZone { impl CabinZone { pub fn new(context: &mut InitContext, zone_id: &ZoneType) -> Self { - let (passengers, zone_volume) = if matches!(zone_id, &ZoneType::Cockpit) { - (2, Volume::new::(C::COCKPIT_VOLUME_CUBIC_METER)) - } else { - (0, Volume::new::(C::CABIN_VOLUME_CUBIC_METER)) + let (passengers, zone_volume) = match *zone_id { + ZoneType::Cockpit => (2, Volume::new::(C::COCKPIT_VOLUME_CUBIC_METER)), + ZoneType::Cabin(_) => ( + 0, + Volume::new::(C::CABIN_ZONE_VOLUME_CUBIC_METER), + ), + ZoneType::Cargo(1) => ( + 0, + Volume::new::(C::FWD_CARGO_ZONE_VOLUME_CUBIC_METER), + ), + ZoneType::Cargo(2) => ( + 0, + Volume::new::(C::BULK_CARGO_ZONE_VOLUME_CUBIC_METER), + ), + _ => panic!("Something went wrong with assigning volume to zone"), }; Self { zone_identifier: context.get_identifier(format!("COND_{}_TEMP", zone_id)), - zone_id: zone_id.id(), + zone_id: *zone_id, zone_air: ZoneAir::new(), zone_volume, passengers, @@ -377,7 +433,7 @@ impl CabinZone { number_of_open_doors: u8, ) { let mut air_in = Air::new(); - air_in.set_temperature(duct_temperature.duct_temperature()[self.zone_id]); + air_in.set_temperature(duct_temperature.duct_temperature()[self.zone_id.id()]); air_in.set_flow_rate(pack_flow_per_cubic_meter * self.zone_volume.get::()); self.passengers = passengers; @@ -391,7 +447,7 @@ impl CabinZone { ); } - fn zone_id(&self) -> usize { + fn zone_id(&self) -> ZoneType { self.zone_id } @@ -495,7 +551,7 @@ impl ZoneAir { .heat_transfer_through_wall_calculation(context, zone_volume) .get::(); // For the cockpit we reduce the effect of opening doors to 20% - if zone_volume < Volume::new::(100.) { + if zone_volume < Volume::new::(20.) { inlet_door_air_energy *= 0.2; outlet_door_air_energy *= 0.2; } @@ -714,11 +770,15 @@ mod cabin_air_tests { } } + impl VcmShared for TestAirConditioningSystem {} + struct TestConstants; impl PressurizationConstants for TestConstants { - const CABIN_VOLUME_CUBIC_METER: f64 = 139.; // m3 + const CABIN_ZONE_VOLUME_CUBIC_METER: f64 = 139.; // m3 const COCKPIT_VOLUME_CUBIC_METER: f64 = 9.; // m3 + const FWD_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 89.4; // m3 + const BULK_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 14.3; // m3 const PRESSURIZED_FUSELAGE_VOLUME_CUBIC_METER: f64 = 330.; // m3 const CABIN_LEAKAGE_AREA: f64 = 0.0003; // m2 const OUTFLOW_VALVE_SIZE: f64 = 0.05; // m2 diff --git a/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_pressure_controller.rs b/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_pressure_controller.rs index 12027d51c6a..5e7fad6d697 100644 --- a/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_pressure_controller.rs +++ b/fbw-common/src/wasm/systems/systems/src/air_conditioning/cabin_pressure_controller.rs @@ -1030,6 +1030,7 @@ transition!(ClimbInternal, Abort); #[cfg(test)] mod tests { use super::*; + use crate::air_conditioning::VcmShared; use crate::shared::ElectricalBusType; use crate::simulation::{Aircraft, InitContext, SimulationElement, SimulationElementVisitor}; use crate::{ @@ -1132,6 +1133,8 @@ mod tests { } } + impl VcmShared for TestAirConditioningSystem {} + struct TestEngine { corrected_n1: Ratio, } @@ -1263,8 +1266,10 @@ mod tests { struct TestConstants; impl PressurizationConstants for TestConstants { - const CABIN_VOLUME_CUBIC_METER: f64 = 139.; // m3 + const CABIN_ZONE_VOLUME_CUBIC_METER: f64 = 139.; // m3 const COCKPIT_VOLUME_CUBIC_METER: f64 = 9.; // m3 + const FWD_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 89.4; // m3 + const BULK_CARGO_ZONE_VOLUME_CUBIC_METER: f64 = 14.3; // m3 const PRESSURIZED_FUSELAGE_VOLUME_CUBIC_METER: f64 = 330.; // m3 const CABIN_LEAKAGE_AREA: f64 = 0.0003; // m2 const OUTFLOW_VALVE_SIZE: f64 = 0.05; // m2 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 a55163a6ed9..1b81814ec83 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,4 +1,4 @@ -use self::acs_controller::{CabinFansSignal, Pack, TrimAirValveController, TrimAirValveSignal}; +use self::acs_controller::{Pack, TrimAirValveController, TrimAirValveSignal}; use crate::{ failures::{Failure, FailureType}, @@ -28,16 +28,21 @@ use uom::si::{ ratio::percent, thermodynamic_temperature::{degree_celsius, kelvin}, volume::cubic_meter, + volume_rate::cubic_meter_per_second, }; pub mod acs_controller; pub mod cabin_air; pub mod cabin_pressure_controller; -pub mod full_digital_agu_controller; pub mod pressure_valve; pub trait DuctTemperature { - fn duct_temperature(&self) -> Vec; + fn duct_temperature(&self) -> Vec { + vec![ThermodynamicTemperature::default()] + } + fn duct_demand_temperature(&self) -> Vec { + vec![ThermodynamicTemperature::default()] + } } pub trait PackFlow { @@ -72,6 +77,25 @@ impl PneumaticValveSignal for PackFlowValveSignal { } } +pub enum CabinFansSignal { + On(Option), + Off, +} + +impl CabinFansSignal { + fn recirculation_flow_demand(&self) -> Option { + match self { + CabinFansSignal::On(flow_demand) => *flow_demand, + CabinFansSignal::Off => None, + } + } +} + +pub enum BulkHeaterSignal { + On, + Off, +} + pub trait OutletAir { fn outlet_air(&self) -> Air; } @@ -85,10 +109,22 @@ pub trait AdirsToAirCondInterface { pub trait AirConditioningOverheadShared { fn selected_cabin_temperature(&self, zone_id: usize) -> ThermodynamicTemperature; + fn selected_cargo_temperature(&self, _zone_id: ZoneType) -> ThermodynamicTemperature { + ThermodynamicTemperature::new::(15.) + } fn pack_pushbuttons_state(&self) -> Vec; - fn hot_air_pushbutton_is_on(&self) -> bool; + fn hot_air_pushbutton_is_on(&self, hot_air_id: usize) -> bool; fn cabin_fans_is_on(&self) -> bool; fn flow_selector_position(&self) -> OverheadFlowSelector; + fn fwd_cargo_isolation_valve_is_on(&self) -> bool { + false + } + fn bulk_isolation_valve_is_on(&self) -> bool { + false + } + fn bulk_cargo_heater_is_on(&self) -> bool { + false + } } pub trait PressurizationOverheadShared { @@ -98,6 +134,27 @@ pub trait PressurizationOverheadShared { fn ldg_elev_knob_value(&self) -> f64; } +pub trait VcmShared { + fn hp_cabin_fans_are_enabled(&self) -> bool { + false + } + fn fwd_extraction_fan_is_on(&self) -> bool { + false + } + fn fwd_isolation_valves_open_allowed(&self) -> bool { + false + } + fn bulk_duct_heater_on_allowed(&self) -> bool { + false + } + fn bulk_extraction_fan_is_on(&self) -> bool { + false + } + fn bulk_isolation_valves_open_allowed(&self) -> bool { + false + } +} + /// Cabin Zones with double digit IDs are specific to the A380 /// 1X is main deck, 2X is upper deck #[derive(Clone, Copy, Eq, PartialEq)] @@ -234,45 +291,56 @@ impl From for Channel { } } -struct OperatingChannel { +pub struct OperatingChannel { channel_id: Channel, powered_by: Vec, is_powered: bool, - failure: Failure, + failure: Option, fault: OperatingChannelFault, } impl OperatingChannel { - fn new(id: usize, failure_type: FailureType, powered_by: &[ElectricalBusType]) -> Self { + pub fn new( + id: usize, + failure_type: Option, + powered_by: &[ElectricalBusType], + ) -> Self { Self { channel_id: id.into(), powered_by: powered_by.to_vec(), is_powered: false, - failure: Failure::new(failure_type), + failure: failure_type.map(Failure::new), fault: OperatingChannelFault::NoFault, } } - fn update_fault(&mut self) { - self.fault = if !self.is_powered || self.failure.is_active() { + pub fn update_fault(&mut self) { + let failure_is_active = self + .failure + .as_ref() + .map_or(false, |failure| failure.is_active()); + + self.fault = if !self.is_powered || failure_is_active { OperatingChannelFault::Fault } else { OperatingChannelFault::NoFault }; } - fn has_fault(&self) -> bool { + pub fn has_fault(&self) -> bool { matches!(self.fault, OperatingChannelFault::Fault) } - fn id(&self) -> Channel { + pub fn id(&self) -> Channel { self.channel_id } } impl SimulationElement for OperatingChannel { fn accept(&mut self, visitor: &mut T) { - self.failure.accept(visitor); + if let Some(failure) = &mut self.failure { + failure.accept(visitor); + } visitor.visit(self); } @@ -282,8 +350,10 @@ impl SimulationElement for OperatingChannel { } pub trait PressurizationConstants { - const CABIN_VOLUME_CUBIC_METER: f64; + const CABIN_ZONE_VOLUME_CUBIC_METER: f64; const COCKPIT_VOLUME_CUBIC_METER: f64; + const FWD_CARGO_ZONE_VOLUME_CUBIC_METER: f64; + const BULK_CARGO_ZONE_VOLUME_CUBIC_METER: f64; const PRESSURIZED_FUSELAGE_VOLUME_CUBIC_METER: f64; const CABIN_LEAKAGE_AREA: f64; const OUTFLOW_VALVE_SIZE: f64; @@ -308,6 +378,7 @@ pub trait PressurizationConstants { /// A320neo fan part number: VD3900-03 pub struct CabinFan { + design_flow_rate: VolumeRate, is_on: bool, outlet_air: Air, @@ -317,12 +388,12 @@ pub struct CabinFan { } impl CabinFan { - const DESIGN_FLOW_RATE_L_S: f64 = 325.; // litres/sec const PRESSURE_RISE_HPA: f64 = 22.; // hPa const FAN_EFFICIENCY: f64 = 0.75; // Ratio - so output matches AMM numbers - pub fn new(id: u8, powered_by: ElectricalBusType) -> Self { + pub fn new(id: u8, design_flow_rate: VolumeRate, powered_by: ElectricalBusType) -> Self { Self { + design_flow_rate, is_on: false, outlet_air: Air::new(), @@ -344,7 +415,7 @@ impl CabinFan { if !self.is_powered || self.failure.is_active() - || !matches!(controller.signal(), Some(CabinFansSignal::On)) + || !matches!(controller.signal(), Some(CabinFansSignal::On(_))) { self.is_on = false; self.outlet_air @@ -356,16 +427,23 @@ impl CabinFan { cabin_simulation.cabin_pressure() + Pressure::new::(Self::PRESSURE_RISE_HPA), ); - self.outlet_air - .set_flow_rate(self.mass_flow_calculation() * Self::FAN_EFFICIENCY); + + self.outlet_air.set_flow_rate( + self.mass_flow_calculation( + controller.signal().unwrap().recirculation_flow_demand(), + ), + ); } } - fn mass_flow_calculation(&self) -> MassRate { - let mass_flow: f64 = - (self.outlet_air.pressure().get::() * Self::DESIGN_FLOW_RATE_L_S * 1e-3) - / (Air::R * self.outlet_air.temperature().get::()); - MassRate::new::(mass_flow) + fn mass_flow_calculation(&self, recirculation_flow_demand: Option) -> MassRate { + let mass_flow: f64 = (self.outlet_air.pressure().get::() + * self.design_flow_rate.get::()) + / (Air::R * self.outlet_air.temperature().get::()); + // If we have flow demand calculated, we assign this directly to the fan flow + // This is a simplification, we could model the fans, send the signal and read the output + recirculation_flow_demand + .unwrap_or(MassRate::new::(mass_flow) * Self::FAN_EFFICIENCY) } pub fn has_fault(&self) -> bool { @@ -501,6 +579,8 @@ impl OutletAir for MixerUnitOutlet { /// Temporary struct until packs are fully simulated pub struct AirConditioningPack { + pack_outlet_temperature_id: VariableIdentifier, + pack_id: Pack, outlet_temperature: LowPassFilter, // Degree Celsius outlet_air: Air, @@ -508,8 +588,13 @@ pub struct AirConditioningPack { impl AirConditioningPack { const PACK_REACTION_TIME: Duration = Duration::from_secs(10); - pub fn new(pack_id: Pack) -> Self { + pub fn new(context: &mut InitContext, pack_id: Pack) -> Self { Self { + pack_outlet_temperature_id: context.get_identifier(format!( + "COND_PACK_{}_OUTLET_TEMPERATURE", + usize::from(pack_id) + )), + pack_id, outlet_temperature: LowPassFilter::new_with_init_value(Self::PACK_REACTION_TIME, 15.), outlet_air: Air::new(), @@ -553,6 +638,15 @@ impl OutletAir for AirConditioningPack { } } +impl SimulationElement for AirConditioningPack { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write( + &self.pack_outlet_temperature_id, + self.outlet_temperature.output(), + ); + } +} + pub struct TrimAirSystem { duct_temperature_id: [VariableIdentifier; ZONES], @@ -571,21 +665,24 @@ impl TrimAirSystem { context: &mut InitContext, cabin_zone_ids: &[ZoneType; ZONES], taprv_ids: &[usize], + pack_mixer_container_volume: Volume, + trim_air_valve_container_volume: Volume, ) -> 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| TrimAirPressureRegulatingValve::new(*id)) - .collect::>(); + .collect(); Self { duct_temperature_id, trim_air_pressure_regulating_valves, - trim_air_valves: cabin_zone_ids.map(|id| TrimAirValve::new(context, &id)), + trim_air_valves: cabin_zone_ids + .map(|id| TrimAirValve::new(context, trim_air_valve_container_volume, &id)), pack_mixer_container: PneumaticPipe::new( - Volume::new::(4.), + pack_mixer_container_volume, Pressure::new::(14.7), ThermodynamicTemperature::new::(15.), ), @@ -600,29 +697,39 @@ impl TrimAirSystem { &mut self, context: &UpdateContext, mixer_air: &MixerUnit, - taprv_controller: &[&impl ControllerSignal], + taprv_controller: [impl ControllerSignal; 2], 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 + // This section needs to be modified if in the future this code is used for an aircaft + // with more than one TAPRV with a non matching number of TAPRV controllers + if self.trim_air_pressure_regulating_valves.len() == taprv_controller.len() { + self.trim_air_pressure_regulating_valves + .iter_mut() + .zip(taprv_controller) + .for_each(|(taprv, controller)| { + taprv.update(context, &mut self.pack_mixer_container, &controller) + }) + } else { + 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(), + ) + }); + } + for (id, tav) in self.trim_air_valves.iter_mut().enumerate() { tav.update( context, @@ -665,7 +772,7 @@ impl TrimAirSystem { / (pack_container[0].mass().get::() + pack_container[1].mass().get::()); self.pack_mixer_container = PneumaticPipe::new( - Volume::new::(4.), + pack_container.iter().map(|pc| pc.volume()).sum(), Pressure::new::(combined_pressure), ThermodynamicTemperature::new::(combined_temperature), ); @@ -692,14 +799,11 @@ impl TrimAirSystem { 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()) + pub fn trim_air_pressure_regulating_valve_is_open(&self, id: usize) -> bool { + self.trim_air_pressure_regulating_valves[id - 1].is_open() } - #[cfg(test)] - fn trim_air_valves_open_amount(&self) -> [Ratio; ZONES] { + pub fn trim_air_valves_open_amount(&self) -> [Ratio; ZONES] { self.trim_air_valves .iter() .map(|tav| tav.trim_air_valve_open_amount()) @@ -709,14 +813,23 @@ impl TrimAirSystem { panic!("Expected a Vec of length {} but it was {}", ZONES, v.len()) }) } + + pub fn trim_air_system_valve_outlet_air(&self, id: usize) -> Air { + self.trim_air_valves[id].outlet_air() + } } impl DuctTemperature for TrimAirSystem { fn duct_temperature(&self) -> Vec { self.trim_air_mixers - .iter() .map(|tam| tam.outlet_temperature()) - .collect::>() + .to_vec() + } +} + +impl OutletAir for TrimAirSystem { + fn outlet_air(&self) -> Air { + self.outlet_air } } @@ -894,7 +1007,11 @@ struct TrimAirValve { impl TrimAirValve { const PRESSURE_DIFFERENCE_WITH_CABIN_PSI: f64 = 4.0611; // PSI; - fn new(context: &mut InitContext, zone_id: &ZoneType) -> Self { + fn new( + context: &mut InitContext, + trim_air_valve_container_volume: Volume, + zone_id: &ZoneType, + ) -> Self { Self { trim_air_valve_id: context .get_identifier(format!("COND_{}_TRIM_AIR_VALVE_POSITION", zone_id)), @@ -902,7 +1019,7 @@ impl TrimAirValve { 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 + trim_air_valve_container_volume, Pressure::new::(14.7 + Self::PRESSURE_DIFFERENCE_WITH_CABIN_PSI), ThermodynamicTemperature::new::(24.), ), @@ -982,6 +1099,89 @@ impl SimulationElement for TrimAirValve { } } +pub struct AirHeater { + is_on: bool, + outlet_air: Air, + + is_powered: bool, + powered_by: ElectricalBusType, +} + +impl AirHeater { + const OUTPUT_POWER: f64 = 400.; // Watt + const NOMINAL_FLOW_RATE: f64 = 161.; // Cubic meter / hour + + pub fn new(powered_by: ElectricalBusType) -> Self { + Self { + is_on: false, + outlet_air: Air::new(), + + is_powered: false, + powered_by, + } + } + + pub fn update( + &mut self, + cabin_simulation: &impl CabinSimulation, + inlet: &impl OutletAir, + controller: &impl ControllerSignal, + ) { + self.outlet_air + .set_pressure(cabin_simulation.cabin_pressure()); + + // We set the flow rate equal to the other zones for simplicity. Minimal error incurred. + self.outlet_air + .set_flow_rate(inlet.outlet_air().flow_rate()); + // TODO: This should come only from the lower deck. The error will be minimal by taking the whole average. + self.outlet_air + .set_temperature(cabin_simulation.cabin_temperature().iter().average()); + self.is_on = + if !self.is_powered || !matches!(controller.signal(), Some(BulkHeaterSignal::On)) { + false + } else { + let heater_flow_rate = self.mass_flow_calculation(); + + self.outlet_air + .set_temperature(self.heater_work_temperature_calculation(heater_flow_rate)); + true + } + } + + fn mass_flow_calculation(&self) -> MassRate { + let mass_flow: f64 = (self.outlet_air.pressure().get::() * Self::NOMINAL_FLOW_RATE + / 3600.) + / (Air::R * self.outlet_air.temperature().get::()); + MassRate::new::(mass_flow) + } + + fn heater_work_temperature_calculation( + &self, + heater_flow_rate: MassRate, + ) -> ThermodynamicTemperature { + let outlet_temperature = ((Self::OUTPUT_POWER / 1000.) + / (heater_flow_rate.get::() * Air::SPECIFIC_HEAT_CAPACITY_VOLUME)) + + self.outlet_air.temperature().get::(); + ThermodynamicTemperature::new::(outlet_temperature) + } + + pub fn is_on(&self) -> bool { + self.is_on + } +} + +impl OutletAir for AirHeater { + fn outlet_air(&self) -> Air { + self.outlet_air + } +} + +impl SimulationElement for AirHeater { + fn receive_power(&mut self, buses: &impl ElectricalBuses) { + self.is_powered = buses.is_powered(self.powered_by); + } +} + #[derive(Clone, Copy)] pub struct Air { temperature: ThermodynamicTemperature, diff --git a/fbw-common/src/wasm/systems/systems/src/hydraulic/cargo_doors.rs b/fbw-common/src/wasm/systems/systems/src/hydraulic/cargo_doors.rs index 1cdff75b9b3..1e6b410beda 100644 --- a/fbw-common/src/wasm/systems/systems/src/hydraulic/cargo_doors.rs +++ b/fbw-common/src/wasm/systems/systems/src/hydraulic/cargo_doors.rs @@ -236,7 +236,7 @@ impl CargoDoor { self.position } - fn is_locked(&self) -> bool { + pub fn is_locked(&self) -> bool { self.is_locked } diff --git a/fbw-common/src/wasm/systems/systems/src/shared/mod.rs b/fbw-common/src/wasm/systems/systems/src/shared/mod.rs index eb912aaee25..243fd69bbd4 100644 --- a/fbw-common/src/wasm/systems/systems/src/shared/mod.rs +++ b/fbw-common/src/wasm/systems/systems/src/shared/mod.rs @@ -90,6 +90,11 @@ pub trait FeedbackPositionPickoffUnit { fn angle(&self) -> Angle; } +pub trait CargoDoorLocked { + fn fwd_cargo_door_locked(&self) -> bool; + fn aft_cargo_door_locked(&self) -> bool; +} + pub trait LgciuWeightOnWheels { fn right_gear_compressed(&self, treat_ext_pwr_as_ground: bool) -> bool; fn right_gear_extended(&self, treat_ext_pwr_as_ground: bool) -> bool;