diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/engines.cfg b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/engines.cfg index 352a1cc1a3d..6da4e9d0c99 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/engines.cfg +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/engines.cfg @@ -36,7 +36,7 @@ fuel_flow_gain = 1 ; Gain on fuel flow inlet_area = 29 ; Square Feet, engine nacelle inlet area rated_N2_rpm = 12200 ; RPM, third stage compressor rated value static_thrust = 80213 ; Lbs, max rated static thrust at Sea Level -reverser_available = 0.5 +reverser_available = 0 reverser_mach_controlled = 0 afterburner_available = 0 afterburner_throttle_threshold = 0.011 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_EXTERIOR.xml b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_EXTERIOR.xml index cdbc5a247b1..4e32be8c33b 100755 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_EXTERIOR.xml +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_EXTERIOR.xml @@ -768,17 +768,17 @@ - - - 2 + thrust_rev_1 + (L:A32NX_REVERSER_2_POSITION) 100 * + 100 - - - 3 + thrust_rev_2 + (L:A32NX_REVERSER_3_POSITION) 100 * + 100 diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/behaviour/legacy/Exterior/A32NX_Exterior_Includes.xml b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/behaviour/legacy/Exterior/A32NX_Exterior_Includes.xml deleted file mode 100644 index 6fc39b3e9b8..00000000000 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/behaviour/legacy/Exterior/A32NX_Exterior_Includes.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/behaviour/legacy/generated/A32NX_Exterior_Engines.xml b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/behaviour/legacy/generated/A32NX_Exterior_Engines.xml deleted file mode 100644 index 3814a57cd60..00000000000 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/behaviour/legacy/generated/A32NX_Exterior_Engines.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - diff --git a/fbw-a380x/src/systems/instruments/src/Common/hooks.tsx b/fbw-a380x/src/systems/instruments/src/Common/hooks.tsx index 1838f62f055..db6c9f62b6c 100644 --- a/fbw-a380x/src/systems/instruments/src/Common/hooks.tsx +++ b/fbw-a380x/src/systems/instruments/src/Common/hooks.tsx @@ -35,7 +35,7 @@ export const useUpdate = (handler: (deltaTime: number) => void) => { return () => { getRootElement().removeEventListener('update', wrappedHandler); }; - }); + }, []); }; export const useInteractionEvent = (event: string, handler: (any?) => void): void => { diff --git a/fbw-a380x/src/systems/instruments/src/EWD/elements/ThrustGauge.tsx b/fbw-a380x/src/systems/instruments/src/EWD/elements/ThrustGauge.tsx index e0310808015..177e246cd10 100644 --- a/fbw-a380x/src/systems/instruments/src/EWD/elements/ThrustGauge.tsx +++ b/fbw-a380x/src/systems/instruments/src/EWD/elements/ThrustGauge.tsx @@ -18,10 +18,12 @@ const ThrustGauge: React.FC // const [thrustLimit] = useSimVar('L:A32NX_AUTOTHRUST_THRUST_LIMIT', 'number', 100); // const [thrustLimitIdle] = useSimVar('L:A32NX_AUTOTHRUST_THRUST_LIMIT_IDLE', 'number', 100); + const availVisible = !!(N1Percent > Math.floor(N1Idle) && engineState === 2); // N1Percent sometimes does not reach N1Idle by .005 or so - const [revVisible] = useSimVar(`L:A32NX_AUTOTHRUST_REVERSE:${engine}`, 'bool', 500); - // Reverse cowl > 5% is treated like fully open, otherwise REV will not turn green for idle reverse - const [revDoorOpenPercentage] = useSimVar(`A:TURB ENG REVERSE NOZZLE PERCENT:${engine}`, 'percent', 100); + const [revDoorOpened] = useSimVar(`L:A32NX_REVERSER_${engine}_DEPLOYED`, 'bool', 100); + const [revDoorTransittt] = useSimVar(`L:A32NX_REVERSER_${engine}_DEPLOYING`, 'bool', 100); + const [revAthr] = useSimVar(`L:A32NX_AUTOTHRUST_REVERSE:${engine}`, 'bool', 100); + const revVisible = revDoorTransittt || revDoorOpened || revAthr; const availRevVisible = availVisible || (revVisible && [2, 3].includes(engine)); const availRevText = availVisible ? 'AVAIL' : 'REV'; @@ -70,7 +72,7 @@ const ThrustGauge: React.FC visible={availVisible || engineState === 1} className='GaugeComponent GaugeThrustFill' /> - + /> {/* reverse */} - + multiplierInner={1.1} /> /> = ({ x, y, mesg, visible, revDoorOpen }) => ( @@ -262,7 +264,7 @@ const AvailRev: React.FC = ({ x, y, mesg, visible, revDoorOpen }) {mesg === 'REV' - && 5 ? 'Green' : 'Amber'}`} x={x - 8} y={y + 9}>REV} + && REV} {mesg === 'AVAIL' && AVAIL} 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 9169260b934..8d85e657422 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/lib.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/lib.rs @@ -12,6 +12,7 @@ mod navigation; mod payload; mod pneumatic; mod power_consumption; +mod reverser; mod structural_flex; use self::{ @@ -34,6 +35,8 @@ use icing::Icing; use navigation::A380RadioAltimeters; use payload::A380Payload; use power_consumption::A380PowerConsumption; +use reverser::{A380ReverserController, A380Reversers}; + use uom::si::{f64::Length, length::nautical_mile, quantities::Velocity, velocity::knot}; use systems::{ @@ -43,7 +46,7 @@ use systems::{ AuxiliaryPowerUnitOverheadPanel, Pw980ApuGenerator, Pw980Constants, Pw980StartMotor, }, electrical::{Electricity, ElectricitySource, ExternalPowerSource}, - engine::{trent_engine::TrentEngine, EngineFireOverheadPanel}, + engine::{reverser_thrust::ReverserForce, trent_engine::TrentEngine, EngineFireOverheadPanel}, enhanced_gpwc::EnhancedGroundProximityWarningComputer, landing_gear::{LandingGear, LandingGearControlInterfaceUnitSet}, navigation::adirs::{ @@ -90,6 +93,10 @@ pub struct A380 { egpwc: EnhancedGroundProximityWarningComputer, icing_simulation: Icing, structural_flex: A380StructuralFlex, + + engine_reverser_control: [A380ReverserController; 2], + reversers_assembly: A380Reversers, + reverse_thrust: ReverserForce, } impl A380 { pub fn new(context: &mut InitContext) -> A380 { @@ -159,6 +166,12 @@ impl A380 { icing_simulation: Icing::new(context), structural_flex: A380StructuralFlex::new(context), + engine_reverser_control: [ + A380ReverserController::new(context, 2), + A380ReverserController::new(context, 3), + ], + reversers_assembly: A380Reversers::new(context), + reverse_thrust: ReverserForce::new(context), } } } @@ -316,6 +329,26 @@ impl Aircraft for A380 { self.egpwc.update(&self.adirs, self.lgcius.lgciu1()); self.fuel.update(context); + + self.engine_reverser_control[0].update( + &self.engine_2, + self.lgcius.lgciu1(), + self.reversers_assembly.reverser_feedback(0), + ); + self.engine_reverser_control[1].update( + &self.engine_3, + self.lgcius.lgciu2(), + self.reversers_assembly.reverser_feedback(1), + ); + + self.reversers_assembly + .update(context, &self.engine_reverser_control); + + self.reverse_thrust.update( + context, + [&self.engine_2, &self.engine_3], + self.reversers_assembly.reversers_position(), + ); } } impl SimulationElement for A380 { @@ -355,6 +388,10 @@ impl SimulationElement for A380 { self.icing_simulation.accept(visitor); self.structural_flex.accept(visitor); + accept_iterable!(self.engine_reverser_control, visitor); + self.reversers_assembly.accept(visitor); + self.reverse_thrust.accept(visitor); + visitor.visit(self); } } diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/reverser/mod.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/reverser/mod.rs new file mode 100644 index 00000000000..ee7b95c6248 --- /dev/null +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/reverser/mod.rs @@ -0,0 +1,301 @@ +use std::fmt::Debug; + +use systems::{ + accept_iterable, + engine::{ + reverser::{A380ReverserAssembly, ElecReverserInterface, ReverserFeedback}, + Engine, + }, + shared::{ElectricalBusType, LgciuWeightOnWheels, ReverserPosition}, + simulation::{ + InitContext, Read, SimulationElement, SimulationElementVisitor, SimulatorReader, + SimulatorWriter, UpdateContext, VariableIdentifier, Write, + }, +}; + +use uom::si::{ + angle::degree, + f64::*, + ratio::{percent, ratio}, +}; + +#[derive(Clone, Copy, Debug, PartialEq)] +enum ReverserControlState { + StowedOff, + StowedOn, + TransitOpening, + TransitClosing, + FullyOpened, +} + +pub struct A380ReverserController { + throttle_lever_angle_id: VariableIdentifier, + + throttle_lever_angle: Angle, + + state: ReverserControlState, + + primary_lock_from_prim_should_unlock: bool, + secondary_lock_from_prim_should_unlock: bool, + tertiary_lock_from_prim_should_unlock: bool, +} +impl A380ReverserController { + const FIRST_LINE_OF_DEFENCE_TLA_ANGLE_DEGREE: f64 = -3.; + const SECOND_LINE_OF_DEFENCE_TLA_ANGLE_DEGREE: f64 = -3.5; + const THIRD_LINE_OF_DEFENCE_TLA_ANGLE_DEGREE: f64 = -4.; + + const OPENING_AUTHORIZATION_TLA_ANGLE_DEGREE: f64 = -4.3; + const MIN_N2_ENGINE_TO_ALLOW_OPENING_PCT: f64 = 50.; + + const POSITION_FEEDBACK_THRESHOLD_FOR_REPORTING_STOWED: f64 = 0.1; + + pub fn new(context: &mut InitContext, engine_number: usize) -> Self { + Self { + throttle_lever_angle_id: context + .get_identifier(format!("AUTOTHRUST_TLA:{}", engine_number)), + + throttle_lever_angle: Angle::default(), + state: ReverserControlState::StowedOff, + + primary_lock_from_prim_should_unlock: false, + secondary_lock_from_prim_should_unlock: false, + tertiary_lock_from_prim_should_unlock: false, + } + } + + fn first_line_of_defense_condition_should_unlock( + &self, + lgciu: &impl LgciuWeightOnWheels, + ) -> bool { + self.throttle_lever_angle.get::() <= Self::FIRST_LINE_OF_DEFENCE_TLA_ANGLE_DEGREE + && lgciu.left_and_right_gear_compressed(false) + } + + fn second_line_of_defense_condition_should_unlock( + &self, + lgciu: &impl LgciuWeightOnWheels, + ) -> bool { + // TODO Should be a switch info from throttle assembly, using a TLA angle value as placeholder + self.throttle_lever_angle.get::() <= Self::SECOND_LINE_OF_DEFENCE_TLA_ANGLE_DEGREE + && lgciu.left_and_right_gear_compressed(false) + } + + fn third_line_of_defense_condition_should_unlock(&self) -> bool { + // TODO This should come from PRIM independant data + self.throttle_lever_angle.get::() <= Self::THIRD_LINE_OF_DEFENCE_TLA_ANGLE_DEGREE + } + + pub fn update( + &mut self, + engine: &impl Engine, + lgciu: &impl LgciuWeightOnWheels, + reverser_feedback: &impl ReverserFeedback, + ) { + let is_confirmed_stowed_available_for_deploy = + reverser_feedback.position_sensor().get::() + <= Self::POSITION_FEEDBACK_THRESHOLD_FOR_REPORTING_STOWED + && reverser_feedback.proximity_sensor_all_stowed(); + + let deploy_authorized = engine.corrected_n2().get::() + > Self::MIN_N2_ENGINE_TO_ALLOW_OPENING_PCT + && lgciu.left_and_right_gear_compressed(false); + + let command_opening = self.throttle_lever_angle.get::() + <= Self::OPENING_AUTHORIZATION_TLA_ANGLE_DEGREE; + + self.primary_lock_from_prim_should_unlock = + self.first_line_of_defense_condition_should_unlock(lgciu); + self.secondary_lock_from_prim_should_unlock = + self.second_line_of_defense_condition_should_unlock(lgciu); + self.tertiary_lock_from_prim_should_unlock = + self.third_line_of_defense_condition_should_unlock(); + + self.state = match self.state { + ReverserControlState::StowedOff => { + if deploy_authorized && is_confirmed_stowed_available_for_deploy { + ReverserControlState::StowedOn + } else { + self.state + } + } + ReverserControlState::StowedOn => { + if command_opening { + ReverserControlState::TransitOpening + } else { + self.state + } + } + ReverserControlState::TransitOpening => { + if reverser_feedback.proximity_sensor_all_deployed() { + ReverserControlState::FullyOpened + } else if !deploy_authorized || !command_opening { + ReverserControlState::TransitClosing + } else { + self.state + } + } + ReverserControlState::TransitClosing => { + if reverser_feedback.proximity_sensor_all_stowed() { + ReverserControlState::StowedOff + } else if command_opening && deploy_authorized { + ReverserControlState::TransitOpening + } else { + self.state + } + } + ReverserControlState::FullyOpened => { + if !deploy_authorized || !command_opening { + ReverserControlState::TransitClosing + } else { + self.state + } + } + }; + } +} +impl SimulationElement for A380ReverserController { + fn read(&mut self, reader: &mut SimulatorReader) { + self.throttle_lever_angle = reader.read(&self.throttle_lever_angle_id); + } +} +impl ElecReverserInterface for A380ReverserController { + fn should_unlock_first(&self) -> bool { + self.primary_lock_from_prim_should_unlock + } + + fn should_unlock_second(&self) -> bool { + self.secondary_lock_from_prim_should_unlock + } + + fn should_unlock_third(&self) -> bool { + self.tertiary_lock_from_prim_should_unlock + } + + fn should_deploy_reverser(&self) -> bool { + self.state == ReverserControlState::TransitOpening + || self.state == ReverserControlState::FullyOpened + } +} +impl Debug for A380ReverserController { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "\nREV Controller => STATE: {:?}/ should_unlock {:?}/{:?}/{:?} / should_deploy_reverser{:?}", + self.state, self.should_unlock_first(),self.should_unlock_second(),self.should_unlock_third(), + self.should_deploy_reverser(), + ) + } +} + +pub struct A380Reversers { + reverser_2_position_id: VariableIdentifier, + reverser_3_position_id: VariableIdentifier, + + reverser_2_in_transition_id: VariableIdentifier, + reverser_3_in_transition_id: VariableIdentifier, + + reverser_2_deployed_id: VariableIdentifier, + reverser_3_deployed_id: VariableIdentifier, + + reversers: [A380ReverserAssembly; 2], + + reversers_in_transition: [bool; 2], + reversers_deployed: [bool; 2], +} +impl A380Reversers { + // TODO use correct electrical scheme + const REVERSER_2_ETRAC_SUPPLY_POWER_BUS: ElectricalBusType = + ElectricalBusType::AlternatingCurrent(2); + const REVERSER_3_ETRAC_SUPPLY_POWER_BUS: ElectricalBusType = + ElectricalBusType::AlternatingCurrent(4); + + const REVERSER_2_TERTIARY_LOCK_SUPPLY_POWER_BUS: ElectricalBusType = + ElectricalBusType::AlternatingCurrent(2); + const REVERSER_3_TERTIARY_LOCK_SUPPLY_POWER_BUS: ElectricalBusType = + ElectricalBusType::AlternatingCurrent(4); + + pub fn new(context: &mut InitContext) -> Self { + Self { + reverser_2_position_id: context.get_identifier("REVERSER_2_POSITION".to_owned()), + reverser_3_position_id: context.get_identifier("REVERSER_3_POSITION".to_owned()), + + reverser_2_in_transition_id: context.get_identifier("REVERSER_2_DEPLOYING".to_owned()), + reverser_3_in_transition_id: context.get_identifier("REVERSER_3_DEPLOYING".to_owned()), + + reverser_2_deployed_id: context.get_identifier("REVERSER_2_DEPLOYED".to_owned()), + reverser_3_deployed_id: context.get_identifier("REVERSER_3_DEPLOYED".to_owned()), + + reversers: [ + A380ReverserAssembly::new( + Self::REVERSER_2_ETRAC_SUPPLY_POWER_BUS, + Self::REVERSER_2_TERTIARY_LOCK_SUPPLY_POWER_BUS, + ), + A380ReverserAssembly::new( + Self::REVERSER_3_ETRAC_SUPPLY_POWER_BUS, + Self::REVERSER_3_TERTIARY_LOCK_SUPPLY_POWER_BUS, + ), + ], + reversers_in_transition: [false, false], + reversers_deployed: [false, false], + } + } + + pub fn update( + &mut self, + context: &UpdateContext, + reverser_controllers: &[impl ElecReverserInterface; 2], + ) { + self.reversers[0].update(context, &reverser_controllers[0]); + + self.reversers[1].update(context, &reverser_controllers[1]); + + self.update_sensors_state(); + } + + fn update_sensors_state(&mut self) { + for (idx, reverser) in self.reversers.iter().enumerate() { + self.reversers_deployed[idx] = reverser.proximity_sensor_all_deployed(); + + self.reversers_in_transition[idx] = !reverser.proximity_sensor_all_deployed() + && !reverser.proximity_sensor_all_stowed(); + } + } + + pub fn reverser_feedback(&self, reverser_index: usize) -> &impl ReverserFeedback { + &self.reversers[reverser_index] + } + + pub fn reversers_position(&self) -> &[impl ReverserPosition] { + &self.reversers[..] + } +} +impl SimulationElement for A380Reversers { + fn accept(&mut self, visitor: &mut T) { + accept_iterable!(self.reversers, visitor); + + visitor.visit(self); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write( + &self.reverser_2_position_id, + self.reversers[0].reverser_position().get::(), + ); + writer.write( + &self.reverser_3_position_id, + self.reversers[1].reverser_position().get::(), + ); + + writer.write( + &self.reverser_2_in_transition_id, + self.reversers_in_transition[0], + ); + writer.write( + &self.reverser_3_in_transition_id, + self.reversers_in_transition[1], + ); + + writer.write(&self.reverser_2_deployed_id, self.reversers_deployed[0]); + writer.write(&self.reverser_3_deployed_id, self.reversers_deployed[1]); + } +} diff --git a/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/lib.rs b/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/lib.rs index 2a81bf2ddf0..2f9db7b6b28 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/lib.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/lib.rs @@ -336,6 +336,10 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { .provides_aircraft_variable("TURB ENG CORRECTED N2", "Percent", 3)? .provides_aircraft_variable("TURB ENG CORRECTED N2", "Percent", 4)? .provides_aircraft_variable("TURB ENG IGNITION SWITCH EX1", "Enum", 1)? + .provides_aircraft_variable("TURB ENG JET THRUST", "Pounds", 1)? + .provides_aircraft_variable("TURB ENG JET THRUST", "Pounds", 2)? + .provides_aircraft_variable("TURB ENG JET THRUST", "Pounds", 3)? + .provides_aircraft_variable("TURB ENG JET THRUST", "Pounds", 4)? .provides_aircraft_variable("UNLIMITED FUEL", "Bool", 0)? .provides_aircraft_variable("VELOCITY BODY X", "feet per second", 0)? .provides_aircraft_variable("VELOCITY BODY Y", "feet per second", 0)? diff --git a/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/reversers.rs b/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/reversers.rs index 7e66804b97d..838031c6315 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/reversers.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems_wasm/src/reversers.rs @@ -1,10 +1,13 @@ use std::error::Error; -use systems_wasm::aspects::{ExecuteOn, MsfsAspectBuilder}; -use systems_wasm::Variable; +use msfs::sim_connect; +use msfs::{sim_connect::SimConnect, sim_connect::SIMCONNECT_OBJECT_ID_USER}; + +use systems_wasm::aspects::{ExecuteOn, MsfsAspectBuilder, ObjectWrite, VariablesToObject}; +use systems_wasm::{set_data_on_sim_object, Variable}; pub(super) fn reversers(builder: &mut MsfsAspectBuilder) -> Result<(), Box> { - // Used for 320 reverser hack. Need it here as well so the 380 sees correct Z acceleration + // We recreate a long accel including the reverser accel that we pass to systems (else MSFS acceleration is not consistent with ingame acceleration when we modify plane velocity) builder.map_many( ExecuteOn::PreTick, vec![ @@ -15,5 +18,65 @@ pub(super) fn reversers(builder: &mut MsfsAspectBuilder) -> Result<(), Box Vec { + vec![ + Variable::aircraft("VELOCITY BODY Z", "Feet per second", 0), + Variable::aspect("REVERSER_DELTA_SPEED"), + Variable::aircraft( + "ROTATION ACCELERATION BODY Y", + "Radian per second squared", + 0, + ), + Variable::named("REVERSER_ANGULAR_ACCELERATION"), + Variable::aspect("BRAKE LEFT FORCE FACTOR"), + Variable::aspect("BRAKE RIGHT FORCE FACTOR"), + ] + } + + fn write(&mut self, values: Vec) -> ObjectWrite { + let brakes_in_use = values[4] + values[5] > 0.05; + + self.velocity_z = if values[0] < 0. + && values[0] > LOW_SPEED_MODE_SPEED_THRESHOLD_FOOT_PER_SEC + && !brakes_in_use + { + values[0] + LOW_SPEED_MODE_SPEED_FORCE_MULTIPLIER * values[1] + } else { + values[0] + values[1] + }; + + self.angular_acc_y = values[2] + ASYMETRY_EFFECT_MAGIC_MULTIPLIER * values[3]; + + ObjectWrite::on(values[1].abs() > 0. || values[3].abs() > 0.) + } + + set_data_on_sim_object!(); +} diff --git a/fbw-common/src/wasm/systems/systems/src/engine/mod.rs b/fbw-common/src/wasm/systems/systems/src/engine/mod.rs index edb26b1e27a..08ae453b56b 100644 --- a/fbw-common/src/wasm/systems/systems/src/engine/mod.rs +++ b/fbw-common/src/wasm/systems/systems/src/engine/mod.rs @@ -8,6 +8,7 @@ use crate::{ }; pub mod leap_engine; +pub mod reverser; pub mod reverser_thrust; pub mod trent_engine; diff --git a/fbw-common/src/wasm/systems/systems/src/engine/reverser.rs b/fbw-common/src/wasm/systems/systems/src/engine/reverser.rs new file mode 100644 index 00000000000..d1926a979e7 --- /dev/null +++ b/fbw-common/src/wasm/systems/systems/src/engine/reverser.rs @@ -0,0 +1,484 @@ +use std::time::Duration; +use uom::si::{f64::*, power::watt, ratio::ratio}; + +use crate::{ + shared::{ + low_pass_filter::LowPassFilter, random_from_normal_distribution, ConsumePower, + ElectricalBusType, ElectricalBuses, ReverserPosition, + }, + simulation::{SimulationElement, SimulationElementVisitor, UpdateContext}, +}; + +struct PowerDistributionUnit { + position: Ratio, + current_speed: LowPassFilter, + nominal_speed: f64, + + powered_by: ElectricalBusType, + + is_powered: bool, + + current_power: Power, +} +impl PowerDistributionUnit { + const NOMINAL_SPEED_RATIO_PER_S: f64 = 0.6; + const SPEED_RATIO_STD_DEVIATION: f64 = 0.05; + + const SPEED_TIME_CONSTANT: Duration = Duration::from_millis(250); + + const SPEED_TO_WATT_GAIN: f64 = 2000.; //TODO Find power consumption of reverser in use at full deploy speed + const STATIC_POWER_CONSUMPTION_WATT: f64 = 5.; //TODO Find consumption of non moving PDU + + fn new(powered_by: ElectricalBusType) -> Self { + Self { + position: Ratio::default(), + current_speed: LowPassFilter::new(Self::SPEED_TIME_CONSTANT), + nominal_speed: random_from_normal_distribution( + Self::NOMINAL_SPEED_RATIO_PER_S, + Self::SPEED_RATIO_STD_DEVIATION, + ), + powered_by, + is_powered: false, + + current_power: Power::default(), + } + } + + fn update( + &mut self, + context: &UpdateContext, + electrical_command_should_deploy: bool, + is_mechanically_locked: bool, + ) { + self.update_current_speed( + context, + electrical_command_should_deploy, + is_mechanically_locked, + ); + + self.position += context.delta_as_secs_f64() * self.current_speed.output(); + + if self.current_speed.output().get::() > 0. && self.position.get::() >= 1. + || self.current_speed.output().get::() < 0. && self.position.get::() <= 0. + { + self.current_speed.reset(Ratio::default()); + } + + self.position = self + .position + .max(Ratio::default()) + .min(Ratio::new::(1.)); + + self.update_current(); + } + + fn update_current_speed( + &mut self, + context: &UpdateContext, + electrical_command_should_deploy: bool, + is_mechanically_locked: bool, + ) { + let final_command = if self.is_powered { + if electrical_command_should_deploy { + Ratio::new::(1.) + } else { + Ratio::new::(-1.) + } + } else { + Ratio::default() + }; + + if is_mechanically_locked { + self.current_speed.reset(Ratio::default()); + } else { + self.current_speed + .update(context.delta(), self.nominal_speed * final_command); + } + } + + fn update_current(&mut self) { + self.current_power = if self.is_powered { + Power::new::( + Self::STATIC_POWER_CONSUMPTION_WATT + + self.current_speed.output().get::().abs() * Self::SPEED_TO_WATT_GAIN, + ) + } else { + Power::default() + }; + } + + fn position(&self) -> Ratio { + self.position + } +} +impl SimulationElement for PowerDistributionUnit { + fn receive_power(&mut self, buses: &impl ElectricalBuses) { + self.is_powered = buses.is_powered(self.powered_by) + } + + fn consume_power(&mut self, _: &UpdateContext, consumption: &mut T) { + consumption.consume_from_bus(self.powered_by, self.current_power); + } +} + +struct ElectricalLock { + is_locked: bool, + is_powered: bool, + powered_by: ElectricalBusType, +} +impl ElectricalLock { + fn new(powered_by: ElectricalBusType) -> Self { + Self { + is_locked: true, + is_powered: false, + powered_by, + } + } + + fn update(&mut self, should_unlock: bool, actuator_position: Ratio) { + let is_locking = !should_unlock || !self.is_powered; + + self.is_locked = is_locking && actuator_position.get::() < 0.01; + } + + fn is_locked(&self) -> bool { + self.is_locked + } +} +impl SimulationElement for ElectricalLock { + fn receive_power(&mut self, buses: &impl ElectricalBuses) { + self.is_powered = buses.is_powered(self.powered_by) + } +} + +pub trait ElecReverserInterface { + fn should_unlock_first(&self) -> bool; + fn should_unlock_second(&self) -> bool; + fn should_unlock_third(&self) -> bool; + fn should_deploy_reverser(&self) -> bool; +} + +pub trait ReverserFeedback { + fn position_sensor(&self) -> Ratio; + fn proximity_sensor_all_stowed(&self) -> bool; + fn proximity_sensor_at_least_one_stowed(&self) -> bool; + fn proximity_sensor_all_deployed(&self) -> bool; + fn pressure_switch_pressurised(&self) -> bool { + false + } + fn tertiary_lock_is_locked(&self) -> bool; +} + +pub struct A380ReverserAssembly { + electrical_lock1: ElectricalLock, + electrical_lock2: ElectricalLock, + electrical_lock3: ElectricalLock, + pdu: PowerDistributionUnit, +} +impl A380ReverserAssembly { + pub fn new( + etrac_powered_by: ElectricalBusType, + third_lock_powered_by: ElectricalBusType, + ) -> Self { + Self { + electrical_lock1: ElectricalLock::new(etrac_powered_by), + electrical_lock2: ElectricalLock::new(etrac_powered_by), + electrical_lock3: ElectricalLock::new(third_lock_powered_by), + pdu: PowerDistributionUnit::new(etrac_powered_by), + } + } + + pub fn update(&mut self, context: &UpdateContext, controller: &impl ElecReverserInterface) { + self.electrical_lock1 + .update(controller.should_unlock_first(), self.reverser_position()); + self.electrical_lock2 + .update(controller.should_unlock_second(), self.reverser_position()); + self.electrical_lock3 + .update(controller.should_unlock_third(), self.reverser_position()); + + self.pdu.update( + context, + controller.should_deploy_reverser(), + self.electrical_lock1.is_locked() + || self.electrical_lock2.is_locked() + || self.electrical_lock3.is_locked(), + ); + } +} +impl ReverserFeedback for A380ReverserAssembly { + fn position_sensor(&self) -> Ratio { + self.reverser_position() + } + + fn proximity_sensor_all_stowed(&self) -> bool { + self.reverser_position().get::() < 0.01 + } + + fn proximity_sensor_all_deployed(&self) -> bool { + self.reverser_position().get::() > 0.95 + } + + fn proximity_sensor_at_least_one_stowed(&self) -> bool { + // We do not model multiple doors for now, placeholder is a higher threshold for one door stowed only + self.reverser_position().get::() < 0.2 + && self.reverser_position().get::() >= 0.01 + } + + fn tertiary_lock_is_locked(&self) -> bool { + self.electrical_lock3.is_locked() + } +} +impl ReverserPosition for A380ReverserAssembly { + fn reverser_position(&self) -> Ratio { + self.pdu.position() + } +} +impl SimulationElement for A380ReverserAssembly { + fn accept(&mut self, visitor: &mut V) { + self.electrical_lock1.accept(visitor); + self.electrical_lock2.accept(visitor); + self.electrical_lock3.accept(visitor); + self.pdu.accept(visitor); + + visitor.visit(self); + } +} + +#[cfg(test)] +mod tests { + use uom::si::electric_potential::volt; + + use crate::electrical::test::TestElectricitySource; + use crate::electrical::ElectricalBus; + use crate::electrical::Electricity; + + use super::*; + use crate::shared::{update_iterator::FixedStepLoop, PotentialOrigin}; + use crate::simulation::test::{SimulationTestBed, TestBed}; + use crate::simulation::{Aircraft, InitContext, SimulationElement}; + + use std::time::Duration; + + struct TestReverserController { + should_lock: bool, + should_deploy_reversers: bool, + } + impl TestReverserController { + fn default() -> Self { + Self { + should_lock: true, + should_deploy_reversers: false, + } + } + + fn set_deploy_reverser(&mut self, is_deploying: bool) { + self.should_deploy_reversers = is_deploying; + } + + fn set_lock_reverser(&mut self, lock: bool) { + self.should_lock = lock; + } + } + impl ElecReverserInterface for TestReverserController { + fn should_unlock_first(&self) -> bool { + !self.should_lock + } + fn should_unlock_second(&self) -> bool { + !self.should_lock + } + fn should_unlock_third(&self) -> bool { + !self.should_lock + } + fn should_deploy_reverser(&self) -> bool { + self.should_deploy_reversers + } + } + + struct TestAircraft { + updater_fixed_step: FixedStepLoop, + + controller: TestReverserController, + + reverser: A380ReverserAssembly, + + powered_source_ac: TestElectricitySource, + dc_2_bus: ElectricalBus, + ac_4_bus: ElectricalBus, + is_dc_elec_powered: bool, + is_ac_elec_powered: bool, + } + impl TestAircraft { + fn new(context: &mut InitContext) -> Self { + Self { + updater_fixed_step: FixedStepLoop::new(Duration::from_millis(10)), + controller: TestReverserController::default(), + + reverser: A380ReverserAssembly::new( + ElectricalBusType::DirectCurrent(2), + ElectricalBusType::AlternatingCurrent(4), + ), + + powered_source_ac: TestElectricitySource::powered( + context, + PotentialOrigin::EngineGenerator(1), + ), + + dc_2_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrent(2)), + ac_4_bus: ElectricalBus::new(context, ElectricalBusType::AlternatingCurrent(4)), + + is_dc_elec_powered: true, + is_ac_elec_powered: true, + } + } + + fn reverser_position(&self) -> Ratio { + self.reverser.reverser_position() + } + + fn reverser_is_locked(&self) -> bool { + self.reverser.electrical_lock1.is_locked() + && self.reverser.electrical_lock2.is_locked() + && self.reverser.electrical_lock3.is_locked() + } + + fn set_ac_elec_power(&mut self, is_on: bool) { + self.is_ac_elec_powered = is_on; + } + + fn set_dc_elec_power(&mut self, is_on: bool) { + self.is_dc_elec_powered = is_on; + } + + fn set_deploy_reverser(&mut self, is_deploying: bool) { + self.controller.set_deploy_reverser(is_deploying) + } + + fn set_lock_reverser(&mut self, lock: bool) { + self.controller.set_lock_reverser(lock) + } + } + impl Aircraft for TestAircraft { + fn update_before_power_distribution( + &mut self, + _: &UpdateContext, + electricity: &mut Electricity, + ) { + self.powered_source_ac + .power_with_potential(ElectricPotential::new::(140.)); + electricity.supplied_by(&self.powered_source_ac); + + if self.is_dc_elec_powered { + electricity.flow(&self.powered_source_ac, &self.dc_2_bus); + } + + if self.is_ac_elec_powered { + electricity.flow(&self.powered_source_ac, &self.ac_4_bus); + } + } + + fn update_after_power_distribution(&mut self, context: &UpdateContext) { + self.updater_fixed_step.update(context); + + for cur_time_step in &mut self.updater_fixed_step { + self.reverser + .update(&context.with_delta(cur_time_step), &self.controller); + + println!( + "Reverser Pos: {:.3} ,Locks {:?}/{:?}/{:?}", + self.reverser.position_sensor().get::(), + self.reverser.electrical_lock1.is_locked(), + self.reverser.electrical_lock2.is_locked(), + self.reverser.electrical_lock3.is_locked(), + ); + } + } + } + impl SimulationElement for TestAircraft { + fn accept(&mut self, visitor: &mut V) { + self.reverser.accept(visitor); + + visitor.visit(self); + } + } + + #[test] + fn reverser_stowed_at_init() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| a.set_ac_elec_power(false)); + test_bed.command(|a| a.set_dc_elec_power(false)); + test_bed.run_with_delta(Duration::from_millis(1000)); + + assert!(test_bed.query(|a| a.reverser_position().get::()) == 0.); + } + + #[test] + fn reverser_do_not_deploy_if_locked() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| a.set_ac_elec_power(true)); + test_bed.command(|a| a.set_dc_elec_power(true)); + test_bed.command(|a| a.set_deploy_reverser(true)); + test_bed.command(|a| a.set_lock_reverser(true)); + + test_bed.run_with_delta(Duration::from_millis(1000)); + + assert!(test_bed.query(|a| a.reverser_position().get::()) == 0.); + } + + #[test] + fn reverser_do_not_deploy_if_unlocked_but_no_lock_power() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| a.set_ac_elec_power(false)); + test_bed.command(|a| a.set_dc_elec_power(true)); + test_bed.command(|a| a.set_deploy_reverser(true)); + test_bed.command(|a| a.set_lock_reverser(false)); + + test_bed.run_with_delta(Duration::from_millis(1000)); + + assert!(test_bed.query(|a| a.reverser_position().get::()) == 0.); + } + + #[test] + fn reverser_deploys_if_unlocked_and_lock_powered() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| a.set_ac_elec_power(true)); + test_bed.command(|a| a.set_dc_elec_power(true)); + test_bed.command(|a| a.set_deploy_reverser(true)); + test_bed.command(|a| a.set_lock_reverser(false)); + + test_bed.run_with_delta(Duration::from_millis(1000)); + + assert!(test_bed.query(|a| a.reverser_position().get::()) >= 0.3); + + test_bed.run_with_delta(Duration::from_millis(1500)); + + assert!(test_bed.query(|a| a.reverser_position().get::()) >= 0.99); + } + + #[test] + fn reverser_deploys_and_can_be_stowed_back() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| a.set_ac_elec_power(true)); + test_bed.command(|a| a.set_dc_elec_power(true)); + test_bed.command(|a| a.set_deploy_reverser(true)); + test_bed.command(|a| a.set_lock_reverser(false)); + + test_bed.run_with_delta(Duration::from_millis(2500)); + assert!(test_bed.query(|a| a.reverser_position().get::()) >= 0.99); + + test_bed.command(|a| a.set_lock_reverser(true)); + test_bed.command(|a| a.set_deploy_reverser(false)); + + test_bed.run_with_delta(Duration::from_millis(1000)); + assert!(test_bed.query(|a| a.reverser_position().get::()) <= 0.9); + assert!(test_bed.query(|a| !a.reverser_is_locked())); + + test_bed.run_with_delta(Duration::from_millis(2000)); + assert!(test_bed.query(|a| a.reverser_position().get::()) <= 0.01); + assert!(test_bed.query(|a| a.reverser_is_locked())); + } +}