diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b66b4b42609..904b3195d95 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.7.0 +1. [HYD] First building block, more to come. Hydraulics do not impact the sim YET. - @crocket6 (crocket) 1. [ENGINE] Fixed fuel consumption model - @Taz5150 (TazX [Z+2]#0405) 1. [ENGINE] Fixed fuel flow being 0 at Start-up - @Taz5150 (TazX [Z+2]#0405) 1. [MCDU] Fixed input and display issues on PERF/W&B and INIT pages - @felixharnstrom (Felix Härnström) diff --git a/.gitignore b/.gitignore index 8a95f75fb16..ee205955d91 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ node_modules /target /src/instruments/src/EFB/web/ /src/systems/target/ +/src/systems/a320_hydraulic_simulation_graphs/*.png !igniter.config.mjs .igniter /src/fdr2csv/*.exe diff --git a/Cargo.lock b/Cargo.lock index df1482c341c..ff41ef2aff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,9 +1,26 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "a320_hydraulic_simulation_graphs" +version = "0.1.0" +dependencies = [ + "a320_systems", + "itertools", + "ntest", + "num-derive", + "num-traits", + "plotlib", + "rand", + "rustplotlib", + "systems", + "uom", +] + [[package]] name = "a320_systems" version = "0.1.0" dependencies = [ + "rand", "systems", "uom", ] @@ -18,6 +35,21 @@ dependencies = [ "uom", ] +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.15" @@ -53,6 +85,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bindgen" version = "0.55.1" @@ -155,6 +201,28 @@ dependencies = [ "termcolor", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "futures" version = "0.3.12" @@ -261,6 +329,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + [[package]] name = "glob" version = "0.3.0" @@ -337,6 +411,16 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "msfs" version = "0.0.1-alpha.2" @@ -436,6 +520,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + [[package]] name = "once_cell" version = "1.5.2" @@ -460,6 +550,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "plotlib" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9462104f987d8d0f6625f0c7764f1c8b890bd1dc8584d8293e031f25c5a0d242" +dependencies = [ + "failure", + "svg", +] + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -569,12 +669,24 @@ version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustplotlib" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4326f7ac67e4ff419282ad12dabf1fcad09481a849b72108c890e01414ebb88a" + [[package]] name = "serde" version = "1.0.123" @@ -599,6 +711,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "svg" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3685c82a045a6af0c488f0550b0f52b4c77d2a52b0ca8aba719f9d268fa96965" + [[package]] name = "syn" version = "1.0.60" @@ -610,6 +728,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "systems" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ae97ebaaaae..fd83c593e23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = [ "src/systems/a320_systems", "src/systems/a320_systems_wasm", - "src/systems/systems" + "src/systems/systems", + "src/systems/a320_hydraulic_simulation_graphs", ] diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index 09d68830bbd..1ae650694fc 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -114,46 +114,42 @@ - Bool - True if pedestal door video button is being held -- A32NX_HYD_ENG1PUMP_FAULT +- A32NX_OVHD_HYD_{name}_PUMP_PB_HAS_FAULT - Bool - - True if engine 1 hyd pump fault - -- A32NX_HYD_ENG1PUMP_TOGGLE - - Bool - - True if engine 1 hyd pump is on - -- A32NX_HYD_ENG2PUMP_FAULT - - Bool - - True if engine 2 hyd pump fault + - True if engine {name} hyd pump fault + - {name} + - ENG_1 + - ENG_2 -- A32NX_HYD_ENG2PUMP_TOGGLE +- A32NX_OVHD_HYD_{name}_PUMP_PB_IS_AUTO - Bool - - True if engine 2 hyd pump is on + - True if {name} hyd pump is on + - {name} + - ENG_1 + - ENG_2 -- A32NX_HYD_ELECPUMP_FAULT +- A32NX_OVHD_HYD_{name}_PB_HAS_FAULT - Bool - - True if elec hyd pump fault + - True if elec {name} hyd pump fault + - {name} + - EPUMPB + - EPUMPY -- A32NX_HYD_ELECPUMP_TOGGLE +- A32NX_OVHD_HYD_{name}_PB_IS_AUTO - Bool - - True if elec hyd pump is on/auto + - True if elec {name} hyd pump is on/auto + - {name} + - EPUMPB + - EPUMPY -- A32NX_HYD_PTU_FAULT +- A32NX_OVHD_HYD_PTU_PB_HAS_FAULT - Bool - True if PTU fault -- A32NX_HYD_PTU_TOGGLE +- A32NX_OVHD_HYD_PTU_PB_IS_AUTO - Bool - True if PTU system on/auto -- A32NX_HYD_ELECPUMPY_FAULT - - Bool - - True if yellow elec hyd pump fault - -- A32NX_HYD_ELECPUMPY_TOGGLE - - Bool - - True if yellow elec hyd pump is on/auto - - A32NX_ENGMANSTART1_TOGGLE - Bool - True if manual engine 1 start on @@ -226,7 +222,7 @@ - Bool - True if PFD metric altitude enabled -- A32NX_OVHD_HYD_BLUEPUMP_OVRD +- A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON - Bool - True if "BLUE PUMP OVRD" switch is off @@ -782,6 +778,98 @@ - 1 - 2 +- A32NX_HYD_{loop_name}_PRESSURE + - Psi + - Current pressure in the {loop_name} hydraulic circuit + - {loop_name} + - GREEN + - BLUE + - YELLOW + +- A32NX_HYD_{loop_name}_RESERVOIR + - Gallon + - Current fluid level in the {loop_name} hydraulic circuit reservoir + - {loop_name} + - GREEN + - BLUE + - YELLOW + +- A32NX_HYD_{loop_name}_EDPUMP_ACTIVE + - Bool + - Engine driven pump of {loop_name} hydraulic circuit is active + - {loop_name} + - GREEN + - YELLOW + +- A32NX_HYD_{loop_name}_EDPUMP_LOW_PRESS + - Bool + - Engine driven pump of {loop_name} hydraulic circuit is active but pressure is too low + - {loop_name} + - GREEN + - YELLOW + +- A32NX_HYD_{loop_name}_EPUMP_ACTIVE + - Bool + - Electric pump of {loop_name} hydraulic circuit is active + - {loop_name} + - BLUE + - YELLOW + +- A32NX_HYD_{loop_name}_EPUMP_LOW_PRESS + - Bool + - Electric pump of {loop_name} hydraulic circuit is active but pressure is too low + - {loop_name} + - BLUE + - YELLOW + +- A32NX_HYD_{loop_name}_FIRE_VALVE_OPENED + - Bool + - Engine driven pump of {loop_name} hydraulic circuit can receive hydraulic fluid + - {loop_name} + - GREEN + - YELLOW + +- A32NX_HYD_PTU_VALVE_OPENED + - Bool + - Power Transfer Unit can receive fluid from yellow and green circuits + +- A32NX_HYD_PTU_ACTIVE_{motor_side} + - Bool + - Power Transfer Unit is trying to transfer hydraulic power from either yellow to green (R2L) or green to yellow (L2R) circuits + - {motor_side} + - L2R + - R2L + +- A32NX_HYD_PTU_MOTOR_FLOW + - Gallon per second + - Power Transfer Unit instantaneous flow in motor side + +- A32NX_HYD_RAT_STOW_POSITION + - Percent over 100 + - RAT position, from fully stowed (0) to fully deployed (1) + +- A32NX_HYD_RAT_RPM + - Rpm + - RAT propeller current RPM + +- A32NX_HYD_BRAKE_NORM_{brake_side}_PRESS + - Psi + - Current pressure in brake slave circuit on green brake circuit + - {brake_side} + - LEFT + - RIGHT + +- A32NX_HYD_BRAKE_ALTN_{brake_side}_PRESS + - Psi + - Current pressure in brake slave circuit on yellow alternate brake circuit + - {brake_side} + - LEFT + - RIGHT + +- A32NX_HYD_BRAKE_ALTN_ACC_PRESS + - Psi + - Current pressure in brake accumulator on yellow alternate brake circuit + - A32NX_FMGC_FLIGHT_PHASE - Enum - Holds the FMGCs current flight phase diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/approach.FLT b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/approach.FLT index 892289cb76b..82ca0650ac5 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/approach.FLT +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/approach.FLT @@ -165,18 +165,13 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_HYD_ENG1PUMP_FAULT=0 -A32NX_HYD_ENG1PUMP_TOGGLE=1 -A32NX_HYD_ENG2PUMP_FAULT=0 -A32NX_HYD_ENG2PUMP_TOGGLE=1 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_HYD_ELECPUMP_FAULT=0 -A32NX_HYD_ELECPUMP_TOGGLE=1 +A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_HYD_PTU_FAULT=0 -A32NX_HYD_PTU_TOGGLE=1 -A32NX_HYD_ELECPUMPY_FAULT=0 -A32NX_HYD_ELECPUMPY_TOGGLE=1 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -201,7 +196,7 @@ A32NX_KNOB_SWITCHING_3_Position=1 A32NX_KNOB_SWITCHING_4_Position=1 A32NX_PANEL_DCDU_L_BRIGHTNESS=0.5 A32NX_PANEL_DCDU_R_BRIGHTNESS=0.5 -A32NX_OVHD_HYD_BLUEPUMP_OVRD=0 +A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON=0 A32NX_OVHD_HYD_LEAK_MEASUREMENT_G=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_B=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_Y=1 diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/apron.FLT b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/apron.FLT index 16bc428a158..7ebb724b040 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/apron.FLT +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/apron.FLT @@ -170,18 +170,13 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_HYD_ENG1PUMP_FAULT=0 -A32NX_HYD_ENG1PUMP_TOGGLE=1 -A32NX_HYD_ENG2PUMP_FAULT=0 -A32NX_HYD_ENG2PUMP_TOGGLE=1 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_HYD_ELECPUMP_FAULT=0 -A32NX_HYD_ELECPUMP_TOGGLE=1 +A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_HYD_PTU_FAULT=0 -A32NX_HYD_PTU_TOGGLE=1 -A32NX_HYD_ELECPUMPY_FAULT=0 -A32NX_HYD_ELECPUMPY_TOGGLE=1 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -206,7 +201,7 @@ A32NX_KNOB_SWITCHING_3_Position=1 A32NX_KNOB_SWITCHING_4_Position=1 A32NX_PANEL_DCDU_L_BRIGHTNESS=0.5 A32NX_PANEL_DCDU_R_BRIGHTNESS=0.5 -A32NX_OVHD_HYD_BLUEPUMP_OVRD=0 +A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON=0 A32NX_OVHD_HYD_LEAK_MEASUREMENT_G=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_B=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_Y=1 diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/cruise.FLT b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/cruise.FLT index 7cd188eabd6..fa84a387992 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/cruise.FLT +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/cruise.FLT @@ -165,18 +165,13 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_HYD_ENG1PUMP_FAULT=0 -A32NX_HYD_ENG1PUMP_TOGGLE=1 -A32NX_HYD_ENG2PUMP_FAULT=0 -A32NX_HYD_ENG2PUMP_TOGGLE=1 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_HYD_ELECPUMP_FAULT=0 -A32NX_HYD_ELECPUMP_TOGGLE=1 +A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_HYD_PTU_FAULT=0 -A32NX_HYD_PTU_TOGGLE=1 -A32NX_HYD_ELECPUMPY_FAULT=0 -A32NX_HYD_ELECPUMPY_TOGGLE=1 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -201,7 +196,7 @@ A32NX_KNOB_SWITCHING_3_Position=1 A32NX_KNOB_SWITCHING_4_Position=1 A32NX_PANEL_DCDU_L_BRIGHTNESS=0.5 A32NX_PANEL_DCDU_R_BRIGHTNESS=0.5 -A32NX_OVHD_HYD_BLUEPUMP_OVRD=0 +A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON=0 A32NX_OVHD_HYD_LEAK_MEASUREMENT_G=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_B=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_Y=1 diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/final.FLT b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/final.FLT index 31e568ced2d..74fd6e4051a 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/final.FLT +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/final.FLT @@ -165,18 +165,13 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_HYD_ENG1PUMP_FAULT=0 -A32NX_HYD_ENG1PUMP_TOGGLE=1 -A32NX_HYD_ENG2PUMP_FAULT=0 -A32NX_HYD_ENG2PUMP_TOGGLE=1 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_HYD_ELECPUMP_FAULT=0 -A32NX_HYD_ELECPUMP_TOGGLE=1 +A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_HYD_PTU_FAULT=0 -A32NX_HYD_PTU_TOGGLE=1 -A32NX_HYD_ELECPUMPY_FAULT=0 -A32NX_HYD_ELECPUMPY_TOGGLE=1 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -201,7 +196,7 @@ A32NX_KNOB_SWITCHING_3_Position=1 A32NX_KNOB_SWITCHING_4_Position=1 A32NX_PANEL_DCDU_L_BRIGHTNESS=0.5 A32NX_PANEL_DCDU_R_BRIGHTNESS=0.5 -A32NX_OVHD_HYD_BLUEPUMP_OVRD=0 +A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON=0 A32NX_OVHD_HYD_LEAK_MEASUREMENT_G=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_B=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_Y=1 diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml index 46d3271be29..3c6c70ed4a5 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml @@ -1859,27 +1859,27 @@ PUSH_OVHD_HYD_ENG1PUMP - L:A32NX_HYD_ENG1PUMP_TOGGLE + L:A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO (L:A32NX_ELEC_AC_1_BUS_IS_POWERED, Bool) - (L:A32NX_HYD_ENG1PUMP_FAULT, Bool) - (L:A32NX_HYD_ENG1PUMP_TOGGLE, Bool) ! + (L:A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT, Bool) + (L:A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO, Bool) ! False False False - %((L:A32NX_HYD_ENG1PUMP_TOGGLE, Bool))%{if}Turn OFF eng 1 hyd pump%{else}Turn ON eng 1 hyd pump%{end} + %((L:A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO, Bool))%{if}Turn OFF eng 1 hyd pump%{else}Turn ON eng 1 hyd pump%{end} PUSH_OVHD_HYD_ENG2PUMP - L:A32NX_HYD_ENG2PUMP_TOGGLE + L:A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO (L:A32NX_ELEC_AC_2_BUS_IS_POWERED, Bool) - (L:A32NX_HYD_ENG2PUMP_FAULT, Bool) - (L:A32NX_HYD_ENG2PUMP_TOGGLE, Bool) ! + (L:A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT, Bool) + (L:A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO, Bool) ! False False False - %((L:A32NX_HYD_ENG2PUMP_TOGGLE, Bool))%{if}Turn OFF eng 2 hyd pump%{else}Turn ON eng 2 hyd pump%{end} + %((L:A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO, Bool))%{if}Turn OFF eng 2 hyd pump%{else}Turn ON eng 2 hyd pump%{end} @@ -1895,41 +1895,41 @@ PUSH_OVHD_HYD_ELECPUMP LOCK_OVHD_HYD_ELECPUMP - L:A32NX_HYD_ELECPUMP_TOGGLE + L:A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO (L:A32NX_ELEC_AC_ESS_SHED_BUS_IS_POWERED, Bool) - (L:A32NX_HYD_ELECPUMP_FAULT, Bool) - (L:A32NX_HYD_ELECPUMP_TOGGLE, Bool) ! + (L:A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT, Bool) + (L:A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO, Bool) ! False False False - %((L:A32NX_HYD_ELECPUMP_TOGGLE, Bool))%{if}Turn OFF elec hyd pump%{else}Turn ON elec hyd pump%{end} + %((L:A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO, Bool))%{if}Turn OFF elec hyd pump%{else}Turn ON elec hyd pump%{end} PUSH_OVHD_HYD_PTU - L:A32NX_HYD_PTU_TOGGLE + L:A32NX_OVHD_HYD_PTU_PB_IS_AUTO (L:A32NX_ELEC_AC_2_BUS_IS_POWERED, Bool) - (L:A32NX_HYD_PTU_FAULT, Bool) - (L:A32NX_HYD_PTU_TOGGLE, Bool) ! + (L:A32NX_OVHD_HYD_PTU_PB_HAS_FAULT, Bool) + (L:A32NX_OVHD_HYD_PTU_PB_IS_AUTO, Bool) ! False False False - %((L:A32NX_HYD_PTU_TOGGLE, Bool))%{if}Turn OFF PTU%{else}Turn ON PTU%{end} + %((L:A32NX_OVHD_HYD_PTU_PB_IS_AUTO, Bool))%{if}Turn OFF PTU%{else}Turn ON PTU%{end} PUSH_OVHD_HYD_ELECPUMPY - L:A32NX_HYD_ELECPUMPY_TOGGLE + L:A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO (L:A32NX_ELEC_AC_2_BUS_IS_POWERED, Bool) - (L:A32NX_HYD_ELECPUMPY_FAULT, Bool) - (L:A32NX_HYD_ELECPUMPY_TOGGLE, Bool) ! + (L:A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT, Bool) + (L:A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO, Bool) ! False False False - %((L:A32NX_HYD_ELECPUMPY_TOGGLE, Bool))%{if}Turn ON yellow elec hyd pump%{else}Turn OFF yellow elec hyd pump%{end} + %((L:A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO, Bool))%{if}Turn ON yellow elec hyd pump%{else}Turn OFF yellow elec hyd pump%{end} @@ -2833,7 +2833,7 @@ PUSH_OVHD_HYD_BLUEPUMP LOCK_OVHD_HYD_BLUEPUMPOVRD - L:A32NX_OVHD_HYD_BLUEPUMP_OVRD + L:A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON (L:A32NX_ELEC_AC_1_BUS_IS_POWERED, Bool) False False @@ -3701,9 +3701,9 @@ Press_Arc_L 3 1 - + (L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED, Bool) if{ - (A:BRAKE PARKING POSITION, Bool) 2 * + (L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS, number) 1000 / } els{ 0 } @@ -3718,7 +3718,7 @@ 1 (L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED, Bool) if{ - (A:BRAKE PARKING POSITION, Bool) 2 * + (L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS, number) 1000 / } els{ 0 } @@ -3733,7 +3733,7 @@ 1 (L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED, Bool) if{ - 0.8 + (L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS, number) 4000 / } els{ 0 } diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/runway.FLT b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/runway.FLT index 1a8b1aeba97..b357545ff37 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/runway.FLT +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/runway.FLT @@ -172,18 +172,13 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_HYD_ENG1PUMP_FAULT=0 -A32NX_HYD_ENG1PUMP_TOGGLE=1 -A32NX_HYD_ENG2PUMP_FAULT=0 -A32NX_HYD_ENG2PUMP_TOGGLE=1 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_HYD_ELECPUMP_FAULT=0 -A32NX_HYD_ELECPUMP_TOGGLE=1 +A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_HYD_PTU_FAULT=0 -A32NX_HYD_PTU_TOGGLE=1 -A32NX_HYD_ELECPUMPY_FAULT=0 -A32NX_HYD_ELECPUMPY_TOGGLE=1 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -208,7 +203,7 @@ A32NX_KNOB_SWITCHING_3_Position=1 A32NX_KNOB_SWITCHING_4_Position=1 A32NX_PANEL_DCDU_L_BRIGHTNESS=0.5 A32NX_PANEL_DCDU_R_BRIGHTNESS=0.5 -A32NX_OVHD_HYD_BLUEPUMP_OVRD=0 +A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON=0 A32NX_OVHD_HYD_LEAK_MEASUREMENT_G=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_B=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_Y=1 diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/taxi.flt b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/taxi.flt index 957dfb09243..c90f9a7c35b 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/taxi.flt +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/taxi.flt @@ -184,7 +184,7 @@ A32NX_KNOB_SWITCHING_3_Position=1 A32NX_KNOB_SWITCHING_4_Position=1 A32NX_PANEL_DCDU_L_BRIGHTNESS=0.5 A32NX_PANEL_DCDU_R_BRIGHTNESS=0.5 -A32NX_OVHD_HYD_BLUEPUMP_OVRD=0 +A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON=0 A32NX_OVHD_HYD_LEAK_MEASUREMENT_G=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_B=1 A32NX_OVHD_HYD_LEAK_MEASUREMENT_Y=1 diff --git a/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml b/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml new file mode 100644 index 00000000000..9cd7db19df2 --- /dev/null +++ b/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "a320_hydraulic_simulation_graphs" +version = "0.1.0" +authors = ["davydecorps <38904654+crocket63@users.noreply.github.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +systems = { path = "../systems" } +a320_systems = { path = "../a320_systems" } +uom = "0.30.0" +rand = "0.8.0" +ntest = "0.7.2" +num-derive = "0.3.3" +num-traits = "0.2.14" +itertools = "0.10.0" +plotlib = "0.5.1" +rustplotlib = "0.0.4" diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs new file mode 100644 index 00000000000..4bc6c0afa3e --- /dev/null +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -0,0 +1,837 @@ +use systems::simulation::UpdateContext; + +pub use systems::hydraulic::*; + +use std::time::Duration; +use uom::si::{ + acceleration::foot_per_second_squared, + f64::*, + length::foot, + pressure::{pascal, psi}, + thermodynamic_temperature::degree_celsius, + velocity::knot, + volume::{gallon, liter}, + volume_rate::gallon_per_second, +}; + +use plotlib::page::Page; +use plotlib::repr::Plot; +use plotlib::style::LineStyle; +use plotlib::view::ContinuousView; + +extern crate rustplotlib; +use rustplotlib::Figure; + +struct TestHydraulicLoopController { + should_open_fire_shutoff_valve: bool, +} +impl TestHydraulicLoopController { + fn commanding_open_fire_shutoff_valve() -> Self { + Self { + should_open_fire_shutoff_valve: true, + } + } +} +impl HydraulicLoopController for TestHydraulicLoopController { + fn should_open_fire_shutoff_valve(&self) -> bool { + self.should_open_fire_shutoff_valve + } +} + +struct TestPumpController { + should_pressurise: bool, +} +impl TestPumpController { + fn commanding_pressurise() -> Self { + Self { + should_pressurise: true, + } + } + + fn commanding_depressurise() -> Self { + Self { + should_pressurise: false, + } + } + + fn command_pressurise(&mut self) { + self.should_pressurise = true; + } + + fn command_depressurise(&mut self) { + self.should_pressurise = false; + } +} +impl PumpController for TestPumpController { + fn should_pressurise(&self) -> bool { + self.should_pressurise + } +} + +struct TestPowerTransferUnitController { + should_enable: bool, +} +impl TestPowerTransferUnitController { + fn commanding_disabled() -> Self { + Self { + should_enable: false, + } + } + + fn commanding_enabled() -> Self { + Self { + should_enable: true, + } + } + + fn command_enable(&mut self) { + self.should_enable = true; + } +} +impl PowerTransferUnitController for TestPowerTransferUnitController { + fn should_enable(&self) -> bool { + self.should_enable + } +} + +fn main() { + println!("Launching hyd simulation..."); + let path = "./src/systems/a320_hydraulic_simulation_graphs/"; + + green_loop_edp_simulation(path); + yellow_green_ptu_loop_simulation(path); + yellow_epump_plus_edp2_with_ptu(path); +} + +fn make_figure(h: &History) -> Figure { + use rustplotlib::{Axes2D, Line2D}; + + let mut all_axis: Vec> = Vec::new(); + + for (idx, cur_data) in h.data_vector.iter().enumerate() { + let mut curr_axis = Axes2D::new() + .add( + Line2D::new(h.name_vector[idx].as_str()) + .data(&h.time_vector, &cur_data) + .color("blue") + // .marker("x") + // .linestyle("--") + .linewidth(1.0), + ) + .xlabel("Time [sec]") + .ylabel(h.name_vector[idx].as_str()) + .legend("best") + .xlim(0.0, *h.time_vector.last().unwrap()); + // .ylim(-2.0, 2.0); + + curr_axis = curr_axis.grid(true); + all_axis.push(Some(curr_axis)); + } + + Figure::new().subplots(all_axis.len() as u32, 1, all_axis) +} + +/// History class to record a simulation +struct History { + /// Simulation time starting from 0 + time_vector: Vec, + /// Name of each var saved + name_vector: Vec, + /// Vector data for each var saved + data_vector: Vec>, + _data_size: usize, +} +impl History { + fn new(names: Vec) -> History { + History { + time_vector: Vec::new(), + name_vector: names.clone(), + data_vector: Vec::new(), + _data_size: names.len(), + } + } + + /// Sets initialisation values of each data before first step + fn init(&mut self, start_time: f64, values: Vec) { + self.time_vector.push(start_time); + for v in values { + self.data_vector.push(vec![v]); + } + } + + /// Updates all values and time vector + fn update(&mut self, delta_time: f64, values: Vec) { + self.time_vector + .push(self.time_vector.last().unwrap() + delta_time); + self.push_data(values); + } + + fn push_data(&mut self, values: Vec) { + for (idx, v) in values.iter().enumerate() { + self.data_vector[idx].push(*v); + } + } + + /// Builds a graph using rust crate plotlib + fn _show(self) { + let mut v = ContinuousView::new() + .x_range(0.0, *self.time_vector.last().unwrap()) + .y_range(0.0, 3500.0) + .x_label("Time (s)") + .y_label("Value"); + + for cur_data in self.data_vector { + // Here build the 2 by Xsamples vector + let mut new_vector: Vec<(f64, f64)> = Vec::new(); + for (idx, sample) in self.time_vector.iter().enumerate() { + new_vector.push((*sample, cur_data[idx])); + } + + // We create our scatter plot from the data + let s1: Plot = Plot::new(new_vector).line_style(LineStyle::new().colour("#DD3355")); + + v = v.add(s1); + } + + // A page with a single view is then saved to an SVG file + Page::single(&v).save("scatter.svg").unwrap(); + } + + /// Builds a graph using matplotlib python backend. PYTHON REQUIRED AS WELL AS MATPLOTLIB PACKAGE + fn show_matplotlib(&self, figure_title: &str, path: &str) { + let fig = make_figure(&self); + + use rustplotlib::backend::Matplotlib; + use rustplotlib::Backend; + let mut mpl = Matplotlib::new().unwrap(); + mpl.set_style("ggplot").unwrap(); + + fig.apply(&mut mpl).unwrap(); + + let mut final_filename: String = path.to_owned(); + final_filename.push_str(figure_title); + + let _result = mpl.savefig(&final_filename); + + mpl.wait().unwrap(); + } +} + +/// Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s +fn green_loop_edp_simulation(path: &str) { + let green_loop_var_names = vec![ + "Loop Pressure".to_string(), + "Loop Volume".to_string(), + "Loop Reservoir".to_string(), + "Loop Flow".to_string(), + ]; + let mut green_loop_history = History::new(green_loop_var_names); + + let edp1_var_names = vec!["Delta Vol Max".to_string(), "pump rpm".to_string()]; + let mut edp1_history = History::new(edp1_var_names); + + let mut edp1 = engine_driven_pump(); + let mut edp1_controller = TestPumpController::commanding_pressurise(); + + let mut green_loop = hydraulic_loop("GREEN"); + let green_loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); + + let edp_rpm = 3000.; + let context = context(Duration::from_millis(100)); + + let green_acc_var_names = vec![ + "Loop Pressure".to_string(), + "Acc gas press".to_string(), + "Acc fluid vol".to_string(), + "Acc gas vol".to_string(), + ]; + let mut accu_green_history = History::new(green_acc_var_names); + + green_loop_history.init( + 0.0, + vec![ + green_loop.pressure().get::(), + green_loop.loop_fluid_volume().get::(), + green_loop.reservoir_volume().get::(), + green_loop.current_flow().get::(), + ], + ); + edp1_history.init(0.0, vec![edp1.delta_vol_max().get::(), edp_rpm]); + accu_green_history.init( + 0.0, + vec![ + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), + ], + ); + for x in 0..600 { + if x == 50 { + // After 5s + assert!(green_loop.pressure() >= Pressure::new::(2850.0)); + } + if x == 200 { + assert!(green_loop.pressure() >= Pressure::new::(2850.0)); + edp1_controller.command_depressurise(); + } + if x >= 500 { + // Shutdown + 30s + assert!(green_loop.pressure() <= Pressure::new::(250.0)); + } + + edp1.update(&context, &green_loop, edp_rpm, &edp1_controller); + green_loop.update( + &context, + Vec::new(), + vec![&edp1], + Vec::new(), + Vec::new(), + &green_loop_controller, + ); + if x % 20 == 0 { + println!("Iteration {}", x); + println!("-------------------------------------------"); + println!("---PSI: {}", green_loop.pressure().get::()); + println!( + "--------Reservoir Volume (g): {}", + green_loop.reservoir_volume().get::() + ); + println!( + "--------Loop Volume (g): {}", + green_loop.loop_fluid_volume().get::() + ); + println!( + "--------Acc Fluid Volume (L): {}", + green_loop.accumulator_fluid_volume().get::() + ); + println!( + "--------Acc Gas Volume (L): {}", + green_loop.accumulator_gas_volume().get::() + ); + println!( + "--------Acc Gas Pressure (psi): {}", + green_loop.accumulator_gas_pressure().get::() + ); + } + + green_loop_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.pressure().get::(), + green_loop.loop_fluid_volume().get::(), + green_loop.reservoir_volume().get::(), + green_loop.current_flow().get::(), + ], + ); + edp1_history.update( + context.delta_as_secs_f64(), + vec![edp1.delta_vol_max().get::(), edp_rpm], + ); + accu_green_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), + ], + ); + } + + green_loop_history.show_matplotlib("green_loop_edp_simulation_press", &path); + edp1_history.show_matplotlib("green_loop_edp_simulation_EDP1 data", &path); + accu_green_history.show_matplotlib("green_loop_edp_simulation_Green Accum data", &path); +} + +fn yellow_green_ptu_loop_simulation(path: &str) { + let loop_var_names = vec![ + "GREEN Loop Pressure".to_string(), + "YELLOW Loop Pressure".to_string(), + "GREEN Loop reservoir".to_string(), + "YELLOW Loop reservoir".to_string(), + "GREEN Loop delta vol".to_string(), + "YELLOW Loop delta vol".to_string(), + ]; + let mut loop_history = History::new(loop_var_names); + + let ptu_var_names = vec![ + "Current flow".to_string(), + "Press delta".to_string(), + "PTU active GREEN".to_string(), + "PTU active YELLOW".to_string(), + ]; + let mut ptu_history = History::new(ptu_var_names); + + let green_acc_var_names = vec![ + "Loop Pressure".to_string(), + "Acc gas press".to_string(), + "Acc fluid vol".to_string(), + "Acc gas vol".to_string(), + ]; + let mut accu_green_history = History::new(green_acc_var_names); + + let yellow_acc_var_names = vec![ + "Loop Pressure".to_string(), + "Acc gas press".to_string(), + "Acc fluid vol".to_string(), + "Acc gas vol".to_string(), + ]; + let mut accu_yellow_history = History::new(yellow_acc_var_names); + + let mut epump = electric_pump(); + let mut epump_controller = TestPumpController::commanding_depressurise(); + let mut yellow_loop = hydraulic_loop("YELLOW"); + + let mut edp1 = engine_driven_pump(); + let mut edp1_controller = TestPumpController::commanding_depressurise(); + + let mut green_loop = hydraulic_loop("GREEN"); + + let loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); + + let mut ptu = PowerTransferUnit::new(); + let mut ptu_controller = TestPowerTransferUnitController::commanding_disabled(); + + let context = context(Duration::from_millis(100)); + + loop_history.init( + 0.0, + vec![ + green_loop.pressure().get::(), + yellow_loop.pressure().get::(), + green_loop.reservoir_volume().get::(), + yellow_loop.reservoir_volume().get::(), + green_loop.current_delta_vol().get::(), + yellow_loop.current_delta_vol().get::(), + ], + ); + ptu_history.init( + 0.0, + vec![ + ptu.flow().get::(), + green_loop.pressure().get::() - yellow_loop.pressure().get::(), + ptu.is_active_left_to_right() as i8 as f64, + ptu.is_active_right_to_left() as i8 as f64, + ], + ); + accu_green_history.init( + 0.0, + vec![ + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), + ], + ); + accu_yellow_history.init( + 0.0, + vec![ + yellow_loop.pressure().get::(), + yellow_loop.accumulator_gas_pressure().get::(), + yellow_loop.accumulator_fluid_volume().get::(), + yellow_loop.accumulator_gas_volume().get::(), + ], + ); + + let yellow_res_at_start = yellow_loop.reservoir_volume(); + let green_res_at_start = green_loop.reservoir_volume(); + + let edp_rpm = 3300.; + for x in 0..800 { + if x == 10 { + // After 1s powering electric pump + println!("------------YELLOW EPUMP ON------------"); + assert!(yellow_loop.pressure() <= Pressure::new::(50.0)); + assert!(yellow_loop.reservoir_volume() == yellow_res_at_start); + + assert!(green_loop.pressure() <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume() == green_res_at_start); + + epump_controller.command_pressurise(); + } + + if x == 110 { + // 10s later enabling ptu + println!("--------------PTU ENABLED--------------"); + assert!(yellow_loop.pressure() >= Pressure::new::(2950.0)); + assert!(yellow_loop.reservoir_volume() <= yellow_res_at_start); + + assert!(green_loop.pressure() <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume() == green_res_at_start); + + ptu_controller.command_enable(); + } + + if x == 300 { + // @30s, ptu should be supplying green loop + println!("----------PTU SUPPLIES GREEN------------"); + assert!(yellow_loop.pressure() >= Pressure::new::(2400.0)); + assert!(green_loop.pressure() >= Pressure::new::(2400.0)); + } + + if x == 400 { + // @40s enabling edp + println!("------------GREEN EDP1 ON------------"); + assert!(yellow_loop.pressure() >= Pressure::new::(2600.0)); + assert!(green_loop.pressure() >= Pressure::new::(2000.0)); + edp1_controller.command_pressurise(); + } + + if (500..=600).contains(&x) { + // 10s later and during 10s, ptu should stay inactive + println!("------------IS PTU ACTIVE??------------"); + assert!(yellow_loop.pressure() >= Pressure::new::(2900.0)); + assert!(green_loop.pressure() >= Pressure::new::(2900.0)); + } + + if x == 600 { + // @60s diabling edp and epump + println!("-------------ALL PUMPS OFF------------"); + assert!(yellow_loop.pressure() >= Pressure::new::(2900.0)); + assert!(green_loop.pressure() >= Pressure::new::(2900.0)); + edp1_controller.command_depressurise(); + epump_controller.command_depressurise(); + } + + if x == 800 { + // @80s diabling edp and epump + println!("-----------IS PRESSURE OFF?-----------"); + assert!(yellow_loop.pressure() < Pressure::new::(50.0)); + assert!(green_loop.pressure() <= Pressure::new::(50.0)); + + assert!( + green_loop.reservoir_volume() > Volume::new::(0.0) + && green_loop.reservoir_volume() <= green_res_at_start + ); + assert!( + yellow_loop.reservoir_volume() > Volume::new::(0.0) + && yellow_loop.reservoir_volume() <= yellow_res_at_start + ); + } + + ptu.update(&green_loop, &yellow_loop, &ptu_controller); + edp1.update(&context, &green_loop, edp_rpm, &edp1_controller); + epump.update(&context, &yellow_loop, &epump_controller); + + yellow_loop.update( + &context, + vec![&epump], + Vec::new(), + Vec::new(), + vec![&ptu], + &loop_controller, + ); + green_loop.update( + &context, + Vec::new(), + vec![&edp1], + Vec::new(), + vec![&ptu], + &loop_controller, + ); + + loop_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.pressure().get::(), + yellow_loop.pressure().get::(), + green_loop.reservoir_volume().get::(), + yellow_loop.reservoir_volume().get::(), + green_loop.current_delta_vol().get::(), + yellow_loop.current_delta_vol().get::(), + ], + ); + ptu_history.update( + context.delta_as_secs_f64(), + vec![ + ptu.flow().get::(), + green_loop.pressure().get::() - yellow_loop.pressure().get::(), + ptu.is_active_left_to_right() as i8 as f64, + ptu.is_active_right_to_left() as i8 as f64, + ], + ); + + accu_green_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), + ], + ); + accu_yellow_history.update( + context.delta_as_secs_f64(), + vec![ + yellow_loop.pressure().get::(), + yellow_loop.accumulator_gas_pressure().get::(), + yellow_loop.accumulator_fluid_volume().get::(), + yellow_loop.accumulator_gas_volume().get::(), + ], + ); + + if x % 20 == 0 { + println!("Iteration {}", x); + println!("-------------------------------------------"); + println!("---PSI YELLOW: {}", yellow_loop.pressure().get::()); + println!("---RPM YELLOW: {}", epump.rpm()); + println!( + "---Priming State: {}/{}", + yellow_loop.loop_fluid_volume().get::(), + yellow_loop.max_volume().get::() + ); + println!("---PSI GREEN: {}", green_loop.pressure().get::()); + println!("---EDP RPM GREEN: {}", edp_rpm); + println!( + "---Priming State: {}/{}", + green_loop.loop_fluid_volume().get::(), + green_loop.max_volume().get::() + ); + } + } + + loop_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Loop_press", &path); + ptu_history.show_matplotlib("yellow_green_ptu_loop_simulation()_PTU", &path); + + accu_green_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Green_acc", &path); + accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc", &path); +} + +fn yellow_epump_plus_edp2_with_ptu(path: &str) { + let loop_var_names = vec![ + "GREEN Loop Pressure".to_string(), + "YELLOW Loop Pressure".to_string(), + "GREEN Loop reservoir".to_string(), + "YELLOW Loop reservoir".to_string(), + "GREEN Loop delta vol".to_string(), + "YELLOW Loop delta vol".to_string(), + ]; + let mut loop_history = History::new(loop_var_names); + + let ptu_var_names = vec![ + "Current flow".to_string(), + "Press delta".to_string(), + "PTU active GREEN".to_string(), + "PTU active YELLOW".to_string(), + ]; + let mut ptu_history = History::new(ptu_var_names); + + let green_acc_var_names = vec![ + "Loop Pressure".to_string(), + "Acc gas press".to_string(), + "Acc fluid vol".to_string(), + "Acc gas vol".to_string(), + ]; + let mut accu_green_history = History::new(green_acc_var_names); + + let yellow_acc_var_names = vec![ + "Loop Pressure".to_string(), + "Acc gas press".to_string(), + "Acc fluid vol".to_string(), + "Acc gas vol".to_string(), + ]; + let mut accu_yellow_history = History::new(yellow_acc_var_names); + + let mut epump = electric_pump(); + let mut epump_controller = TestPumpController::commanding_depressurise(); + let mut yellow_loop = hydraulic_loop("YELLOW"); + + let mut edp2 = engine_driven_pump(); + let mut edp2_controller = TestPumpController::commanding_depressurise(); + + let edp_rpm = 3300.; + + let mut green_loop = hydraulic_loop("GREEN"); + + let loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); + + let mut ptu = PowerTransferUnit::new(); + let ptu_controller = TestPowerTransferUnitController::commanding_enabled(); + + let context = context(Duration::from_millis(100)); + + loop_history.init( + 0.0, + vec![ + green_loop.pressure().get::(), + yellow_loop.pressure().get::(), + green_loop.reservoir_volume().get::(), + yellow_loop.reservoir_volume().get::(), + green_loop.current_delta_vol().get::(), + yellow_loop.current_delta_vol().get::(), + ], + ); + ptu_history.init( + 0.0, + vec![ + ptu.flow().get::(), + green_loop.pressure().get::() - yellow_loop.pressure().get::(), + ptu.is_active_left_to_right() as i8 as f64, + ptu.is_active_right_to_left() as i8 as f64, + ], + ); + accu_green_history.init( + 0.0, + vec![ + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), + ], + ); + accu_yellow_history.init( + 0.0, + vec![ + yellow_loop.pressure().get::(), + yellow_loop.accumulator_gas_pressure().get::(), + yellow_loop.accumulator_fluid_volume().get::(), + yellow_loop.accumulator_gas_volume().get::(), + ], + ); + + for x in 0..800 { + if x == 10 { + // After 1s powering electric pump + epump_controller.command_pressurise(); + } + + if x == 110 { + // 10s later enabling edp2 + edp2_controller.command_pressurise(); + } + + if x >= 400 { + println!("Gpress={}", green_loop.pressure().get::()); + } + ptu.update(&green_loop, &yellow_loop, &ptu_controller); + edp2.update(&context, &yellow_loop, edp_rpm, &edp2_controller); + epump.update(&context, &yellow_loop, &epump_controller); + + yellow_loop.update( + &context, + vec![&epump], + vec![&edp2], + Vec::new(), + vec![&ptu], + &loop_controller, + ); + green_loop.update( + &context, + Vec::new(), + Vec::new(), + Vec::new(), + vec![&ptu], + &loop_controller, + ); + + loop_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.pressure().get::(), + yellow_loop.pressure().get::(), + green_loop.reservoir_volume().get::(), + yellow_loop.reservoir_volume().get::(), + green_loop.current_delta_vol().get::(), + yellow_loop.current_delta_vol().get::(), + ], + ); + ptu_history.update( + context.delta_as_secs_f64(), + vec![ + ptu.flow().get::(), + green_loop.pressure().get::() - yellow_loop.pressure().get::(), + ptu.is_active_left_to_right() as i8 as f64, + ptu.is_active_right_to_left() as i8 as f64, + ], + ); + + accu_green_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), + ], + ); + accu_yellow_history.update( + context.delta_as_secs_f64(), + vec![ + yellow_loop.pressure().get::(), + yellow_loop.accumulator_gas_pressure().get::(), + yellow_loop.accumulator_fluid_volume().get::(), + yellow_loop.accumulator_gas_volume().get::(), + ], + ); + } + + loop_history.show_matplotlib("yellow_epump_plus_edp2_with_ptu()_Loop_press", &path); + ptu_history.show_matplotlib("yellow_epump_plus_edp2_with_ptu()_PTU", &path); + + accu_green_history.show_matplotlib("yellow_epump_plus_edp2_with_ptu()_Green_acc", &path); + accu_yellow_history.show_matplotlib("yellow_epump_plus_edp2_with_ptu()_Yellow_acc", &path); +} + +fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { + match loop_color { + "GREEN" => HydraulicLoop::new( + loop_color, + true, + false, + Volume::new::(26.41), + Volume::new::(26.41), + Volume::new::(10.0), + Volume::new::(3.83), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(1450.), + Pressure::new::(1750.), + ), + "YELLOW" => HydraulicLoop::new( + loop_color, + false, + true, + Volume::new::(10.2), + Volume::new::(10.2), + Volume::new::(8.0), + Volume::new::(3.3), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(1450.), + Pressure::new::(1750.), + ), + _ => HydraulicLoop::new( + loop_color, + false, + false, + Volume::new::(15.85), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.5), + Fluid::new(Pressure::new::(1450000000.0)), + false, + Pressure::new::(1450.), + Pressure::new::(1750.), + ), + } +} + +fn electric_pump() -> ElectricPump { + ElectricPump::new("DEFAULT") +} + +fn engine_driven_pump() -> EngineDrivenPump { + EngineDrivenPump::new("DEFAULT") +} + +fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + ) +} diff --git a/src/systems/a320_systems/Cargo.toml b/src/systems/a320_systems/Cargo.toml index 331099f0257..1d49b65a880 100644 --- a/src/systems/a320_systems/Cargo.toml +++ b/src/systems/a320_systems/Cargo.toml @@ -6,4 +6,5 @@ edition = "2018" [dependencies] uom = "0.30.0" +rand = "0.8.0" systems = { path = "../systems" } diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 324a3ced367..721c50687f3 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,20 +1,3819 @@ -use systems::simulation::{SimulationElement, UpdateContext}; +use std::time::Duration; +use uom::si::{ + angular_velocity::revolution_per_minute, f64::*, pressure::pascal, pressure::psi, + ratio::percent, velocity::knot, volume::gallon, +}; -pub struct A320Hydraulic { - // Until hydraulic is implemented, we'll fake it with this boolean. - blue_pressurised: bool, +use systems::hydraulic::{ + ElectricPump, EngineDrivenPump, Fluid, HydraulicLoop, HydraulicLoopController, + PowerTransferUnit, PowerTransferUnitController, PressureSwitch, PumpController, RamAirTurbine, + RamAirTurbineController, +}; +use systems::overhead::{ + AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, +}; +use systems::simulation::{ + SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, +}; + +use systems::{engine::Engine, landing_gear::LandingGear}; +use systems::{ + hydraulic::brake_circuit::BrakeCircuit, shared::DelayedFalseLogicGate, + shared::DelayedTrueLogicGate, +}; + +pub(super) struct A320Hydraulic { + hyd_brake_logic: A320HydraulicBrakingLogic, + blue_loop: HydraulicLoop, + blue_loop_controller: A320HydraulicLoopController, + green_loop: HydraulicLoop, + green_loop_controller: A320HydraulicLoopController, + yellow_loop: HydraulicLoop, + yellow_loop_controller: A320HydraulicLoopController, + + engine_driven_pump_1_pressure_switch: PressureSwitch, + engine_driven_pump_1: EngineDrivenPump, + engine_driven_pump_1_controller: A320EngineDrivenPumpController, + + engine_driven_pump_2_pressure_switch: PressureSwitch, + engine_driven_pump_2: EngineDrivenPump, + engine_driven_pump_2_controller: A320EngineDrivenPumpController, + + blue_electric_pump: ElectricPump, + blue_electric_pump_controller: A320BlueElectricPumpController, + + yellow_electric_pump: ElectricPump, + yellow_electric_pump_controller: A320YellowElectricPumpController, + + forward_cargo_door: Door, + aft_cargo_door: Door, + pushback_tug: PushbackTug, + + ram_air_turbine: RamAirTurbine, + ram_air_turbine_controller: A320RamAirTurbineController, + + power_transfer_unit: PowerTransferUnit, + power_transfer_unit_controller: A320PowerTransferUnitController, + + braking_circuit_norm: BrakeCircuit, + braking_circuit_altn: BrakeCircuit, + total_sim_time_elapsed: Duration, + lag_time_accumulator: Duration, } impl A320Hydraulic { - pub fn new() -> A320Hydraulic { + const MIN_PRESS_EDP_SECTION_LO_HYST: f64 = 1740.0; + const MIN_PRESS_EDP_SECTION_HI_HYST: f64 = 2200.0; + const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1450.0; + const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 1750.0; + + // Refresh rate of hydraulic simulation + const HYDRAULIC_SIM_TIME_STEP_MILLISECONDS: u64 = 100; + // Refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update. + const ACTUATORS_SIM_TIME_STEP_MULTIPLIER: u32 = 2; + + pub(super) fn new() -> A320Hydraulic { A320Hydraulic { - blue_pressurised: true, + hyd_brake_logic: A320HydraulicBrakingLogic::new(), + + blue_loop: HydraulicLoop::new( + "BLUE", + false, + false, + Volume::new::(15.8), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.56), + Fluid::new(Pressure::new::(1450000000.0)), + false, + Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), + Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), + ), + blue_loop_controller: A320HydraulicLoopController::new(None), + green_loop: HydraulicLoop::new( + "GREEN", + true, + false, + Volume::new::(26.38), + Volume::new::(26.41), + Volume::new::(15.), + Volume::new::(3.6), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), + Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), + ), + green_loop_controller: A320HydraulicLoopController::new(Some(1)), + yellow_loop: HydraulicLoop::new( + "YELLOW", + false, + true, + Volume::new::(19.81), + Volume::new::(19.81), + Volume::new::(10.0), + Volume::new::(3.6), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), + Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), + ), + yellow_loop_controller: A320HydraulicLoopController::new(Some(2)), + + engine_driven_pump_1_pressure_switch: PressureSwitch::new( + Pressure::new::(Self::MIN_PRESS_EDP_SECTION_HI_HYST), + Pressure::new::(Self::MIN_PRESS_EDP_SECTION_LO_HYST), + ), + engine_driven_pump_1: EngineDrivenPump::new("GREEN"), + engine_driven_pump_1_controller: A320EngineDrivenPumpController::new(1), + + engine_driven_pump_2_pressure_switch: PressureSwitch::new( + Pressure::new::(Self::MIN_PRESS_EDP_SECTION_HI_HYST), + Pressure::new::(Self::MIN_PRESS_EDP_SECTION_LO_HYST), + ), + engine_driven_pump_2: EngineDrivenPump::new("YELLOW"), + engine_driven_pump_2_controller: A320EngineDrivenPumpController::new(2), + + blue_electric_pump: ElectricPump::new("BLUE"), + blue_electric_pump_controller: A320BlueElectricPumpController::new(), + + yellow_electric_pump: ElectricPump::new("YELLOW"), + yellow_electric_pump_controller: A320YellowElectricPumpController::new(), + + forward_cargo_door: Door::new(5), + aft_cargo_door: Door::new(3), + pushback_tug: PushbackTug::new(), + + ram_air_turbine: RamAirTurbine::new(), + ram_air_turbine_controller: A320RamAirTurbineController::new(), + + power_transfer_unit: PowerTransferUnit::new(), + power_transfer_unit_controller: A320PowerTransferUnitController::new(), + + braking_circuit_norm: BrakeCircuit::new( + "NORM", + Volume::new::(0.), + Volume::new::(0.), + Volume::new::(0.13), + ), + + braking_circuit_altn: BrakeCircuit::new( + "ALTN", + Volume::new::(1.5), + Volume::new::(0.5), + Volume::new::(0.13), + ), + + total_sim_time_elapsed: Duration::new(0, 0), + lag_time_accumulator: Duration::new(0, 0), + } + } + + pub(super) fn update( + &mut self, + context: &UpdateContext, + engine1: &T, + engine2: &T, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + landing_gear: &LandingGear, + ) { + let min_hyd_loop_timestep = + Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP_MILLISECONDS); + + self.total_sim_time_elapsed += context.delta(); + + // Time to catch up in our simulation = new delta + time not updated last iteration + let time_to_catch = context.delta() + self.lag_time_accumulator; + + // Number of time steps (with floating part) to do according to required time step + let number_of_steps_floating_point = + time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); + + // Here we update everything requiring same refresh as the sim calls us, more likely visual stuff + self.update_at_every_frames(&context); + + if number_of_steps_floating_point < 1.0 { + // Can't do a full time step + // we can decide either do an update with smaller step or wait next iteration + // for now we only update lag accumulator and chose a hard fixed step: if smaller + // than chosen time step we do nothing and wait next iteration + + // Time lag is float part only of num of steps (because is < 1.0 here) * fixed time step to get a result in time + self.lag_time_accumulator = Duration::from_secs_f64( + number_of_steps_floating_point * min_hyd_loop_timestep.as_secs_f64(), + ); + } else { + // Int part is the actual number of loops to do + // rest of floating part goes into accumulator + let num_of_update_loops = number_of_steps_floating_point.floor() as u32; + + self.lag_time_accumulator = Duration::from_secs_f64( + (number_of_steps_floating_point - (num_of_update_loops as f64)) + * min_hyd_loop_timestep.as_secs_f64(), + ); // Keep track of time left after all fixed loop are done + + // Then run fixed update loop for main hydraulics + for _ in 0..num_of_update_loops { + // First update what is currently consumed and given back by each actuator + // Todo: might have to split the actuator volumes by expected number of loops + self.update_actuators_volume(); + + self.update_fixed_step( + &context.with_delta(min_hyd_loop_timestep), + engine1, + engine2, + overhead_panel, + engine_fire_overhead, + landing_gear, + ); + } + + // This is the "fast" update loop refreshing ACTUATORS_SIM_TIME_STEP_MULT times faster + // here put everything that needs higher simulation rates like physics solving + let num_of_actuators_update_loops = + num_of_update_loops * Self::ACTUATORS_SIM_TIME_STEP_MULTIPLIER; + + // If X times faster we divide step by X + let delta_time_physics = + min_hyd_loop_timestep / Self::ACTUATORS_SIM_TIME_STEP_MULTIPLIER; + for _ in 0..num_of_actuators_update_loops { + self.update_fast_rate(&context, &delta_time_physics); + } + } + } + + fn green_edp_has_low_press_fault(&self) -> bool { + self.engine_driven_pump_1_controller + .has_pressure_low_fault() + } + + fn yellow_epump_has_low_press_fault(&self) -> bool { + self.yellow_electric_pump_controller + .has_pressure_low_fault() + } + + fn yellow_edp_has_low_press_fault(&self) -> bool { + self.engine_driven_pump_2_controller + .has_pressure_low_fault() + } + + fn blue_epump_has_fault(&self) -> bool { + self.blue_electric_pump_controller.has_pressure_low_fault() + } + + #[cfg(test)] + fn should_pressurise_yellow_pump_for_cargo_door_operation(&self) -> bool { + self.yellow_electric_pump_controller + .should_pressurise_for_cargo_door_operation() + } + + #[cfg(test)] + fn nose_wheel_steering_pin_is_inserted(&self) -> bool { + self.power_transfer_unit_controller + .nose_wheel_steering_pin_is_inserted() + } + + pub(super) fn is_blue_pressurised(&self) -> bool { + self.blue_loop.is_pressurised() + } + + #[cfg(test)] + fn is_green_pressurised(&self) -> bool { + self.green_loop.is_pressurised() + } + + #[cfg(test)] + fn is_yellow_pressurised(&self) -> bool { + self.yellow_loop.is_pressurised() + } + + // Update with same refresh rate as the sim + fn update_at_every_frames(&mut self, context: &UpdateContext) { + // Updating rat stowed pos on all frames in case it's used for graphics + self.ram_air_turbine.update_position(&context.delta()); + + // Tug has its angle changing on each frame and we'd like to detect this + self.pushback_tug.update(); + } + + // All the higher frequency updates like physics + fn update_fast_rate(&mut self, context: &UpdateContext, delta_time_physics: &Duration) { + self.ram_air_turbine + .update_physics(&delta_time_physics, &context.indicated_airspeed()); + } + + // For each hydraulic loop retrieves volumes from and to each actuator and pass it to the loops + fn update_actuators_volume(&mut self) { + self.update_green_actuators_volume(); + self.update_yellow_actuators_volume(); + self.update_blue_actuators_volume(); + } + + fn update_green_actuators_volume(&mut self) { + self.green_loop + .update_actuator_volumes(&self.braking_circuit_norm); + self.braking_circuit_norm.reset_accumulators(); + } + + fn update_yellow_actuators_volume(&mut self) { + self.yellow_loop + .update_actuator_volumes(&self.braking_circuit_altn); + self.braking_circuit_altn.reset_accumulators(); + } + + fn update_blue_actuators_volume(&mut self) {} + + // All the core hydraulics updates that needs to be done at the slowest fixed step rate + fn update_fixed_step( + &mut self, + context: &UpdateContext, + engine1: &T, + engine2: &T, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + landing_gear: &LandingGear, + ) { + // Process brake logic (which circuit brakes) and send brake demands (how much) + self.hyd_brake_logic.update_brake_demands( + context, + &self.green_loop, + &self.braking_circuit_altn, + &landing_gear, + ); + self.hyd_brake_logic.update_brake_pressure_limitation( + &mut self.braking_circuit_norm, + &mut self.braking_circuit_altn, + ); + self.hyd_brake_logic.send_brake_demands( + &mut self.braking_circuit_norm, + &mut self.braking_circuit_altn, + ); + + self.power_transfer_unit_controller.update( + context, + overhead_panel, + &self.forward_cargo_door, + &self.aft_cargo_door, + &self.pushback_tug, + ); + self.power_transfer_unit.update( + &self.green_loop, + &self.yellow_loop, + &self.power_transfer_unit_controller, + ); + + self.engine_driven_pump_1_pressure_switch + .update(self.green_loop.pressure()); + self.engine_driven_pump_1_controller.update( + overhead_panel, + engine_fire_overhead, + engine1.corrected_n2(), + engine1.oil_pressure(), + self.engine_driven_pump_1_pressure_switch.is_pressurised(), + ); + + self.engine_driven_pump_1.update( + context, + &self.green_loop, + engine1 + .hydraulic_pump_output_speed() + .get::(), + &self.engine_driven_pump_1_controller, + ); + + self.engine_driven_pump_2_pressure_switch + .update(self.yellow_loop.pressure()); + self.engine_driven_pump_2_controller.update( + overhead_panel, + engine_fire_overhead, + engine2.corrected_n2(), + engine2.oil_pressure(), + self.engine_driven_pump_2_pressure_switch.is_pressurised(), + ); + + self.engine_driven_pump_2.update( + context, + &self.yellow_loop, + engine2 + .hydraulic_pump_output_speed() + .get::(), + &self.engine_driven_pump_2_controller, + ); + + self.blue_electric_pump_controller.update( + overhead_panel, + self.blue_loop.is_pressurised(), + engine1.oil_pressure(), + engine2.oil_pressure(), + engine1.is_above_minimum_idle(), + engine2.is_above_minimum_idle(), + ); + self.blue_electric_pump.update( + context, + &self.blue_loop, + &self.blue_electric_pump_controller, + ); + + self.yellow_electric_pump_controller.update( + context, + overhead_panel, + &self.forward_cargo_door, + &self.aft_cargo_door, + self.yellow_loop.is_pressurised(), + ); + self.yellow_electric_pump.update( + context, + &self.yellow_loop, + &self.yellow_electric_pump_controller, + ); + + self.ram_air_turbine_controller.update(context); + self.ram_air_turbine + .update(context, &self.blue_loop, &self.ram_air_turbine_controller); + + self.green_loop_controller.update(engine_fire_overhead); + self.green_loop.update( + context, + Vec::new(), + vec![&self.engine_driven_pump_1], + Vec::new(), + vec![&self.power_transfer_unit], + &self.green_loop_controller, + ); + + self.yellow_loop_controller.update(engine_fire_overhead); + self.yellow_loop.update( + context, + vec![&self.yellow_electric_pump], + vec![&self.engine_driven_pump_2], + Vec::new(), + vec![&self.power_transfer_unit], + &self.yellow_loop_controller, + ); + + self.blue_loop_controller.update(engine_fire_overhead); + self.blue_loop.update( + context, + vec![&self.blue_electric_pump], + Vec::new(), + vec![&self.ram_air_turbine], + Vec::new(), + &self.blue_loop_controller, + ); + + self.braking_circuit_norm.update(context, &self.green_loop); + self.braking_circuit_altn.update(context, &self.yellow_loop); + } +} +impl SimulationElement for A320Hydraulic { + fn accept(&mut self, visitor: &mut T) { + self.engine_driven_pump_1.accept(visitor); + self.engine_driven_pump_1_controller.accept(visitor); + + self.engine_driven_pump_2.accept(visitor); + self.engine_driven_pump_2_controller.accept(visitor); + + self.blue_electric_pump.accept(visitor); + self.blue_electric_pump_controller.accept(visitor); + + self.yellow_electric_pump.accept(visitor); + self.yellow_electric_pump_controller.accept(visitor); + + self.forward_cargo_door.accept(visitor); + self.aft_cargo_door.accept(visitor); + self.pushback_tug.accept(visitor); + + self.ram_air_turbine.accept(visitor); + self.ram_air_turbine_controller.accept(visitor); + + self.power_transfer_unit.accept(visitor); + self.power_transfer_unit_controller.accept(visitor); + + self.blue_loop.accept(visitor); + self.green_loop.accept(visitor); + self.yellow_loop.accept(visitor); + + self.hyd_brake_logic.accept(visitor); + + self.braking_circuit_norm.accept(visitor); + self.braking_circuit_altn.accept(visitor); + + visitor.visit(self); + } +} + +struct A320HydraulicLoopController { + engine_number: Option, + should_open_fire_shutoff_valve: bool, +} +impl A320HydraulicLoopController { + fn new(engine_number: Option) -> Self { + Self { + engine_number, + should_open_fire_shutoff_valve: true, + } + } + + fn update(&mut self, engine_fire_overhead: &A320EngineFireOverheadPanel) { + if let Some(eng_number) = self.engine_number { + self.should_open_fire_shutoff_valve = + !engine_fire_overhead.fire_push_button_is_released(eng_number); + } + } +} +impl HydraulicLoopController for A320HydraulicLoopController { + fn should_open_fire_shutoff_valve(&self) -> bool { + self.should_open_fire_shutoff_valve + } +} + +struct A320EngineDrivenPumpController { + engine_number: usize, + engine_master_on_id: String, + engine_master_on: bool, + weight_on_wheels: bool, + should_pressurise: bool, + has_pressure_low_fault: bool, + is_pressure_low: bool, +} +impl A320EngineDrivenPumpController { + const MIN_ENGINE_OIL_PRESS_THRESHOLD_TO_INHIBIT_FAULT: f64 = 18.; + + fn new(engine_number: usize) -> Self { + Self { + engine_number, + engine_master_on_id: format!("GENERAL ENG STARTER ACTIVE:{}", engine_number), + engine_master_on: false, + weight_on_wheels: true, + should_pressurise: true, + has_pressure_low_fault: false, + is_pressure_low: true, + } + } + + fn update_low_pressure_state( + &mut self, + engine_n2: Ratio, + engine_oil_pressure: Pressure, + pressure_switch_state: bool, + ) { + // Faking edp section pressure low level as if engine is slow we shouldn't have pressure + let faked_is_edp_section_low_pressure = engine_n2.get::() < 5.; + + // Engine off state uses oil pressure threshold (treshold is 18psi) + let is_engine_low_oil_pressure = engine_oil_pressure.get::() + < Self::MIN_ENGINE_OIL_PRESS_THRESHOLD_TO_INHIBIT_FAULT; + + // TODO when edp section pressure is modeled we can remove fake low press and use dedicated pressure switch + self.is_pressure_low = self.should_pressurise() + && (!pressure_switch_state || faked_is_edp_section_low_pressure); + + // Fault inhibited if on ground AND engine oil pressure is low (11KS1 elec relay) + self.has_pressure_low_fault = + self.is_pressure_low && (!is_engine_low_oil_pressure || !self.weight_on_wheels); + } + + fn update( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + engine_n2: Ratio, + engine_oil_pressure: Pressure, + pressure_switch_state: bool, + ) { + if overhead_panel.edp_push_button_is_auto(self.engine_number) + && !engine_fire_overhead.fire_push_button_is_released(self.engine_number) + { + self.should_pressurise = true; + } else if overhead_panel.edp_push_button_is_off(self.engine_number) + || engine_fire_overhead.fire_push_button_is_released(self.engine_number) + { + self.should_pressurise = false; + } + + self.update_low_pressure_state(engine_n2, engine_oil_pressure, pressure_switch_state); + } + + fn has_pressure_low_fault(&self) -> bool { + self.has_pressure_low_fault + } +} +impl PumpController for A320EngineDrivenPumpController { + fn should_pressurise(&self) -> bool { + self.should_pressurise + } +} +impl SimulationElement for A320EngineDrivenPumpController { + fn read(&mut self, state: &mut SimulatorReader) { + self.engine_master_on = state.read_bool(&self.engine_master_on_id); + self.weight_on_wheels = state.read_bool("SIM ON GROUND"); + } + + fn write(&self, writer: &mut SimulatorWriter) { + if self.engine_number == 1 { + writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.is_pressure_low); + } else if self.engine_number == 2 { + writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.is_pressure_low); + } else { + panic!("The A320 only supports two engines."); + } + } +} + +struct A320BlueElectricPumpController { + should_pressurise: bool, + has_pressure_low_fault: bool, + is_pressure_low: bool, + weight_on_wheels: bool, +} +impl A320BlueElectricPumpController { + const MIN_ENGINE_OIL_PRESS_THRESHOLD_TO_INHIBIT_FAULT: f64 = 18.; + + fn new() -> Self { + Self { + should_pressurise: false, + has_pressure_low_fault: false, + is_pressure_low: true, + weight_on_wheels: true, + } + } + + fn update( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + pressure_switch_state: bool, + engine1_oil_pressure: Pressure, + engine2_oil_pressure: Pressure, + engine1_above_min_idle: bool, + engine2_above_min_idle: bool, + ) { + if overhead_panel.blue_epump_push_button.is_auto() { + if !self.weight_on_wheels + || engine1_above_min_idle + || engine2_above_min_idle + || overhead_panel.blue_epump_override_push_button_is_on() + { + self.should_pressurise = true; + } else { + self.should_pressurise = false; + } + } else if overhead_panel.blue_epump_push_button_is_off() { + self.should_pressurise = false; + } + + self.update_low_pressure_state( + overhead_panel, + pressure_switch_state, + engine1_oil_pressure, + engine2_oil_pressure, + ); + } + + fn update_low_pressure_state( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + pressure_switch_state: bool, + engine1_oil_pressure: Pressure, + engine2_oil_pressure: Pressure, + ) { + // Low engine oil pressure inhibits fault under 18psi level + let is_engine_low_oil_pressure = engine1_oil_pressure.get::() + < Self::MIN_ENGINE_OIL_PRESS_THRESHOLD_TO_INHIBIT_FAULT + && engine2_oil_pressure.get::() + < Self::MIN_ENGINE_OIL_PRESS_THRESHOLD_TO_INHIBIT_FAULT; + + self.is_pressure_low = self.should_pressurise() && !pressure_switch_state; + + self.has_pressure_low_fault = self.is_pressure_low + && ((!is_engine_low_oil_pressure || !self.weight_on_wheels) + || overhead_panel.blue_epump_override_push_button_is_on()); + } + + fn has_pressure_low_fault(&self) -> bool { + self.has_pressure_low_fault + } +} + +impl PumpController for A320BlueElectricPumpController { + fn should_pressurise(&self) -> bool { + self.should_pressurise + } +} + +impl SimulationElement for A320BlueElectricPumpController { + fn read(&mut self, state: &mut SimulatorReader) { + self.weight_on_wheels = state.read_bool("SIM ON GROUND"); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.is_pressure_low); + } +} + +impl Default for A320BlueElectricPumpController { + fn default() -> Self { + Self::new() + } +} + +struct A320YellowElectricPumpController { + should_pressurise: bool, + has_pressure_low_fault: bool, + is_pressure_low: bool, + should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate, +} +impl A320YellowElectricPumpController { + const DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION: Duration = + Duration::from_secs(20); + + fn new() -> Self { + Self { + should_pressurise: false, + has_pressure_low_fault: false, + is_pressure_low: true, + should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate::new( + Self::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, + ), + } + } + + fn update( + &mut self, + context: &UpdateContext, + overhead_panel: &A320HydraulicOverheadPanel, + forward_cargo_door: &Door, + aft_cargo_door: &Door, + pressure_switch_state: bool, + ) { + self.should_activate_yellow_pump_for_cargo_door_operation + .update( + context, + forward_cargo_door.has_moved() || aft_cargo_door.has_moved(), + ); + + self.should_pressurise = overhead_panel.yellow_epump_push_button.is_on() + || self + .should_activate_yellow_pump_for_cargo_door_operation + .output(); + + self.update_low_pressure_state(pressure_switch_state); + } + + fn update_low_pressure_state(&mut self, pressure_switch_state: bool) { + self.is_pressure_low = self.should_pressurise() && !pressure_switch_state; + + self.has_pressure_low_fault = self.is_pressure_low; + } + + fn has_pressure_low_fault(&self) -> bool { + self.has_pressure_low_fault + } + + #[cfg(test)] + fn should_pressurise_for_cargo_door_operation(&self) -> bool { + self.should_activate_yellow_pump_for_cargo_door_operation + .output() + } +} +impl PumpController for A320YellowElectricPumpController { + fn should_pressurise(&self) -> bool { + self.should_pressurise + } +} +impl SimulationElement for A320YellowElectricPumpController { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.is_pressure_low); + } +} +impl Default for A320YellowElectricPumpController { + fn default() -> Self { + Self::new() + } +} + +struct A320PowerTransferUnitController { + should_enable: bool, + should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate, + nose_wheel_steering_pin_inserted: DelayedFalseLogicGate, + + parking_brake_lever_pos: bool, + eng_1_master_on: bool, + eng_2_master_on: bool, + weight_on_wheels: bool, +} +impl A320PowerTransferUnitController { + const DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION: Duration = Duration::from_secs(40); + const DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK: Duration = + Duration::from_secs(15); + + fn new() -> Self { + Self { + should_enable: false, + should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate::new( + Self::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, + ), + nose_wheel_steering_pin_inserted: DelayedFalseLogicGate::new( + Self::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, + ), + + parking_brake_lever_pos: false, + eng_1_master_on: false, + eng_2_master_on: false, + weight_on_wheels: false, + } + } + + fn update( + &mut self, + context: &UpdateContext, + overhead_panel: &A320HydraulicOverheadPanel, + forward_cargo_door: &Door, + aft_cargo_door: &Door, + pushback_tug: &PushbackTug, + ) { + self.should_inhibit_ptu_after_cargo_door_operation.update( + context, + forward_cargo_door.has_moved() || aft_cargo_door.has_moved(), + ); + self.nose_wheel_steering_pin_inserted + .update(context, pushback_tug.is_connected()); + + let ptu_inhibited = self.should_inhibit_ptu_after_cargo_door_operation.output() + && overhead_panel.yellow_epump_push_button_is_auto(); + + self.should_enable = overhead_panel.ptu_push_button_is_auto() + && (!self.weight_on_wheels + || self.eng_1_master_on && self.eng_2_master_on + || !self.eng_1_master_on && !self.eng_2_master_on + || (!self.parking_brake_lever_pos + && !self.nose_wheel_steering_pin_inserted.output())) + && !ptu_inhibited; + } + + #[cfg(test)] + fn nose_wheel_steering_pin_is_inserted(&self) -> bool { + self.nose_wheel_steering_pin_inserted.output() + } +} +impl PowerTransferUnitController for A320PowerTransferUnitController { + fn should_enable(&self) -> bool { + self.should_enable + } +} +impl SimulationElement for A320PowerTransferUnitController { + fn read(&mut self, state: &mut SimulatorReader) { + self.parking_brake_lever_pos = state.read_bool("BRAKE PARKING INDICATOR"); + self.eng_1_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:1"); + self.eng_2_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:2"); + self.weight_on_wheels = state.read_bool("SIM ON GROUND"); + } +} + +struct A320RamAirTurbineController { + should_deploy: bool, + eng_1_master_on: bool, + eng_2_master_on: bool, +} +impl A320RamAirTurbineController { + fn new() -> Self { + Self { + should_deploy: false, + eng_1_master_on: false, + eng_2_master_on: false, + } + } + + fn update(&mut self, context: &UpdateContext) { + // RAT Deployment + // Todo check all other needed conditions this is faked with engine master while it should check elec buses + self.should_deploy = !self.eng_1_master_on + && !self.eng_2_master_on + // Todo get speed from ADIRS + && context.indicated_airspeed() > Velocity::new::(100.) + } +} +impl RamAirTurbineController for A320RamAirTurbineController { + fn should_deploy(&self) -> bool { + self.should_deploy + } +} +impl SimulationElement for A320RamAirTurbineController { + fn read(&mut self, state: &mut SimulatorReader) { + self.eng_1_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:1"); + self.eng_2_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:2"); + } +} + +struct A320HydraulicBrakingLogic { + parking_brake_demand: bool, + weight_on_wheels: bool, + is_gear_lever_down: bool, + left_brake_pilot_input: f64, + right_brake_pilot_input: f64, + left_brake_green_output: f64, + left_brake_yellow_output: f64, + right_brake_green_output: f64, + right_brake_yellow_output: f64, + normal_brakes_available: bool, + should_disable_auto_brake_when_retracting: DelayedTrueLogicGate, + anti_skid_activated: bool, + autobrakes_setting: u8, +} +/// Implements brakes computers logic +impl A320HydraulicBrakingLogic { + // Minimum pressure hysteresis on green until main switched on ALTN brakes + // Feedback by Cpt. Chaos — 25/04/2021 #pilot-feedback + const MIN_PRESSURE_BRAKE_ALTN_HYST_LO: f64 = 1305.; + const MIN_PRESSURE_BRAKE_ALTN_HYST_HI: f64 = 2176.; + + // Min pressure when parking brake enabled. Lower normal braking is allowed to use pilot input as emergency braking + // Feedback by avteknisyan — 25/04/2021 #pilot-feedback + const MIN_PRESSURE_PARK_BRAKE_EMERGENCY: f64 = 507.; + + const AUTOBRAKE_GEAR_RETRACTION_DURATION_S: f64 = 3.; + + fn new() -> A320HydraulicBrakingLogic { + A320HydraulicBrakingLogic { + // Position of parking brake lever + parking_brake_demand: true, + weight_on_wheels: true, + is_gear_lever_down: true, + left_brake_pilot_input: 0.0, + right_brake_pilot_input: 0.0, + // Actual command sent to left green circuit + left_brake_green_output: 0.0, + // Actual command sent to left yellow circuit. Init 1 as considering park brake on on init + left_brake_yellow_output: 1.0, + // Actual command sent to right green circuit + right_brake_green_output: 0.0, + // Actual command sent to right yellow circuit. Init 1 as considering park brake on on init + right_brake_yellow_output: 1.0, + normal_brakes_available: false, + should_disable_auto_brake_when_retracting: DelayedTrueLogicGate::new( + Duration::from_secs_f64(Self::AUTOBRAKE_GEAR_RETRACTION_DURATION_S), + ), + anti_skid_activated: true, + autobrakes_setting: 0, + } + } + + fn update_normal_braking_availability(&mut self, normal_braking_loop_pressure: &Pressure) { + if normal_braking_loop_pressure.get::() > Self::MIN_PRESSURE_BRAKE_ALTN_HYST_HI + && (self.left_brake_pilot_input < 0.2 && self.right_brake_pilot_input < 0.2) + { + self.normal_brakes_available = true; + } else if normal_braking_loop_pressure.get::() < Self::MIN_PRESSURE_BRAKE_ALTN_HYST_LO + { + self.normal_brakes_available = false; + } + } + + fn update_brake_pressure_limitation( + &mut self, + norm_brk: &mut BrakeCircuit, + altn_brk: &mut BrakeCircuit, + ) { + let yellow_manual_braking_input = self.left_brake_pilot_input + > self.left_brake_yellow_output + 0.2 + || self.right_brake_pilot_input > self.right_brake_yellow_output + 0.2; + + // Nominal braking from pedals is limited to 2538psi + norm_brk.set_brake_limit_active(true); + norm_brk.set_brake_press_limit(Pressure::new::(2538.)); + + if self.parking_brake_demand { + altn_brk.set_brake_limit_active(true); + + // If no pilot action, standard park brake pressure limit + if !yellow_manual_braking_input { + altn_brk.set_brake_press_limit(Pressure::new::(2103.)); + } else { + // Else manual action limited to a higher max nominal pressure + altn_brk.set_brake_press_limit(Pressure::new::(2538.)); + } + } else if !self.anti_skid_activated { + altn_brk.set_brake_press_limit(Pressure::new::(1160.)); + altn_brk.set_brake_limit_active(true); + } else { + // Else if any manual braking we use standard limit + altn_brk.set_brake_press_limit(Pressure::new::(2538.)); + altn_brk.set_brake_limit_active(true); + } + } + + /// Updates final brake demands per hydraulic loop based on pilot pedal demands + fn update_brake_demands( + &mut self, + context: &UpdateContext, + green_loop: &HydraulicLoop, + alternate_circuit: &BrakeCircuit, + landing_gear: &LandingGear, + ) { + self.update_normal_braking_availability(&green_loop.pressure()); + + let is_in_flight_gear_lever_up = !self.weight_on_wheels && !self.is_gear_lever_down; + self.should_disable_auto_brake_when_retracting.update( + context, + !landing_gear.is_down_and_locked() && !self.is_gear_lever_down, + ); + + if is_in_flight_gear_lever_up { + if self.should_disable_auto_brake_when_retracting.output() { + self.left_brake_green_output = 0.; + self.right_brake_green_output = 0.; + } else { + // Slight brake pressure to stop the spinning wheels (have no pressure data available yet, 0.2 is random one) + self.left_brake_green_output = 0.2; + self.right_brake_green_output = 0.2; + } + + self.left_brake_yellow_output = 0.; + self.right_brake_yellow_output = 0.; + } else { + let green_used_for_brakes = self.normal_brakes_available + && self.anti_skid_activated + && !self.parking_brake_demand; + + if green_used_for_brakes { + self.left_brake_green_output = self.left_brake_pilot_input; + self.right_brake_green_output = self.right_brake_pilot_input; + self.left_brake_yellow_output = 0.; + self.right_brake_yellow_output = 0.; + } else { + self.left_brake_green_output = 0.; + self.right_brake_green_output = 0.; + if !self.parking_brake_demand { + // Normal braking but using alternate circuit + self.left_brake_yellow_output = self.left_brake_pilot_input; + self.right_brake_yellow_output = self.right_brake_pilot_input; + } else { + // Else we just use parking brake + self.left_brake_yellow_output = 1.; + self.right_brake_yellow_output = 1.; + + // Special case: parking brake on but yellow can't provide enough brakes: green are allowed to brake for emergency + if alternate_circuit.left_brake_pressure().get::() + < Self::MIN_PRESSURE_PARK_BRAKE_EMERGENCY + || alternate_circuit.right_brake_pressure().get::() + < Self::MIN_PRESSURE_PARK_BRAKE_EMERGENCY + { + self.left_brake_green_output = self.left_brake_pilot_input; + self.right_brake_green_output = self.right_brake_pilot_input; + } + } + } + } + + // Limiting final values + self.left_brake_yellow_output = self.left_brake_yellow_output.min(1.).max(0.); + self.right_brake_yellow_output = self.right_brake_yellow_output.min(1.).max(0.); + self.left_brake_green_output = self.left_brake_green_output.min(1.).max(0.); + self.right_brake_green_output = self.right_brake_green_output.min(1.).max(0.); + } + + fn send_brake_demands(&mut self, norm: &mut BrakeCircuit, altn: &mut BrakeCircuit) { + norm.set_brake_demand_left(self.left_brake_green_output); + norm.set_brake_demand_right(self.right_brake_green_output); + altn.set_brake_demand_left(self.left_brake_yellow_output); + altn.set_brake_demand_right(self.right_brake_yellow_output); + } +} + +impl SimulationElement for A320HydraulicBrakingLogic { + fn read(&mut self, state: &mut SimulatorReader) { + self.parking_brake_demand = state.read_bool("BRAKE PARKING INDICATOR"); + self.weight_on_wheels = state.read_bool("SIM ON GROUND"); + self.is_gear_lever_down = state.read_bool("GEAR HANDLE POSITION"); + self.anti_skid_activated = state.read_bool("ANTISKID BRAKES ACTIVE"); + self.left_brake_pilot_input = state.read_f64("BRAKE LEFT POSITION") / 100.0; + self.right_brake_pilot_input = state.read_f64("BRAKE RIGHT POSITION") / 100.0; + self.autobrakes_setting = state.read_f64("AUTOBRAKES SETTING").floor() as u8; + } +} + +struct Door { + exit_id: String, + position: f64, + previous_position: f64, +} +impl Door { + fn new(id: usize) -> Self { + Self { + exit_id: format!("EXIT OPEN:{}", id), + position: 0., + previous_position: 0., + } + } + + fn has_moved(&self) -> bool { + (self.position - self.previous_position).abs() > f64::EPSILON + } +} +impl SimulationElement for Door { + fn read(&mut self, state: &mut SimulatorReader) { + self.previous_position = self.position; + self.position = state.read_f64(&self.exit_id); + } +} + +struct PushbackTug { + angle: f64, + previous_angle: f64, + // Type of pushback: + // 0 = Straight + // 1 = Left + // 2 = Right + // 3 = Assumed to be no pushback + // 4 = might be finishing pushback, to confirm + state: f64, + is_connected_to_nose_gear: bool, +} +impl PushbackTug { + const STATE_NO_PUSHBACK: f64 = 3.; + + fn new() -> Self { + Self { + angle: 0., + previous_angle: 0., + state: Self::STATE_NO_PUSHBACK, + is_connected_to_nose_gear: false, } } - pub fn is_blue_pressurised(&self) -> bool { - self.blue_pressurised + fn update(&mut self) { + if self.is_pushing() { + self.is_connected_to_nose_gear = true; + } else if (self.state - PushbackTug::STATE_NO_PUSHBACK).abs() <= f64::EPSILON { + self.is_connected_to_nose_gear = false; + } + } + + fn is_connected(&self) -> bool { + self.is_connected_to_nose_gear } - pub fn update(&mut self, _: &UpdateContext) {} + fn is_pushing(&self) -> bool { + // The angle keeps changing while pushing or is frozen high on high angle manoeuvering. + (self.angle - self.previous_angle).abs() > f64::EPSILON + && (self.state - PushbackTug::STATE_NO_PUSHBACK).abs() > f64::EPSILON + } +} +impl SimulationElement for PushbackTug { + fn read(&mut self, state: &mut SimulatorReader) { + self.previous_angle = self.angle; + self.angle = state.read_f64("PUSHBACK ANGLE"); + self.state = state.read_f64("PUSHBACK STATE"); + } +} + +pub(super) struct A320HydraulicOverheadPanel { + edp1_push_button: AutoOffFaultPushButton, + edp2_push_button: AutoOffFaultPushButton, + blue_epump_push_button: AutoOffFaultPushButton, + ptu_push_button: AutoOffFaultPushButton, + rat_push_button: AutoOffFaultPushButton, + yellow_epump_push_button: AutoOnFaultPushButton, + blue_epump_override_push_button: OnOffFaultPushButton, +} +impl A320HydraulicOverheadPanel { + pub(super) fn new() -> A320HydraulicOverheadPanel { + A320HydraulicOverheadPanel { + edp1_push_button: AutoOffFaultPushButton::new_auto("HYD_ENG_1_PUMP"), + edp2_push_button: AutoOffFaultPushButton::new_auto("HYD_ENG_2_PUMP"), + blue_epump_push_button: AutoOffFaultPushButton::new_auto("HYD_EPUMPB"), + ptu_push_button: AutoOffFaultPushButton::new_auto("HYD_PTU"), + rat_push_button: AutoOffFaultPushButton::new_off("HYD_RAT"), + yellow_epump_push_button: AutoOnFaultPushButton::new_auto("HYD_EPUMPY"), + blue_epump_override_push_button: OnOffFaultPushButton::new_off("HYD_EPUMPY_OVRD"), + } + } + + pub(super) fn update(&mut self, hyd: &A320Hydraulic) { + self.edp1_push_button + .set_fault(hyd.green_edp_has_low_press_fault()); + self.edp2_push_button + .set_fault(hyd.yellow_edp_has_low_press_fault()); + self.blue_epump_push_button + .set_fault(hyd.blue_epump_has_fault()); + self.yellow_epump_push_button + .set_fault(hyd.yellow_epump_has_low_press_fault()); + } + + fn yellow_epump_push_button_is_auto(&self) -> bool { + self.yellow_epump_push_button.is_auto() + } + + fn ptu_push_button_is_auto(&self) -> bool { + self.ptu_push_button.is_auto() + } + + fn edp_push_button_is_auto(&self, number: usize) -> bool { + match number { + 1 => self.edp1_push_button.is_auto(), + 2 => self.edp2_push_button.is_auto(), + _ => panic!("The A320 only supports two engines."), + } + } + + fn edp_push_button_is_off(&self, number: usize) -> bool { + match number { + 1 => self.edp1_push_button.is_off(), + 2 => self.edp2_push_button.is_off(), + _ => panic!("The A320 only supports two engines."), + } + } + + fn blue_epump_override_push_button_is_on(&self) -> bool { + self.blue_epump_override_push_button.is_on() + } + + fn blue_epump_push_button_is_off(&self) -> bool { + self.blue_epump_push_button.is_off() + } +} +impl SimulationElement for A320HydraulicOverheadPanel { + fn accept(&mut self, visitor: &mut T) { + self.edp1_push_button.accept(visitor); + self.edp2_push_button.accept(visitor); + self.blue_epump_push_button.accept(visitor); + self.ptu_push_button.accept(visitor); + self.rat_push_button.accept(visitor); + self.yellow_epump_push_button.accept(visitor); + self.blue_epump_override_push_button.accept(visitor); + + visitor.visit(self); + } +} + +pub(super) struct A320EngineFireOverheadPanel { + eng1_fire_pb: FirePushButton, + eng2_fire_pb: FirePushButton, +} +impl A320EngineFireOverheadPanel { + pub(super) fn new() -> Self { + Self { + eng1_fire_pb: FirePushButton::new("ENG1"), + eng2_fire_pb: FirePushButton::new("ENG2"), + } + } + + fn fire_push_button_is_released(&self, engine_number: usize) -> bool { + match engine_number { + 1 => self.eng1_fire_pb.is_released(), + 2 => self.eng2_fire_pb.is_released(), + _ => panic!("The A320 only supports two engines."), + } + } +} +impl SimulationElement for A320EngineFireOverheadPanel { + fn accept(&mut self, visitor: &mut T) { + self.eng1_fire_pb.accept(visitor); + self.eng2_fire_pb.accept(visitor); + + visitor.visit(self); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::Rng; + + mod a320_hydraulics { + use super::*; + use systems::engine::leap_engine::LeapEngine; + use systems::simulation::{test::SimulationTestBed, Aircraft}; + use uom::si::{ + acceleration::foot_per_second_squared, length::foot, ratio::percent, + thermodynamic_temperature::degree_celsius, velocity::knot, + }; + + struct A320HydraulicsTestAircraft { + engine_1: LeapEngine, + engine_2: LeapEngine, + hydraulics: A320Hydraulic, + overhead: A320HydraulicOverheadPanel, + engine_fire_overhead: A320EngineFireOverheadPanel, + landing_gear: LandingGear, + } + impl A320HydraulicsTestAircraft { + fn new() -> Self { + Self { + engine_1: LeapEngine::new(1), + engine_2: LeapEngine::new(2), + hydraulics: A320Hydraulic::new(), + overhead: A320HydraulicOverheadPanel::new(), + engine_fire_overhead: A320EngineFireOverheadPanel::new(), + landing_gear: LandingGear::new(), + } + } + + fn is_green_edp_commanded_on(&self) -> bool { + self.hydraulics + .engine_driven_pump_1_controller + .should_pressurise() + } + + fn is_yellow_edp_commanded_on(&self) -> bool { + self.hydraulics + .engine_driven_pump_2_controller + .should_pressurise() + } + + fn get_yellow_brake_accumulator_fluid_volume(&self) -> Volume { + self.hydraulics + .braking_circuit_altn + .accumulator_fluid_volume() + } + + fn is_nws_pin_inserted(&self) -> bool { + self.hydraulics.nose_wheel_steering_pin_is_inserted() + } + + fn is_cargo_powering_yellow_epump(&self) -> bool { + self.hydraulics + .should_pressurise_yellow_pump_for_cargo_door_operation() + } + + fn is_ptu_enabled(&self) -> bool { + self.hydraulics.power_transfer_unit.is_enabled() + } + + fn is_blue_pressurised(&self) -> bool { + self.hydraulics.is_blue_pressurised() + } + + fn is_green_pressurised(&self) -> bool { + self.hydraulics.is_green_pressurised() + } + + fn is_yellow_pressurised(&self) -> bool { + self.hydraulics.is_yellow_pressurised() + } + } + + impl Aircraft for A320HydraulicsTestAircraft { + fn update_after_power_distribution(&mut self, context: &UpdateContext) { + self.hydraulics.update( + context, + &self.engine_1, + &self.engine_2, + &self.overhead, + &self.engine_fire_overhead, + &self.landing_gear, + ); + + self.overhead.update(&self.hydraulics); + } + } + impl SimulationElement for A320HydraulicsTestAircraft { + fn accept(&mut self, visitor: &mut T) { + self.engine_1.accept(visitor); + self.engine_2.accept(visitor); + self.hydraulics.accept(visitor); + self.overhead.accept(visitor); + self.engine_fire_overhead.accept(visitor); + self.landing_gear.accept(visitor); + + visitor.visit(self); + } + } + + struct A320HydraulicsTestBed { + aircraft: A320HydraulicsTestAircraft, + simulation_test_bed: SimulationTestBed, + } + impl A320HydraulicsTestBed { + fn new() -> Self { + let mut aircraft = A320HydraulicsTestAircraft::new(); + Self { + simulation_test_bed: SimulationTestBed::seeded_with(&mut aircraft), + aircraft, + } + } + + fn run_one_tick(self) -> Self { + self.run_waiting_for(Duration::from_millis( + A320Hydraulic::HYDRAULIC_SIM_TIME_STEP_MILLISECONDS, + )) + } + + fn run_waiting_for(mut self, delta: Duration) -> Self { + self.simulation_test_bed.set_delta(delta); + self.simulation_test_bed.run_aircraft(&mut self.aircraft); + self + } + + fn is_green_edp_commanded_on(&self) -> bool { + self.aircraft.is_green_edp_commanded_on() + } + + fn is_yellow_edp_commanded_on(&self) -> bool { + self.aircraft.is_yellow_edp_commanded_on() + } + + fn is_ptu_enabled(&self) -> bool { + self.aircraft.is_ptu_enabled() + } + + fn is_blue_pressurised(&self) -> bool { + self.aircraft.is_blue_pressurised() + } + + fn is_green_pressurised(&self) -> bool { + self.aircraft.is_green_pressurised() + } + + fn is_yellow_pressurised(&self) -> bool { + self.aircraft.is_yellow_pressurised() + } + + fn green_pressure(&mut self) -> Pressure { + Pressure::new::(self.simulation_test_bed.read_f64("HYD_GREEN_PRESSURE")) + } + + fn blue_pressure(&mut self) -> Pressure { + Pressure::new::(self.simulation_test_bed.read_f64("HYD_BLUE_PRESSURE")) + } + + fn yellow_pressure(&mut self) -> Pressure { + Pressure::new::(self.simulation_test_bed.read_f64("HYD_YELLOW_PRESSURE")) + } + + fn get_yellow_reservoir_volume(&mut self) -> Volume { + Volume::new::(self.simulation_test_bed.read_f64("HYD_YELLOW_RESERVOIR")) + } + + fn is_green_edp_press_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_GREEN_EDPUMP_LOW_PRESS") + } + + fn is_green_edp_press_low_fault(&mut self) -> bool { + self.simulation_test_bed + .read_bool("OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT") + } + + fn is_yellow_edp_press_low_fault(&mut self) -> bool { + self.simulation_test_bed + .read_bool("OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT") + } + + fn is_yellow_edp_press_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_YELLOW_EDPUMP_LOW_PRESS") + } + + fn is_yellow_epump_press_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_YELLOW_EPUMP_LOW_PRESS") + } + + fn is_blue_epump_press_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_BLUE_EPUMP_LOW_PRESS") + } + + fn is_blue_epump_press_low_fault(&mut self) -> bool { + self.simulation_test_bed + .read_bool("OVHD_HYD_EPUMPB_PB_HAS_FAULT") + } + + fn get_brake_left_yellow_pressure(&mut self) -> Pressure { + Pressure::new::( + self.simulation_test_bed + .read_f64("HYD_BRAKE_ALTN_LEFT_PRESS"), + ) + } + + fn get_brake_right_yellow_pressure(&mut self) -> Pressure { + Pressure::new::( + self.simulation_test_bed + .read_f64("HYD_BRAKE_ALTN_RIGHT_PRESS"), + ) + } + + fn get_green_reservoir_volume(&mut self) -> Volume { + Volume::new::(self.simulation_test_bed.read_f64("HYD_GREEN_RESERVOIR")) + } + + fn get_blue_reservoir_volume(&mut self) -> Volume { + Volume::new::(self.simulation_test_bed.read_f64("HYD_BLUE_RESERVOIR")) + } + + fn get_brake_left_green_pressure(&mut self) -> Pressure { + Pressure::new::( + self.simulation_test_bed + .read_f64("HYD_BRAKE_NORM_LEFT_PRESS"), + ) + } + + fn get_brake_right_green_pressure(&mut self) -> Pressure { + Pressure::new::( + self.simulation_test_bed + .read_f64("HYD_BRAKE_NORM_RIGHT_PRESS"), + ) + } + + fn get_brake_yellow_accumulator_pressure(&mut self) -> Pressure { + Pressure::new::( + self.simulation_test_bed + .read_f64("HYD_BRAKE_ALTN_ACC_PRESS"), + ) + } + + fn get_brake_yellow_accumulator_fluid_volume(&self) -> Volume { + self.aircraft.get_yellow_brake_accumulator_fluid_volume() + } + + fn get_rat_position(&mut self) -> f64 { + self.simulation_test_bed.read_f64("HYD_RAT_STOW_POSITION") + } + + fn get_rat_rpm(&mut self) -> f64 { + self.simulation_test_bed.read_f64("A32NX_HYD_RAT_RPM") + } + + fn is_fire_valve_eng1_closed(&mut self) -> bool { + !self + .simulation_test_bed + .read_bool("HYD_GREEN_FIRE_VALVE_OPENED") + && !self + .aircraft + .hydraulics + .green_loop + .is_fire_shutoff_valve_opened() + } + + fn is_fire_valve_eng2_closed(&mut self) -> bool { + !self + .simulation_test_bed + .read_bool("HYD_YELLOW_FIRE_VALVE_OPENED") + && !self + .aircraft + .hydraulics + .yellow_loop + .is_fire_shutoff_valve_opened() + } + + fn engines_off(self) -> Self { + self.stop_eng1().stop_eng2() + } + + fn on_the_ground(mut self) -> Self { + self.simulation_test_bed + .set_indicated_altitude(Length::new::(0.)); + self.simulation_test_bed.set_on_ground(true); + self.simulation_test_bed + .set_indicated_airspeed(Velocity::new::(5.)); + self + } + + fn in_flight(mut self) -> Self { + self.simulation_test_bed.set_on_ground(false); + self.simulation_test_bed + .set_indicated_altitude(Length::new::(2500.)); + self.simulation_test_bed + .set_indicated_airspeed(Velocity::new::(180.)); + self.start_eng1(Ratio::new::(80.)) + .start_eng2(Ratio::new::(80.)) + .set_gear_up() + .set_park_brake(false) + } + + fn set_gear_compressed_switch(mut self, is_compressed: bool) -> Self { + self.simulation_test_bed.set_on_ground(is_compressed); + self + } + + fn set_eng1_fire_button(mut self, is_active: bool) -> Self { + self.simulation_test_bed + .write_bool("FIRE_BUTTON_ENG1", is_active); + self + } + + fn set_eng2_fire_button(mut self, is_active: bool) -> Self { + self.simulation_test_bed + .write_bool("FIRE_BUTTON_ENG2", is_active); + self + } + + fn set_cargo_door_state(mut self, position: f64) -> Self { + self.simulation_test_bed.write_f64("EXIT OPEN:5", position); + self + } + + fn set_pushback_state(mut self, is_pushed_back: bool) -> Self { + if is_pushed_back { + let mut rng = rand::thread_rng(); + + self.simulation_test_bed + .write_f64("PUSHBACK ANGLE", rng.gen_range(0.0..0.1)); + self.simulation_test_bed.write_f64("PUSHBACK STATE", 0.); + } else { + self.simulation_test_bed.write_f64("PUSHBACK STATE", 3.); + } + self + } + + fn start_eng1(mut self, n2: Ratio) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:1", true); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:1", n2.get::()); + + self + } + + fn start_eng2(mut self, n2: Ratio) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:2", true); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:2", n2.get::()); + + self + } + + fn stop_eng1(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:1", false); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:1", 0.); + + self + } + + fn stopping_eng1(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:1", false); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:1", 25.); + + self + } + + fn stop_eng2(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:2", false); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:2", 0.); + + self + } + + fn stopping_eng2(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:2", false); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:2", 25.); + + self + } + + fn set_park_brake(mut self, is_set: bool) -> Self { + self.simulation_test_bed + .write_bool("BRAKE PARKING INDICATOR", is_set); + self + } + + fn set_gear_up(mut self) -> Self { + self.simulation_test_bed + .write_f64("GEAR CENTER POSITION", 0.); + self.simulation_test_bed + .write_bool("GEAR HANDLE POSITION", false); + + self + } + + fn set_gear_down(mut self) -> Self { + self.simulation_test_bed + .write_f64("GEAR CENTER POSITION", 100.); + self.simulation_test_bed + .write_bool("GEAR HANDLE POSITION", true); + + self + } + + fn set_anti_skid(mut self, is_set: bool) -> Self { + self.simulation_test_bed + .write_bool("ANTISKID BRAKES ACTIVE", is_set); + self + } + + fn set_yellow_e_pump(mut self, is_auto: bool) -> Self { + self.simulation_test_bed + .write_bool("OVHD_HYD_EPUMPY_PB_IS_AUTO", is_auto); + self + } + + fn set_blue_e_pump(mut self, is_auto: bool) -> Self { + self.simulation_test_bed + .write_bool("OVHD_HYD_EPUMPB_PB_IS_AUTO", is_auto); + self + } + + fn set_blue_e_pump_ovrd(mut self, is_on: bool) -> Self { + self.simulation_test_bed + .write_bool("OVHD_HYD_EPUMPY_OVRD_PB_IS_ON", is_on); + self + } + + fn set_green_ed_pump(mut self, is_auto: bool) -> Self { + self.simulation_test_bed + .write_bool("OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO", is_auto); + self + } + + fn set_yellow_ed_pump(mut self, is_auto: bool) -> Self { + self.simulation_test_bed + .write_bool("OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO", is_auto); + self + } + + fn set_ptu_state(mut self, is_auto: bool) -> Self { + self.simulation_test_bed + .write_bool("OVHD_HYD_PTU_PB_IS_AUTO", is_auto); + self + } + + fn set_cold_dark_inputs(self) -> Self { + self.set_blue_e_pump_ovrd(false) + .set_eng1_fire_button(false) + .set_eng2_fire_button(false) + .set_blue_e_pump(true) + .set_yellow_e_pump(true) + .set_green_ed_pump(true) + .set_yellow_ed_pump(true) + .set_ptu_state(true) + .set_park_brake(true) + .set_anti_skid(true) + .set_cargo_door_state(0.) + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .set_gear_down() + } + + fn set_left_brake(mut self, position_percent: Ratio) -> Self { + self.simulation_test_bed + .write_f64("BRAKE LEFT POSITION", position_percent.get::()); + self + } + + fn set_right_brake(mut self, position_percent: Ratio) -> Self { + self.simulation_test_bed + .write_f64("BRAKE RIGHT POSITION", position_percent.get::()); + self + } + + fn empty_brake_accumulator_using_park_brake(mut self) -> Self { + self = self + .set_park_brake(true) + .run_waiting_for(Duration::from_secs(1)); + + let mut number_of_loops = 0; + while self + .get_brake_yellow_accumulator_fluid_volume() + .get::() + > 0.001 + { + self = self + .set_park_brake(false) + .run_waiting_for(Duration::from_secs(1)) + .set_park_brake(true) + .run_waiting_for(Duration::from_secs(1)); + number_of_loops += 1; + assert!(number_of_loops < 20); + } + + self = self + .set_park_brake(false) + .run_waiting_for(Duration::from_secs(1)) + .set_park_brake(true) + .run_waiting_for(Duration::from_secs(1)); + + self + } + + fn empty_brake_accumulator_using_pedal_brake(mut self) -> Self { + let mut number_of_loops = 0; + while self + .get_brake_yellow_accumulator_fluid_volume() + .get::() + > 0.001 + { + self = self + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)) + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(1)); + number_of_loops += 1; + assert!(number_of_loops < 50); + } + + self = self + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)) + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(1)); + + self + } + } + + fn test_bed() -> A320HydraulicsTestBed { + A320HydraulicsTestBed::new() + } + + fn test_bed_with() -> A320HydraulicsTestBed { + test_bed() + } + + #[test] + fn pressure_state_at_init_one_simulation_step() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + assert!(test_bed.is_ptu_enabled()); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn pressure_state_after_5s() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_waiting_for(Duration::from_secs(5)); + + assert!(test_bed.is_ptu_enabled()); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn ptu_inhibits() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Enabled on cold start + assert!(test_bed.is_ptu_enabled()); + + // Ptu push button disables PTU accordingly + test_bed = test_bed.set_ptu_state(false).run_one_tick(); + assert!(!test_bed.is_ptu_enabled()); + test_bed = test_bed.set_ptu_state(true).run_one_tick(); + assert!(test_bed.is_ptu_enabled()); + + // Not all engines on or off should disable ptu if on ground and park brake on + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_one_tick(); + assert!(!test_bed.is_ptu_enabled()); + test_bed = test_bed.set_park_brake(false).run_one_tick(); + assert!(test_bed.is_ptu_enabled()); + test_bed = test_bed.set_park_brake(true).run_one_tick(); + test_bed = test_bed.set_gear_compressed_switch(true).run_one_tick(); + assert!(!test_bed.is_ptu_enabled()); + test_bed = test_bed.set_gear_compressed_switch(false).run_one_tick(); + assert!(test_bed.is_ptu_enabled()); + } + + #[test] + fn ptu_cargo_operation_inhibit() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Enabled on cold start + assert!(test_bed.is_ptu_enabled()); + + // Ptu push button disables PTU accordingly + test_bed = test_bed.set_cargo_door_state(1.).run_one_tick(); + assert!(!test_bed.is_ptu_enabled()); + test_bed = test_bed.run_waiting_for(Duration::from_secs(1)); + assert!(!test_bed.is_ptu_enabled()); + test_bed = test_bed.run_waiting_for( + A320PowerTransferUnitController::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, + ); // Should re enabled after 40s + assert!(test_bed.is_ptu_enabled()); + } + + #[test] + fn nose_wheel_pin_detection() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + assert!(!test_bed.aircraft.is_nws_pin_inserted()); + + test_bed = test_bed.set_pushback_state(true).run_one_tick(); + assert!(test_bed.aircraft.is_nws_pin_inserted()); + + test_bed = test_bed + .set_pushback_state(false) + .run_waiting_for(Duration::from_secs(1)); + assert!(test_bed.aircraft.is_nws_pin_inserted()); + + test_bed = test_bed.set_pushback_state(false).run_waiting_for( + A320PowerTransferUnitController::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, + ); + + assert!(!test_bed.aircraft.is_nws_pin_inserted()); + } + + #[test] + fn cargo_door_yellow_epump_powering() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + assert!(!test_bed.aircraft.is_cargo_powering_yellow_epump()); + + test_bed = test_bed.set_cargo_door_state(1.0).run_one_tick(); + assert!(test_bed.aircraft.is_cargo_powering_yellow_epump()); + + test_bed = test_bed.run_waiting_for(Duration::from_secs(1)); + assert!(test_bed.aircraft.is_cargo_powering_yellow_epump()); + + test_bed = test_bed.run_waiting_for( + A320YellowElectricPumpController::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, + ); + + assert!(!test_bed.aircraft.is_cargo_powering_yellow_epump()); + } + + #[test] + fn ptu_pressurise_green_from_yellow_epump() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Enabled on cold start + assert!(test_bed.is_ptu_enabled()); + + // Yellow epump ON / Waiting 25s + test_bed = test_bed + .set_yellow_e_pump(false) + .run_waiting_for(Duration::from_secs(25)); + + assert!(test_bed.is_ptu_enabled()); + + // Now we should have pressure in yellow and green + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2000.)); + assert!(test_bed.green_pressure() < Pressure::new::(3100.)); + + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(test_bed.blue_pressure() > Pressure::new::(-50.)); + + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2000.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(3100.)); + + // Ptu push button disables PTU / green press should fall + test_bed = test_bed + .set_ptu_state(false) + .run_waiting_for(Duration::from_secs(20)); + assert!(!test_bed.is_ptu_enabled()); + + // Now we should have pressure in yellow only + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2000.)); + } + + #[test] + fn ptu_pressurise_green_from_yellow_epump_and_edp2() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .on_the_ground() + .start_eng2(Ratio::new::(100.)) + .set_park_brake(false) + .set_yellow_e_pump(false) + .set_yellow_ed_pump(true) // Else Ptu inhibited by parking brake + .run_waiting_for(Duration::from_secs(25)); + + assert!(test_bed.is_ptu_enabled()); + + // Now we should have pressure in yellow and green + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2000.)); + assert!(test_bed.green_pressure() < Pressure::new::(3100.)); + + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2000.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(3100.)); + } + + #[test] + fn green_edp_buildup() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Starting eng 1 + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_one_tick(); + + // ALMOST No pressure + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + + // Blue is auto run from engine master switches logic + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(500.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2900.)); + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + + // Stoping engine, pressure should fall in 20s + test_bed = test_bed + .stop_eng1() + .run_waiting_for(Duration::from_secs(20)); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(200.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn green_edp_no_fault_on_ground_eng_off() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_green_edp_commanded_on()); + // EDP should have no fault + assert!(!test_bed.is_green_edp_press_low_fault()); + } + + #[test] + fn green_edp_fault_not_on_ground_eng_off() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .in_flight() + .engines_off() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_green_edp_commanded_on()); + + assert!(!test_bed.is_green_pressurised()); + assert!(!test_bed.is_yellow_pressurised()); + // EDP should have a fault as we are in flight + assert!(test_bed.is_green_edp_press_low_fault()); + } + + #[test] + fn green_edp_fault_on_ground_eng_starting() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_green_edp_commanded_on()); + // EDP should have no fault + assert!(!test_bed.is_green_edp_press_low_fault()); + + test_bed = test_bed + .start_eng1(Ratio::new::(3.)) + .run_one_tick(); + + assert!(!test_bed.is_green_edp_press_low_fault()); + + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_one_tick(); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.is_green_edp_press_low_fault()); + + test_bed = test_bed.run_waiting_for(Duration::from_secs(10)); + + // When finally pressurised no fault + assert!(test_bed.is_green_pressurised()); + assert!(!test_bed.is_green_edp_press_low_fault()); + } + + #[test] + fn yellow_edp_no_fault_on_ground_eng_off() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_yellow_edp_commanded_on()); + // EDP should have no fault + assert!(!test_bed.is_yellow_edp_press_low_fault()); + } + + #[test] + fn yellow_edp_fault_not_on_ground_eng_off() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .in_flight() + .engines_off() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_yellow_edp_commanded_on()); + + assert!(!test_bed.is_green_pressurised()); + assert!(!test_bed.is_yellow_pressurised()); + // EDP should have a fault as we are in flight + assert!(test_bed.is_yellow_edp_press_low_fault()); + } + + #[test] + fn yellow_edp_fault_on_ground_eng_starting() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_yellow_edp_commanded_on()); + // EDP should have no fault + assert!(!test_bed.is_yellow_edp_press_low_fault()); + + test_bed = test_bed + .start_eng2(Ratio::new::(3.)) + .run_one_tick(); + + assert!(!test_bed.is_yellow_edp_press_low_fault()); + + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_one_tick(); + + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.is_yellow_edp_press_low_fault()); + + test_bed = test_bed.run_waiting_for(Duration::from_secs(10)); + + // When finally pressurised no fault + assert!(test_bed.is_yellow_pressurised()); + assert!(!test_bed.is_yellow_edp_press_low_fault()); + } + + #[test] + fn blue_epump_no_fault_on_ground_eng_starting() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Blue epump should have no fault + assert!(!test_bed.is_blue_epump_press_low_fault()); + + test_bed = test_bed + .start_eng2(Ratio::new::(3.)) + .run_one_tick(); + + assert!(!test_bed.is_blue_epump_press_low_fault()); + + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_one_tick(); + + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.is_blue_epump_press_low_fault()); + + test_bed = test_bed.run_waiting_for(Duration::from_secs(10)); + + // When finally pressurised no fault + assert!(test_bed.is_blue_pressurised()); + assert!(!test_bed.is_blue_epump_press_low_fault()); + } + + #[test] + fn blue_epump_fault_on_ground_using_override() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Blue epump should have no fault + assert!(!test_bed.is_blue_epump_press_low_fault()); + + test_bed = test_bed.set_blue_e_pump_ovrd(true).run_one_tick(); + + // As we use override, this bypasses eng off fault inhibit so we have a fault + assert!(test_bed.is_blue_epump_press_low_fault()); + + test_bed = test_bed.run_waiting_for(Duration::from_secs(10)); + + // When finally pressurised no fault + assert!(test_bed.is_blue_pressurised()); + assert!(!test_bed.is_blue_epump_press_low_fault()); + } + + #[test] + fn green_edp_press_low_engine_off_to_on() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_green_edp_commanded_on()); + + // EDP should be LOW pressure state + assert!(test_bed.is_green_edp_press_low()); + + // Starting eng 1 N2 is low at start + test_bed = test_bed + .start_eng1(Ratio::new::(3.)) + .run_one_tick(); + + // Engine commanded on but pressure couldn't rise enough: we are in fault low + assert!(test_bed.is_green_edp_press_low()); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + // No more fault LOW expected + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2900.)); + assert!(!test_bed.is_green_edp_press_low()); + + // Stoping pump, no fault expected + test_bed = test_bed + .set_green_ed_pump(false) + .run_waiting_for(Duration::from_secs(1)); + assert!(!test_bed.is_green_edp_press_low()); + } + + #[test] + fn green_edp_press_low_engine_on_to_off() { + let mut test_bed = test_bed_with() + .on_the_ground() + .set_cold_dark_inputs() + .start_eng1(Ratio::new::(75.)) + .run_waiting_for(Duration::from_secs(5)); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_green_edp_commanded_on()); + assert!(test_bed.is_green_pressurised()); + // EDP should not be in fault low when engine running and pressure is ok + assert!(!test_bed.is_green_edp_press_low()); + + // Stoping eng 1 with N2 still turning + test_bed = test_bed.stopping_eng1().run_one_tick(); + + // Edp should still be in pressurized mode but as engine just stopped no fault + assert!(test_bed.is_green_edp_commanded_on()); + assert!(!test_bed.is_green_edp_press_low()); + + // Waiting for 25s pressure should drop and still no fault + test_bed = test_bed + .stop_eng1() + .run_waiting_for(Duration::from_secs(25)); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(test_bed.is_green_edp_press_low()); + } + + #[test] + fn yellow_edp_press_low_engine_on_to_off() { + let mut test_bed = test_bed_with() + .on_the_ground() + .set_cold_dark_inputs() + .start_eng2(Ratio::new::(75.)) + .run_waiting_for(Duration::from_secs(5)); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_yellow_edp_commanded_on()); + assert!(test_bed.is_yellow_pressurised()); + // EDP should not be in fault low when engine running and pressure is ok + assert!(!test_bed.is_yellow_edp_press_low()); + + // Stoping eng 2 with N2 still turning + test_bed = test_bed.stopping_eng2().run_one_tick(); + + // Edp should still be in pressurized mode but as engine just stopped no fault + assert!(test_bed.is_yellow_edp_commanded_on()); + assert!(!test_bed.is_yellow_edp_press_low()); + + // Waiting for 25s pressure should drop and still no fault + test_bed = test_bed + .stop_eng2() + .run_waiting_for(Duration::from_secs(25)); + + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + assert!(test_bed.is_yellow_edp_press_low()); + } + + #[test] + fn yellow_edp_press_low_engine_off_to_on() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_yellow_edp_commanded_on()); + + // EDP should be LOW pressure state + assert!(test_bed.is_yellow_edp_press_low()); + + // Starting eng 2 N2 is low at start + test_bed = test_bed + .start_eng2(Ratio::new::(3.)) + .run_one_tick(); + + // Engine commanded on but pressure couldn't rise enough: we are in fault low + assert!(test_bed.is_yellow_edp_press_low()); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + // No more fault LOW expected + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); + assert!(!test_bed.is_yellow_edp_press_low()); + + // Stoping pump, no fault expected + test_bed = test_bed + .set_yellow_ed_pump(false) + .run_waiting_for(Duration::from_secs(1)); + assert!(!test_bed.is_yellow_edp_press_low()); + } + + #[test] + fn yellow_edp_press_low_engine_off_to_on_with_e_pump() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .set_ptu_state(false) + .set_yellow_e_pump(false) + .run_one_tick(); + + // EDP should be commanded on even without engine running + assert!(test_bed.is_yellow_edp_commanded_on()); + + // EDP should be LOW pressure state + assert!(test_bed.is_yellow_edp_press_low()); + + // Waiting for 20s pressure should be at 3000 psi + test_bed = test_bed.run_waiting_for(Duration::from_secs(20)); + + // Yellow pressurised but edp still off, we expect fault LOW press + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); + assert!(test_bed.is_yellow_edp_press_low()); + + // Starting eng 2 N2 is low at start + test_bed = test_bed + .start_eng2(Ratio::new::(3.)) + .run_one_tick(); + + // Engine commanded on but pressure couldn't rise enough: we are in fault low + assert!(test_bed.is_yellow_edp_press_low()); + + // Waiting for 5s pressure should be at 3000 psi in EDP section + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + // No more fault LOW expected + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); + assert!(!test_bed.is_yellow_edp_press_low()); + } + + #[test] + fn green_edp_press_low_engine_off_to_on_with_ptu() { + let mut test_bed = test_bed_with() + .on_the_ground() + .set_cold_dark_inputs() + .set_park_brake(false) + .start_eng2(Ratio::new::(80.)) + .run_one_tick(); + + // EDP should be LOW pressure state + assert!(test_bed.is_green_edp_press_low()); + + // Waiting for 20s pressure should be at 2300+ psi thanks to ptu + test_bed = test_bed.run_waiting_for(Duration::from_secs(20)); + + // Yellow pressurised by engine2, green presurised from ptu we expect fault LOW press on EDP1 + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2800.)); + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2300.)); + assert!(test_bed.is_green_edp_press_low()); + + // Starting eng 1 N2 is low at start + test_bed = test_bed + .start_eng1(Ratio::new::(3.)) + .run_one_tick(); + + // Engine commanded on but pressure couldn't rise enough: we are in fault low + assert!(test_bed.is_green_edp_press_low()); + + // Waiting for 5s pressure should be at 3000 psi in EDP section + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + // No more fault LOW expected + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2900.)); + assert!(!test_bed.is_green_edp_press_low()); + } + + #[test] + fn yellow_epump_press_low_at_pump_on() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should not be in fault low when cold start + assert!(!test_bed.is_yellow_epump_press_low()); + + // Starting epump + test_bed = test_bed.set_yellow_e_pump(false).run_one_tick(); + + // Pump commanded on but pressure couldn't rise enough: we are in fault low + assert!(test_bed.is_yellow_epump_press_low()); + + // Waiting for 20s pressure should be at 3000 psi + test_bed = test_bed.run_waiting_for(Duration::from_secs(20)); + + // No more fault LOW expected + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2500.)); + assert!(!test_bed.is_yellow_epump_press_low()); + + // Stoping epump, no fault expected + test_bed = test_bed + .set_yellow_e_pump(true) + .run_waiting_for(Duration::from_secs(1)); + assert!(!test_bed.is_yellow_epump_press_low()); + } + + #[test] + fn blue_epump_press_low_at_pump_on() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // EDP should not be in fault low when cold start + assert!(!test_bed.is_blue_epump_press_low()); + + // Starting epump + test_bed = test_bed.set_blue_e_pump_ovrd(true).run_one_tick(); + + // Pump commanded on but pressure couldn't rise enough: we are in fault low + assert!(test_bed.is_blue_epump_press_low()); + + // Waiting for 10s pressure should be at 3000 psi + test_bed = test_bed.run_waiting_for(Duration::from_secs(10)); + + // No more fault LOW expected + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() > Pressure::new::(2900.)); + assert!(!test_bed.is_blue_epump_press_low()); + + // Stoping epump, no fault expected + test_bed = test_bed + .set_blue_e_pump_ovrd(false) + .run_waiting_for(Duration::from_secs(1)); + assert!(!test_bed.is_blue_epump_press_low()); + } + + #[test] + fn edp_deactivation() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .set_ptu_state(false) + .run_one_tick(); + + // Starting eng 1 and eng 2 + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .start_eng2(Ratio::new::(80.)) + .run_one_tick(); + + // ALMOST No pressure + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); + + assert!(test_bed.green_pressure() > Pressure::new::(2900.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); + + // Stoping edp1, pressure should fall in 20s + test_bed = test_bed + .set_green_ed_pump(false) + .run_waiting_for(Duration::from_secs(20)); + + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); + + // Stoping edp2, pressure should fall in 20s + test_bed = test_bed + .set_yellow_ed_pump(false) + .run_waiting_for(Duration::from_secs(20)); + + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + } + + #[test] + fn yellow_edp_buildup() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Starting eng 1 + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_one_tick(); + // ALMOST No pressure + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(!test_bed.is_blue_pressurised()); + + // Blue is auto run + assert!(test_bed.blue_pressure() < Pressure::new::(500.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2800.)); + + // Stoping engine, pressure should fall in 20s + test_bed = test_bed + .stop_eng2() + .run_waiting_for(Duration::from_secs(20)); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(200.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + } + + #[test] + // Checks numerical stability of reservoir level: level should remain after multiple pressure cycles + fn yellow_loop_reservoir_coherency() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .set_ptu_state(false) + // Park brake off to not use fluid in brakes + .set_park_brake(false) + .run_one_tick(); + + // Starting epump wait for pressure rise to make sure system is primed including brake accumulator + test_bed = test_bed + .set_yellow_e_pump(false) + .run_waiting_for(Duration::from_secs(20)); + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2500.)); + + // Shutdown and wait for pressure stabilisation + test_bed = test_bed + .set_yellow_e_pump(true) + .run_waiting_for(Duration::from_secs(50)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(-50.)); + + let reservoir_level_after_priming = test_bed.get_yellow_reservoir_volume(); + + let total_fluid_res_plus_accumulator_before_loops = reservoir_level_after_priming + + test_bed.get_brake_yellow_accumulator_fluid_volume(); + + // Now doing cycles of pressurisation on EDP and ePump + for _ in 1..6 { + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(50)); + + assert!(test_bed.yellow_pressure() < Pressure::new::(3100.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2500.)); + + let mut current_res_level = test_bed.get_yellow_reservoir_volume(); + assert!(current_res_level < reservoir_level_after_priming); + + test_bed = test_bed + .stop_eng2() + .run_waiting_for(Duration::from_secs(50)); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(-50.)); + + test_bed = test_bed + .set_yellow_e_pump(false) + .run_waiting_for(Duration::from_secs(50)); + + assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2500.)); + + current_res_level = test_bed.get_yellow_reservoir_volume(); + assert!(current_res_level < reservoir_level_after_priming); + + test_bed = test_bed + .set_yellow_e_pump(true) + .run_waiting_for(Duration::from_secs(50)); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(-50.)); + } + let total_fluid_res_plus_accumulator_after_loops = test_bed + .get_yellow_reservoir_volume() + + test_bed.get_brake_yellow_accumulator_fluid_volume(); + + let total_fluid_difference = total_fluid_res_plus_accumulator_before_loops + - total_fluid_res_plus_accumulator_after_loops; + + // Make sure no more deviation than 0.001 gallon is lost after full pressure and unpressurized states + assert!(total_fluid_difference.get::().abs() < 0.001); + } + + #[test] + // Checks numerical stability of reservoir level: level should remain after multiple pressure cycles + fn green_loop_reservoir_coherency() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .set_ptu_state(false) + .run_one_tick(); + + // Starting EDP wait for pressure rise to make sure system is primed + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(20)); + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(3500.)); + assert!(test_bed.green_pressure() > Pressure::new::(2500.)); + + // Shutdown and wait for pressure stabilisation + test_bed = test_bed + .stop_eng1() + .run_waiting_for(Duration::from_secs(50)); + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(test_bed.green_pressure() > Pressure::new::(-50.)); + + let reservoir_level_after_priming = test_bed.get_green_reservoir_volume(); + + // Now doing cycles of pressurisation on EDP + for _ in 1..6 { + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(50)); + + assert!(test_bed.green_pressure() < Pressure::new::(3500.)); + assert!(test_bed.green_pressure() > Pressure::new::(2500.)); + + let current_res_level = test_bed.get_green_reservoir_volume(); + assert!(current_res_level < reservoir_level_after_priming); + + test_bed = test_bed + .stop_eng1() + .run_waiting_for(Duration::from_secs(50)); + assert!(test_bed.green_pressure() < Pressure::new::(50.)); + assert!(test_bed.green_pressure() > Pressure::new::(-50.)); + } + + let total_fluid_difference = + reservoir_level_after_priming - test_bed.get_green_reservoir_volume(); + + // Make sure no more deviation than 0.001 gallon is lost after full pressure and unpressurized states + assert!(total_fluid_difference.get::().abs() < 0.001); + } + + #[test] + // Checks numerical stability of reservoir level: level should remain after multiple pressure cycles + fn blue_loop_reservoir_coherency() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Starting blue_epump wait for pressure rise to make sure system is primed + test_bed = test_bed + .set_blue_e_pump_ovrd(true) + .run_waiting_for(Duration::from_secs(20)); + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(3500.)); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + + // Shutdown and wait for pressure stabilisation + test_bed = test_bed + .set_blue_e_pump_ovrd(false) + .run_waiting_for(Duration::from_secs(50)); + assert!(!test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(test_bed.blue_pressure() > Pressure::new::(-50.)); + + let reservoir_level_after_priming = test_bed.get_blue_reservoir_volume(); + + // Now doing cycles of pressurisation on epump relying on auto run of epump when eng is on + for _ in 1..6 { + test_bed = test_bed + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(50)); + + assert!(test_bed.blue_pressure() < Pressure::new::(3500.)); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + + let current_res_level = test_bed.get_blue_reservoir_volume(); + assert!(current_res_level < reservoir_level_after_priming); + + test_bed = test_bed + .stop_eng1() + .run_waiting_for(Duration::from_secs(50)); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(test_bed.blue_pressure() > Pressure::new::(-50.)); + + // Now engine 2 is used + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(50)); + + assert!(test_bed.blue_pressure() < Pressure::new::(3500.)); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + + let current_res_level = test_bed.get_blue_reservoir_volume(); + assert!(current_res_level < reservoir_level_after_priming); + + test_bed = test_bed + .stop_eng2() + .run_waiting_for(Duration::from_secs(50)); + assert!(test_bed.blue_pressure() < Pressure::new::(50.)); + assert!(test_bed.blue_pressure() > Pressure::new::(-50.)); + } + + let total_fluid_difference = + reservoir_level_after_priming - test_bed.get_blue_reservoir_volume(); + + // Make sure no more deviation than 0.001 gallon is lost after full pressure and unpressurized states + assert!(total_fluid_difference.get::().abs() < 0.001); + } + + #[test] + fn yellow_green_edp_firevalve() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // PTU would mess up the test + test_bed = test_bed.set_ptu_state(false).run_one_tick(); + assert!(!test_bed.is_ptu_enabled()); + + assert!(!test_bed.is_fire_valve_eng1_closed()); + assert!(!test_bed.is_fire_valve_eng2_closed()); + + // Starting eng 1 + test_bed = test_bed + .start_eng2(Ratio::new::(80.)) + .start_eng1(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(5)); + + // Waiting for 5s pressure should be at 3000 psi + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() > Pressure::new::(2900.)); + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2800.)); + + assert!(!test_bed.is_fire_valve_eng1_closed()); + assert!(!test_bed.is_fire_valve_eng2_closed()); + + // Green shutoff valve + test_bed = test_bed + .set_eng1_fire_button(true) + .run_waiting_for(Duration::from_secs(20)); + + assert!(test_bed.is_fire_valve_eng1_closed()); + assert!(!test_bed.is_fire_valve_eng2_closed()); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); + + // Yellow shutoff valve + test_bed = test_bed + .set_eng2_fire_button(true) + .run_waiting_for(Duration::from_secs(20)); + + assert!(test_bed.is_fire_valve_eng1_closed()); + assert!(test_bed.is_fire_valve_eng2_closed()); + + assert!(!test_bed.is_green_pressurised()); + assert!(test_bed.green_pressure() < Pressure::new::(500.)); + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.blue_pressure() > Pressure::new::(2500.)); + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + } + + #[test] + fn yellow_brake_accumulator() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Getting accumulator pressure on cold start + let mut accumulator_pressure = test_bed.get_brake_yellow_accumulator_pressure(); + + // No brakes on green, no more pressure than in accumulator on yellow + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!( + test_bed.get_brake_left_yellow_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + assert!( + test_bed.get_brake_right_yellow_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + + // No brakes even if we brake on green, no more than accumulator pressure on yellow + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(5)); + + accumulator_pressure = test_bed.get_brake_yellow_accumulator_pressure(); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!( + test_bed.get_brake_left_yellow_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + assert!( + test_bed.get_brake_right_yellow_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + assert!( + test_bed.get_brake_yellow_accumulator_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + + // Park brake off, loading accumulator, we expect no brake pressure but accumulator loaded + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .set_park_brake(false) + .set_yellow_e_pump(false) + .run_waiting_for(Duration::from_secs(30)); + + assert!(test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() > Pressure::new::(2500.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + assert!(test_bed.get_brake_yellow_accumulator_pressure() > Pressure::new::(2500.)); + + // Park brake on, loaded accumulator, we expect brakes on yellow side only + test_bed = test_bed + .set_park_brake(true) + .run_waiting_for(Duration::from_secs(3)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() > Pressure::new::(2000.)); + assert!(test_bed.get_brake_right_yellow_pressure() > Pressure::new::(2000.)); + + assert!(test_bed.get_brake_yellow_accumulator_pressure() > Pressure::new::(2500.)); + } + + #[test] + fn norm_brake_vs_altn_brake() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Getting accumulator pressure on cold start + let accumulator_pressure = test_bed.get_brake_yellow_accumulator_pressure(); + + // No brakes + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!( + test_bed.get_brake_left_yellow_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + assert!( + test_bed.get_brake_right_yellow_pressure() + < accumulator_pressure + Pressure::new::(50.) + ); + + test_bed = test_bed + .start_eng1(Ratio::new::(100.)) + .start_eng2(Ratio::new::(100.)) + .set_park_brake(false) + .run_waiting_for(Duration::from_secs(5)); + + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.is_yellow_pressurised()); + // No brakes if we don't brake + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Braking cause green braking system to rise + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(2000.)); + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(3500.)); + assert!(test_bed.get_brake_right_green_pressure() > Pressure::new::(2000.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(3500.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Disabling Askid causes alternate braking to work and release green brakes + test_bed = test_bed + .set_anti_skid(false) + .run_waiting_for(Duration::from_secs(2)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() > Pressure::new::(950.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(3500.)); + assert!(test_bed.get_brake_right_yellow_pressure() > Pressure::new::(950.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(3500.)); + } + + #[test] + fn no_brake_inversion() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + test_bed = test_bed + .start_eng1(Ratio::new::(100.)) + .start_eng2(Ratio::new::(100.)) + .set_park_brake(false) + .run_waiting_for(Duration::from_secs(5)); + + assert!(test_bed.is_green_pressurised()); + assert!(test_bed.is_yellow_pressurised()); + // Braking left + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(2000.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Braking right + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() > Pressure::new::(2000.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Disabling Askid causes alternate braking to work and release green brakes + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(100.)) + .set_anti_skid(false) + .run_waiting_for(Duration::from_secs(2)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() > Pressure::new::(950.)); + + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(2)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() > Pressure::new::(950.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn auto_brake_at_gear_retraction() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + test_bed = test_bed + .start_eng1(Ratio::new::(100.)) + .start_eng2(Ratio::new::(100.)) + .set_park_brake(false) + .run_waiting_for(Duration::from_secs(15)); + + // No brake inputs + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Positive climb, gear up + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .in_flight() + .set_gear_up() + .run_waiting_for(Duration::from_secs(1)); + + // Check auto brake is active + assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() > Pressure::new::(50.)); + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(1500.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(1500.)); + + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Check no more autobrakes after 3s + test_bed = test_bed.run_waiting_for(Duration::from_secs(3)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn alternate_brake_accumulator_is_emptying_while_braking() { + let mut test_bed = test_bed_with() + .on_the_ground() + .set_cold_dark_inputs() + .start_eng1(Ratio::new::(100.)) + .start_eng2(Ratio::new::(100.)) + .set_park_brake(false) + .run_waiting_for(Duration::from_secs(15)); + + // Check we got yellow pressure and brake accumulator loaded + assert!(test_bed.yellow_pressure() >= Pressure::new::(2500.)); + assert!( + test_bed.get_brake_yellow_accumulator_pressure() >= Pressure::new::(2500.) + ); + + // Disabling green and yellow side so accumulator stop being able to reload + test_bed = test_bed + .set_ptu_state(false) + .set_yellow_ed_pump(false) + .set_green_ed_pump(false) + .set_yellow_e_pump(true) + .run_waiting_for(Duration::from_secs(30)); + + assert!(test_bed.yellow_pressure() <= Pressure::new::(100.)); + assert!(test_bed.green_pressure() <= Pressure::new::(100.)); + assert!( + test_bed.get_brake_yellow_accumulator_pressure() >= Pressure::new::(2500.) + ); + + // Now using brakes and check accumulator gets empty + test_bed = test_bed + .empty_brake_accumulator_using_pedal_brake() + .run_waiting_for(Duration::from_secs(1)); + + assert!( + test_bed.get_brake_yellow_accumulator_pressure() <= Pressure::new::(1000.) + ); + assert!( + test_bed.get_brake_yellow_accumulator_fluid_volume() <= Volume::new::(0.01) + ); + } + + #[test] + fn brakes_inactive_in_flight() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .in_flight() + .set_gear_up() + .run_waiting_for(Duration::from_secs(10)); + + // No brake inputs + test_bed = test_bed + .set_left_brake(Ratio::new::(0.)) + .set_right_brake(Ratio::new::(0.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + // Now full brakes + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)); + + // Check no action on brakes + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn brakes_norm_active_in_flight_gear_down() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .in_flight() + .set_gear_up() + .run_waiting_for(Duration::from_secs(10)); + + // Now full brakes gear down + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .set_gear_down() + .run_waiting_for(Duration::from_secs(1)); + + // Brakes norm should work normally + assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() > Pressure::new::(50.)); + + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn brakes_alternate_active_in_flight_gear_down() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .in_flight() + .set_gear_up() + .run_waiting_for(Duration::from_secs(10)); + + // Now full brakes gear down + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .set_gear_down() + .set_anti_skid(false) + .run_waiting_for(Duration::from_secs(1)); + + // Brakes norm should work normally + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + + assert!(test_bed.get_brake_left_yellow_pressure() > Pressure::new::(900.)); + assert!(test_bed.get_brake_right_yellow_pressure() > Pressure::new::(900.)); + } + + #[test] + // Testing that green for brakes is only available if park brake is on while altn pressure is at too low level + fn brake_logic_green_backup_emergency() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + // Setting on ground with yellow side hydraulics off + // This should prevent yellow accumulator to fill + test_bed = test_bed + .start_eng1(Ratio::new::(100.)) + .start_eng2(Ratio::new::(100.)) + .set_park_brake(true) + .set_ptu_state(false) + .set_yellow_e_pump(true) + .set_yellow_ed_pump(false) + .run_waiting_for(Duration::from_secs(15)); + + // Braking but park is on: no output on green brakes expected + test_bed = test_bed + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_left_yellow_pressure() > Pressure::new::(500.)); + assert!(test_bed.get_brake_right_yellow_pressure() > Pressure::new::(500.)); + + // With no more fluid in yellow accumulator, green should work as emergency + test_bed = test_bed + .empty_brake_accumulator_using_park_brake() + .set_left_brake(Ratio::new::(100.)) + .set_right_brake(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(1000.)); + assert!(test_bed.get_brake_right_green_pressure() > Pressure::new::(1000.)); + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + } + + #[test] + fn controller_blue_epump_activates_when_no_weight_on_wheels() { + let engine_off_oil_pressure = Pressure::new::(10.); + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + overhead_panel.blue_epump_override_push_button.push_off(); + + let mut blue_epump_controller = A320BlueElectricPumpController::new(); + + let eng1_above_idle = false; + let eng2_above_idle = false; + blue_epump_controller.weight_on_wheels = false; + + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(blue_epump_controller.should_pressurise()); + + blue_epump_controller.weight_on_wheels = true; + + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(!blue_epump_controller.should_pressurise()); + } + + #[test] + fn controller_blue_epump_split_engine_states() { + let engine_on_oil_pressure = Pressure::new::(30.); + let engine_off_oil_pressure = Pressure::new::(10.); + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + overhead_panel.blue_epump_override_push_button.push_off(); + + let mut blue_epump_controller = A320BlueElectricPumpController::new(); + + let eng1_above_idle = false; + let eng2_above_idle = false; + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(!blue_epump_controller.should_pressurise()); + + let eng1_above_idle = true; + let eng2_above_idle = false; + blue_epump_controller.update( + &overhead_panel, + true, + engine_on_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(blue_epump_controller.should_pressurise()); + + let eng1_above_idle = false; + let eng2_above_idle = true; + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_on_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(blue_epump_controller.should_pressurise()); + } + + #[test] + fn controller_blue_epump_on_off_engines() { + let engine_on_oil_pressure = Pressure::new::(30.); + let engine_off_oil_pressure = Pressure::new::(10.); + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + overhead_panel.blue_epump_override_push_button.push_off(); + + let mut blue_epump_controller = A320BlueElectricPumpController::new(); + + let eng1_above_idle = true; + let eng2_above_idle = true; + blue_epump_controller.update( + &overhead_panel, + true, + engine_on_oil_pressure, + engine_on_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(blue_epump_controller.should_pressurise()); + + let eng1_above_idle = false; + let eng2_above_idle = false; + blue_epump_controller.update( + &overhead_panel, + false, + engine_off_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(!blue_epump_controller.should_pressurise()); + } + + #[test] + fn controller_blue_epump_override() { + let engine_off_oil_pressure = Pressure::new::(10.); + + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + let mut blue_epump_controller = A320BlueElectricPumpController::new(); + + let eng1_above_idle = false; + let eng2_above_idle = false; + overhead_panel.blue_epump_override_push_button.push_on(); + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(blue_epump_controller.should_pressurise()); + + let eng1_above_idle = false; + let eng2_above_idle = false; + overhead_panel.blue_epump_override_push_button.push_off(); + blue_epump_controller.update( + &overhead_panel, + false, + engine_off_oil_pressure, + engine_off_oil_pressure, + eng1_above_idle, + eng2_above_idle, + ); + assert!(!blue_epump_controller.should_pressurise()); + } + + #[test] + fn controller_yellow_epump_overhead_button_logic() { + let fwd_door = Door::new(1); + let aft_door = Door::new(2); + let context = context(Duration::from_millis(100)); + + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + + let mut yellow_epump_controller = A320YellowElectricPumpController::new(); + + overhead_panel.yellow_epump_push_button.push_auto(); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, true); + assert!(!yellow_epump_controller.should_pressurise()); + + overhead_panel.yellow_epump_push_button.push_on(); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, true); + assert!(yellow_epump_controller.should_pressurise()); + + overhead_panel.yellow_epump_push_button.push_auto(); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, true); + assert!(!yellow_epump_controller.should_pressurise()); + } + + #[test] + fn controller_yellow_epump_cargo_doors_starts_pump_for_timeout_delay() { + let context = context(Duration::from_millis(100)); + + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + + let mut yellow_epump_controller = A320YellowElectricPumpController::new(); + + overhead_panel.yellow_epump_push_button.push_auto(); + assert!(!yellow_epump_controller.should_pressurise()); + + let aft_door = non_moving_door(2); + let fwd_door = moving_door(1); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, true); + assert!(yellow_epump_controller.should_pressurise()); + let fwd_door = non_moving_door(1); + + yellow_epump_controller.update(&context.with_delta(Duration::from_secs(1) + A320YellowElectricPumpController::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION), &overhead_panel,&fwd_door,&aft_door, true); + assert!(!yellow_epump_controller.should_pressurise()); + + let aft_door = moving_door(2); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, true); + assert!(yellow_epump_controller.should_pressurise()); + let aft_door = non_moving_door(2); + + yellow_epump_controller.update(&context.with_delta(Duration::from_secs(1) + A320YellowElectricPumpController::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION), &overhead_panel,&fwd_door,&aft_door, true); + assert!(!yellow_epump_controller.should_pressurise()); + } + + #[test] + fn controller_engine_driven_pump1_overhead_button_logic_with_eng_on() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + let fire_overhead_panel = A320EngineFireOverheadPanel::new(); + overhead_panel.edp1_push_button.push_auto(); + + let mut edp1_controller = A320EngineDrivenPumpController::new(1); + edp1_controller.engine_master_on = true; + + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(edp1_controller.should_pressurise()); + + overhead_panel.edp1_push_button.push_off(); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(!edp1_controller.should_pressurise()); + + overhead_panel.edp1_push_button.push_auto(); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(edp1_controller.should_pressurise()); + } + + #[test] + fn controller_engine_driven_pump1_fire_overhead_released_stops_pump() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + let mut fire_overhead_panel = A320EngineFireOverheadPanel::new(); + overhead_panel.edp1_push_button.push_auto(); + fire_overhead_panel.eng1_fire_pb.set(false); + + let mut edp1_controller = A320EngineDrivenPumpController::new(1); + edp1_controller.engine_master_on = true; + + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(edp1_controller.should_pressurise()); + + fire_overhead_panel.eng1_fire_pb.set(true); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(10.), + true, + ); + assert!(!edp1_controller.should_pressurise()); + } + + #[test] + fn controller_engine_driven_pump2_overhead_button_logic_with_eng_on() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + let fire_overhead_panel = A320EngineFireOverheadPanel::new(); + overhead_panel.edp2_push_button.push_auto(); + + let mut edp2_controller = A320EngineDrivenPumpController::new(2); + edp2_controller.engine_master_on = true; + + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(edp2_controller.should_pressurise()); + + overhead_panel.edp2_push_button.push_off(); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(!edp2_controller.should_pressurise()); + + overhead_panel.edp2_push_button.push_auto(); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(edp2_controller.should_pressurise()); + } + + #[test] + fn controller_engine_driven_pump2_fire_overhead_released_stops_pump() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + let mut fire_overhead_panel = A320EngineFireOverheadPanel::new(); + overhead_panel.edp2_push_button.push_auto(); + fire_overhead_panel.eng2_fire_pb.set(false); + + let mut edp2_controller = A320EngineDrivenPumpController::new(2); + edp2_controller.engine_master_on = true; + + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(30.), + true, + ); + assert!(edp2_controller.should_pressurise()); + + fire_overhead_panel.eng2_fire_pb.set(true); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + Pressure::new::(5.), + true, + ); + assert!(!edp2_controller.should_pressurise()); + } + + #[test] + fn controller_ptu_on_off_cargo_door() { + let tug = PushbackTug::new(); + let context = context(Duration::from_millis(100)); + + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + + let mut ptu_controller = A320PowerTransferUnitController::new(); + + overhead_panel.ptu_push_button.push_auto(); + + ptu_controller.update( + &context, + &overhead_panel, + &non_moving_door(1), + &non_moving_door(2), + &tug, + ); + assert!(ptu_controller.should_enable()); + + overhead_panel.ptu_push_button.push_off(); + + ptu_controller.update( + &context, + &overhead_panel, + &non_moving_door(1), + &non_moving_door(2), + &tug, + ); + assert!(!ptu_controller.should_enable()); + + overhead_panel.ptu_push_button.push_auto(); + + ptu_controller.update( + &context, + &overhead_panel, + &moving_door(1), + &non_moving_door(2), + &tug, + ); + assert!(!ptu_controller.should_enable()); + + ptu_controller.update(&context.with_delta(Duration::from_secs(1) + A320PowerTransferUnitController::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION), &overhead_panel, &non_moving_door(1),&non_moving_door(2),&tug); + assert!(ptu_controller.should_enable()); + } + + #[test] + fn controller_ptu_tug() { + let fwd_door = Door::new(1); + let aft_door = Door::new(2); + let mut tug = PushbackTug::new(); + let context = context(Duration::from_millis(100)); + + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + + let mut ptu_controller = A320PowerTransferUnitController::new(); + overhead_panel.ptu_push_button.push_auto(); + + ptu_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, &tug); + assert!(ptu_controller.should_enable()); + + ptu_controller.weight_on_wheels = true; + ptu_controller.eng_1_master_on = true; + ptu_controller.eng_2_master_on = false; + ptu_controller.parking_brake_lever_pos = false; + + tug = detached_tug(); + ptu_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, &tug); + assert!(ptu_controller.should_enable()); + + tug = attached_tug(); + ptu_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, &tug); + assert!(!ptu_controller.should_enable()); + + tug = detached_tug(); + ptu_controller.update(&context.with_delta(Duration::from_secs(1) + A320PowerTransferUnitController::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK), &overhead_panel, &fwd_door, &aft_door,&tug); + assert!(ptu_controller.should_enable()); + } + + #[test] + fn rat_does_not_deploy_on_ground_at_eng_off() { + let mut test_bed = test_bed_with() + .set_cold_dark_inputs() + .on_the_ground() + .start_eng1(Ratio::new::(80.)) + .start_eng2(Ratio::new::(80.)) + .run_waiting_for(Duration::from_secs(10)); + + assert!(test_bed.is_blue_pressurised()); + assert!(test_bed.get_rat_position() <= 0.); + assert!(test_bed.get_rat_rpm() <= 1.); + + // Stopping both engines + test_bed = test_bed + .stop_eng1() + .stop_eng2() + .run_waiting_for(Duration::from_secs(2)); + + // RAT has not deployed + assert!(test_bed.get_rat_position() <= 0.); + assert!(test_bed.get_rat_rpm() <= 1.); + } + + fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + ) + } + + fn moving_door(id: usize) -> Door { + let mut door = Door::new(id); + door.position += 0.01; + door + } + + fn non_moving_door(id: usize) -> Door { + let mut door = Door::new(id); + door.previous_position = door.position; + door + } + + fn attached_tug() -> PushbackTug { + let mut tug = PushbackTug::new(); + tug.angle = tug.previous_angle + 0.1; + tug.state = 0.; + tug.update(); + tug + } + + fn detached_tug() -> PushbackTug { + let mut tug = PushbackTug::new(); + tug.angle = tug.previous_angle; + tug.state = 3.; + tug.update(); + tug + } + } } -impl SimulationElement for A320Hydraulic {} diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index ff9af5c5dc6..3b806532e20 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -5,11 +5,14 @@ mod pneumatic; mod power_consumption; use self::{fuel::A320Fuel, pneumatic::A320PneumaticOverheadPanel}; + use electrical::{ A320Electrical, A320ElectricalOverheadPanel, A320ElectricalUpdateArguments, A320EmergencyElectricalOverheadPanel, }; -use hydraulic::A320Hydraulic; + +use hydraulic::{A320EngineFireOverheadPanel, A320Hydraulic, A320HydraulicOverheadPanel}; + use power_consumption::A320PowerConsumption; use systems::{ apu::{ @@ -17,7 +20,7 @@ use systems::{ AuxiliaryPowerUnitFireOverheadPanel, AuxiliaryPowerUnitOverheadPanel, }, electrical::{consumption::SuppliedPower, ElectricalSystem, ExternalPowerSource}, - engine::Engine, + engine::{leap_engine::LeapEngine, Engine}, landing_gear::LandingGear, simulation::{Aircraft, SimulationElement, SimulationElementVisitor, UpdateContext}, }; @@ -30,12 +33,14 @@ pub struct A320 { electrical_overhead: A320ElectricalOverheadPanel, emergency_electrical_overhead: A320EmergencyElectricalOverheadPanel, fuel: A320Fuel, - engine_1: Engine, - engine_2: Engine, + engine_1: LeapEngine, + engine_2: LeapEngine, + engine_fire_overhead: A320EngineFireOverheadPanel, electrical: A320Electrical, power_consumption: A320PowerConsumption, ext_pwr: ExternalPowerSource, hydraulic: A320Hydraulic, + hydraulic_overhead: A320HydraulicOverheadPanel, landing_gear: LandingGear, } impl A320 { @@ -48,12 +53,14 @@ impl A320 { electrical_overhead: A320ElectricalOverheadPanel::new(), emergency_electrical_overhead: A320EmergencyElectricalOverheadPanel::new(), fuel: A320Fuel::new(), - engine_1: Engine::new(1), - engine_2: Engine::new(2), + engine_1: LeapEngine::new(1), + engine_2: LeapEngine::new(2), + engine_fire_overhead: A320EngineFireOverheadPanel::new(), electrical: A320Electrical::new(), power_consumption: A320PowerConsumption::new(), ext_pwr: ExternalPowerSource::new(), hydraulic: A320Hydraulic::new(), + hydraulic_overhead: A320HydraulicOverheadPanel::new(), landing_gear: LandingGear::new(), } } @@ -108,7 +115,17 @@ impl Aircraft for A320 { } fn update_after_power_distribution(&mut self, context: &UpdateContext) { - self.hydraulic.update(context); + self.hydraulic.update( + context, + &self.engine_1, + &self.engine_2, + &self.hydraulic_overhead, + &self.engine_fire_overhead, + &self.landing_gear, + ); + + self.hydraulic_overhead.update(&self.hydraulic); + self.power_consumption.update(context); } @@ -127,9 +144,12 @@ impl SimulationElement for A320 { self.pneumatic_overhead.accept(visitor); self.engine_1.accept(visitor); self.engine_2.accept(visitor); + self.engine_fire_overhead.accept(visitor); self.electrical.accept(visitor); self.power_consumption.accept(visitor); self.ext_pwr.accept(visitor); + self.hydraulic.accept(visitor); + self.hydraulic_overhead.accept(visitor); self.landing_gear.accept(visitor); visitor.visit(self); diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index efdd1f9ad80..5fea2d9eba7 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -33,6 +33,7 @@ struct A320SimulatorReaderWriter { engine_generator_1_pb_on: AircraftVariable, engine_generator_2_pb_on: AircraftVariable, gear_center_position: AircraftVariable, + gear_handle_position: AircraftVariable, turb_eng_corrected_n2_1: AircraftVariable, turb_eng_corrected_n2_2: AircraftVariable, airspeed_indicated: AircraftVariable, @@ -40,6 +41,17 @@ struct A320SimulatorReaderWriter { fuel_tank_left_main_quantity: AircraftVariable, sim_on_ground: AircraftVariable, unlimited_fuel: AircraftVariable, + parking_brake_demand: AircraftVariable, + master_eng_1: AircraftVariable, + master_eng_2: AircraftVariable, + cargo_door_front_pos: AircraftVariable, + cargo_door_back_pos: AircraftVariable, + pushback_angle: AircraftVariable, + pushback_state: AircraftVariable, + anti_skid_activated: AircraftVariable, + left_brake_command: AircraftVariable, + right_brake_command: AircraftVariable, + longitudinal_accel: AircraftVariable, } impl A320SimulatorReaderWriter { fn new() -> Result> { @@ -66,6 +78,7 @@ impl A320SimulatorReaderWriter { 2, )?, gear_center_position: AircraftVariable::from("GEAR CENTER POSITION", "Percent", 0)?, + gear_handle_position: AircraftVariable::from("GEAR HANDLE POSITION", "Bool", 0)?, turb_eng_corrected_n2_1: AircraftVariable::from("TURB ENG CORRECTED N2", "Percent", 1)?, turb_eng_corrected_n2_2: AircraftVariable::from("TURB ENG CORRECTED N2", "Percent", 2)?, airspeed_indicated: AircraftVariable::from("AIRSPEED INDICATED", "Knots", 0)?, @@ -77,6 +90,24 @@ impl A320SimulatorReaderWriter { )?, sim_on_ground: AircraftVariable::from("SIM ON GROUND", "Bool", 0)?, unlimited_fuel: AircraftVariable::from("UNLIMITED FUEL", "Bool", 0)?, + parking_brake_demand: AircraftVariable::from("BRAKE PARKING INDICATOR", "Bool", 0)?, + master_eng_1: AircraftVariable::from("GENERAL ENG STARTER ACTIVE", "Bool", 1)?, + master_eng_2: AircraftVariable::from("GENERAL ENG STARTER ACTIVE", "Bool", 2)?, + cargo_door_front_pos: AircraftVariable::from("EXIT OPEN", "Percent", 5)?, + + // TODO It is the catering door for now. + cargo_door_back_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, + + pushback_angle: AircraftVariable::from("PUSHBACK ANGLE", "Radian", 0)?, + pushback_state: AircraftVariable::from("PUSHBACK STATE", "Enum", 0)?, + anti_skid_activated: AircraftVariable::from("ANTISKID BRAKES ACTIVE", "Bool", 0)?, + left_brake_command: AircraftVariable::from("BRAKE LEFT POSITION", "Percent", 0)?, + right_brake_command: AircraftVariable::from("BRAKE RIGHT POSITION", "Percent", 0)?, + longitudinal_accel: AircraftVariable::from( + "ACCELERATION BODY Z", + "feet per second squared", + 0, + )?, }) } } @@ -91,6 +122,7 @@ impl SimulatorReaderWriter for A320SimulatorReaderWriter { "AMBIENT TEMPERATURE" => self.ambient_temperature.get(), "EXTERNAL POWER AVAILABLE:1" => self.external_power_available.get(), "GEAR CENTER POSITION" => self.gear_center_position.get(), + "GEAR HANDLE POSITION" => self.gear_handle_position.get(), "TURB ENG CORRECTED N2:1" => self.turb_eng_corrected_n2_1.get(), "TURB ENG CORRECTED N2:2" => self.turb_eng_corrected_n2_2.get(), "FUEL TANK LEFT MAIN QUANTITY" => self.fuel_tank_left_main_quantity.get(), @@ -98,6 +130,17 @@ impl SimulatorReaderWriter for A320SimulatorReaderWriter { "AIRSPEED INDICATED" => self.airspeed_indicated.get(), "INDICATED ALTITUDE" => self.indicated_altitude.get(), "SIM ON GROUND" => self.sim_on_ground.get(), + "GENERAL ENG STARTER ACTIVE:1" => self.master_eng_1.get(), + "GENERAL ENG STARTER ACTIVE:2" => self.master_eng_2.get(), + "BRAKE PARKING INDICATOR" => self.parking_brake_demand.get(), + "EXIT OPEN:5" => self.cargo_door_front_pos.get(), + "EXIT OPEN:3" => self.cargo_door_back_pos.get(), + "PUSHBACK ANGLE" => self.pushback_angle.get(), + "PUSHBACK STATE" => self.pushback_state.get(), + "ANTISKID BRAKES ACTIVE" => self.anti_skid_activated.get(), + "BRAKE LEFT POSITION" => self.left_brake_command.get(), + "BRAKE RIGHT POSITION" => self.right_brake_command.get(), + "ACCELERATION BODY Z" => self.longitudinal_accel.get(), _ => { lookup_named_variable(&mut self.dynamic_named_variables, "A32NX_", name).get_value() } diff --git a/src/systems/systems/src/apu/air_intake_flap.rs b/src/systems/systems/src/apu/air_intake_flap.rs index 862f5c2befb..6b22967e2bf 100644 --- a/src/systems/systems/src/apu/air_intake_flap.rs +++ b/src/systems/systems/src/apu/air_intake_flap.rs @@ -44,7 +44,7 @@ impl AirIntakeFlap { } fn get_flap_change_for_delta(&self, context: &UpdateContext) -> f64 { - 100. * (context.delta().as_secs_f64() / self.delay.as_secs_f64()) + 100. * (context.delta_as_secs_f64() / self.delay.as_secs_f64()) } pub fn is_fully_open(&self) -> bool { diff --git a/src/systems/systems/src/apu/aps3200.rs b/src/systems/systems/src/apu/aps3200.rs index 312cafe9c23..5068923fe60 100644 --- a/src/systems/systems/src/apu/aps3200.rs +++ b/src/systems/systems/src/apu/aps3200.rs @@ -233,9 +233,9 @@ impl BleedAirUsageEgtDelta { if (self.current - self.target).abs() > f64::EPSILON { if self.current > self.target { - self.current -= self.delta_per_second() * context.delta().as_secs_f64(); + self.current -= self.delta_per_second() * context.delta_as_secs_f64(); } else { - self.current += self.delta_per_second() * context.delta().as_secs_f64(); + self.current += self.delta_per_second() * context.delta_as_secs_f64(); } } @@ -299,9 +299,7 @@ impl ApuGenUsageEgtDelta { ApuGenUsageEgtDelta::SECONDS_TO_REACH_TARGET, )) } else { - Duration::from_secs_f64( - (self.time.as_secs_f64() - context.delta().as_secs_f64()).max(0.), - ) + Duration::from_secs_f64((self.time.as_secs_f64() - context.delta_as_secs_f64()).max(0.)) }; } @@ -344,7 +342,7 @@ impl Running { ) -> ThermodynamicTemperature { // Reduce the deviation by 1 per second to slowly creep back to normal temperatures self.base_egt_deviation -= TemperatureInterval::new::( - (context.delta().as_secs_f64() * 1.).min( + (context.delta_as_secs_f64() * 1.).min( self.base_egt_deviation .get::(), ), diff --git a/src/systems/systems/src/engine/leap_engine.rs b/src/systems/systems/src/engine/leap_engine.rs new file mode 100644 index 00000000000..703d6fd55a5 --- /dev/null +++ b/src/systems/systems/src/engine/leap_engine.rs @@ -0,0 +1,68 @@ +use uom::si::{angular_velocity::revolution_per_minute, f64::*, pressure::psi, ratio::percent}; + +use crate::simulation::{SimulationElement, SimulatorReader, UpdateContext}; + +use super::Engine; +pub struct LeapEngine { + corrected_n2_id: String, + corrected_n2: Ratio, + n2_speed: AngularVelocity, + hydraulic_pump_output_speed: AngularVelocity, + oil_pressure: Pressure, +} +impl LeapEngine { + // According to the Type Certificate Data Sheet of LEAP 1A26 + // Max N2 rpm is 116.5% @ 19391 RPM + // 100% @ 16645 RPM + const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; + // Gear ratio from primary gearbox input to EDP drive shaft + const PUMP_N2_GEAR_RATIO: f64 = 0.211; + + const MIN_IDLE_N2_THRESHOLD: f64 = 60.; + + pub fn new(number: usize) -> LeapEngine { + LeapEngine { + corrected_n2_id: format!("TURB ENG CORRECTED N2:{}", number), + corrected_n2: Ratio::new::(0.), + n2_speed: AngularVelocity::new::(0.), + hydraulic_pump_output_speed: AngularVelocity::new::(0.), + oil_pressure: Pressure::new::(0.), + } + } + + pub fn update(&mut self, _: &UpdateContext) {} + + fn update_parameters(&mut self) { + self.n2_speed = AngularVelocity::new::( + self.corrected_n2.get::() * Self::LEAP_1A26_MAX_N2_RPM / 100., + ); + self.hydraulic_pump_output_speed = self.n2_speed * Self::PUMP_N2_GEAR_RATIO; + + // Ultra stupid model just to have 18psi crossing at 25% N2 + self.oil_pressure = Pressure::new::(18. / 25. * self.corrected_n2.get::()); + } +} +impl SimulationElement for LeapEngine { + fn read(&mut self, reader: &mut SimulatorReader) { + self.corrected_n2 = Ratio::new::(reader.read_f64(&self.corrected_n2_id)); + self.update_parameters(); + } +} + +impl Engine for LeapEngine { + fn corrected_n2(&self) -> Ratio { + self.corrected_n2 + } + + fn hydraulic_pump_output_speed(&self) -> AngularVelocity { + self.hydraulic_pump_output_speed + } + + fn oil_pressure(&self) -> Pressure { + self.oil_pressure + } + + fn is_above_minimum_idle(&self) -> bool { + self.corrected_n2 >= Ratio::new::(LeapEngine::MIN_IDLE_N2_THRESHOLD) + } +} diff --git a/src/systems/systems/src/engine/mod.rs b/src/systems/systems/src/engine/mod.rs index 02835998c1f..cc6e015f60b 100644 --- a/src/systems/systems/src/engine/mod.rs +++ b/src/systems/systems/src/engine/mod.rs @@ -1,27 +1,10 @@ -use uom::si::{f64::*, ratio::percent}; +use uom::si::f64::*; -use crate::simulation::{SimulationElement, SimulatorReader, UpdateContext}; +pub mod leap_engine; -pub struct Engine { - corrected_n2_id: String, - corrected_n2: Ratio, -} -impl Engine { - pub fn new(number: usize) -> Engine { - Engine { - corrected_n2_id: format!("TURB ENG CORRECTED N2:{}", number), - corrected_n2: Ratio::new::(0.), - } - } - - pub fn corrected_n2(&self) -> Ratio { - self.corrected_n2 - } - - pub fn update(&mut self, _: &UpdateContext) {} -} -impl SimulationElement for Engine { - fn read(&mut self, reader: &mut SimulatorReader) { - self.corrected_n2 = Ratio::new::(reader.read_f64(&self.corrected_n2_id)); - } +pub trait Engine { + fn corrected_n2(&self) -> Ratio; + fn hydraulic_pump_output_speed(&self) -> AngularVelocity; + fn oil_pressure(&self) -> Pressure; + fn is_above_minimum_idle(&self) -> bool; } diff --git a/src/systems/systems/src/hydraulic/brake_circuit.rs b/src/systems/systems/src/hydraulic/brake_circuit.rs new file mode 100644 index 00000000000..a57bf44ec34 --- /dev/null +++ b/src/systems/systems/src/hydraulic/brake_circuit.rs @@ -0,0 +1,670 @@ +use crate::{ + hydraulic::HydraulicLoop, + simulation::{SimulationElement, SimulatorWriter, UpdateContext}, +}; + +use std::f64::consts::E; +use std::string::String; + +use uom::si::{f64::*, pressure::psi, volume::gallon}; + +use super::Accumulator; + +pub trait Actuator { + fn used_volume(&self) -> Volume; + fn reservoir_return(&self) -> Volume; +} + +struct BrakeActuator { + total_displacement: Volume, + base_speed: f64, + + current_position: f64, + + required_position: f64, + + volume_to_actuator_accumulator: Volume, + volume_to_res_accumulator: Volume, +} +impl BrakeActuator { + const ACTUATOR_BASE_SPEED: f64 = 1.5; // movement in percent/100 per second. 1 means 0 to 1 in 1s + const MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI: f64 = 50.; + const PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI: f64 = 3100.; + + fn new(total_displacement: Volume) -> Self { + Self { + total_displacement, + base_speed: BrakeActuator::ACTUATOR_BASE_SPEED, + current_position: 0., + required_position: 0., + volume_to_actuator_accumulator: Volume::new::(0.), + volume_to_res_accumulator: Volume::new::(0.), + } + } + + fn set_position_demand(&mut self, required_position: f64) { + self.required_position = required_position; + } + + fn get_max_position_reachable(&self, received_pressure: Pressure) -> f64 { + if received_pressure.get::() > Self::MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI { + (received_pressure.get::() / Self::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI) + .min(1.) + .max(0.) + } else { + 0. + } + } + + fn get_applied_brake_pressure(&self) -> Pressure { + Pressure::new::(Self::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI) * self.current_position + } + + fn update(&mut self, context: &UpdateContext, received_pressure: Pressure) { + let final_delta_position = self.update_position(context, received_pressure); + + if final_delta_position > 0. { + self.volume_to_actuator_accumulator += final_delta_position * self.total_displacement; + } else { + self.volume_to_res_accumulator += -final_delta_position * self.total_displacement; + } + } + + fn reset_accumulators(&mut self) { + self.volume_to_actuator_accumulator = Volume::new::(0.); + self.volume_to_res_accumulator = Volume::new::(0.); + } + + fn update_position(&mut self, context: &UpdateContext, loop_pressure: Pressure) -> f64 { + // Final required position for actuator is the required one unless we can't reach it due to pressure + let final_required_position = self + .required_position + .min(self.get_max_position_reachable(loop_pressure)); + let delta_position_required = final_required_position - self.current_position; + + let mut new_position = self.current_position; + if delta_position_required > 0.001 { + new_position = self.current_position + context.delta_as_secs_f64() * self.base_speed; + new_position = new_position.min(self.current_position + delta_position_required); + } else if delta_position_required < -0.001 { + new_position = self.current_position - context.delta_as_secs_f64() * self.base_speed; + new_position = new_position.max(self.current_position + delta_position_required); + } + new_position = new_position.min(1.).max(0.); + let final_delta_position = new_position - self.current_position; + self.current_position = new_position; + + final_delta_position + } +} +impl Actuator for BrakeActuator { + fn used_volume(&self) -> Volume { + self.volume_to_actuator_accumulator + } + fn reservoir_return(&self) -> Volume { + self.volume_to_res_accumulator + } +} + +/// Brakes implementation. This tries to do a simple model with a possibility to have an accumulator (or not) +/// Brake model is simplified as we just move brake actuator position from 0 to 1 and take corresponding fluid volume (vol = max_displacement * brake_position). +/// So it's fairly simplified as we just end up with brake pressure = PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI * current_position +pub struct BrakeCircuit { + _id: String, + id_left_press: String, + id_right_press: String, + id_acc_press: String, + + left_brake_actuator: BrakeActuator, + right_brake_actuator: BrakeActuator, + + demanded_brake_position_left: f64, + pressure_applied_left: Pressure, + demanded_brake_position_right: f64, + pressure_applied_right: Pressure, + + pressure_limitation: Pressure, + pressure_limitation_active: bool, + + /// Brake accumulator variables. Accumulator can have 0 volume if no accumulator + has_accumulator: bool, + accumulator: Accumulator, + + /// Common vars to all actuators: will be used by the calling loop to know what is used + /// and what comes back to reservoir at each iteration + volume_to_actuator_accumulator: Volume, + volume_to_res_accumulator: Volume, + + /// Fluid pressure in brake circuit filtered for cockpit gauges + accumulator_fluid_pressure_sensor_filtered: Pressure, +} +impl BrakeCircuit { + const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1000.0; // Nitrogen PSI + const ACCUMULATOR_PRESS_BREAKPTS: [f64; 10] = [ + 0.0, 5.0, 25.0, 40.0, 100.0, 200.0, 500.0, 1000.0, 3000., 10000.0, + ]; + const ACCUMULATOR_FLOW_CARAC: [f64; 10] = + [0.0, 0.001, 0.004, 0.006, 0.02, 0.05, 0.15, 0.35, 0.5, 0.5]; + + // Filtered using time constant low pass: new_val = old_val + (new_val - old_val)* (1 - e^(-dt/TCONST)) + // Time constant of the filter used to measure brake circuit pressure + const ACC_PRESSURE_SENSOR_FILTER_TIMECONST: f64 = 0.1; + + pub fn new( + id: &str, + accumulator_volume: Volume, + accumulator_fluid_volume_at_init: Volume, + total_displacement: Volume, + ) -> BrakeCircuit { + let mut has_accu = true; + if accumulator_volume <= Volume::new::(0.) { + has_accu = false; + } + + BrakeCircuit { + _id: String::from(id).to_uppercase(), + id_left_press: format!("HYD_BRAKE_{}_LEFT_PRESS", id), + id_right_press: format!("HYD_BRAKE_{}_RIGHT_PRESS", id), + id_acc_press: format!("HYD_BRAKE_{}_ACC_PRESS", id), + + // We assume displacement is just split on left and right + left_brake_actuator: BrakeActuator::new(total_displacement / 2.), + right_brake_actuator: BrakeActuator::new(total_displacement / 2.), + + demanded_brake_position_left: 0.0, + pressure_applied_left: Pressure::new::(0.0), + demanded_brake_position_right: 0.0, + pressure_applied_right: Pressure::new::(0.0), + pressure_limitation: Pressure::new::(0.0), + pressure_limitation_active: false, + has_accumulator: has_accu, + accumulator: Accumulator::new( + Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), + accumulator_volume, + accumulator_fluid_volume_at_init, + Self::ACCUMULATOR_PRESS_BREAKPTS, + Self::ACCUMULATOR_FLOW_CARAC, + true, + ), + volume_to_actuator_accumulator: Volume::new::(0.), + volume_to_res_accumulator: Volume::new::(0.), + + // Pressure measured after accumulator in brake circuit + accumulator_fluid_pressure_sensor_filtered: Pressure::new::(0.0), + } + } + + pub fn set_brake_press_limit(&mut self, pressure_limit: Pressure) { + self.pressure_limitation = pressure_limit; + } + + pub fn set_brake_limit_active(&mut self, is_pressure_limit_active: bool) { + self.pressure_limitation_active = is_pressure_limit_active; + } + + fn update_brake_actuators(&mut self, context: &UpdateContext, hyd_pressure: Pressure) { + self.left_brake_actuator + .set_position_demand(self.demanded_brake_position_left); + self.right_brake_actuator + .set_position_demand(self.demanded_brake_position_right); + + let actual_max_allowed_pressure: Pressure; + if self.pressure_limitation_active { + actual_max_allowed_pressure = hyd_pressure.min(self.pressure_limitation); + } else { + actual_max_allowed_pressure = hyd_pressure; + } + + self.left_brake_actuator + .update(context, actual_max_allowed_pressure); + self.right_brake_actuator + .update(context, actual_max_allowed_pressure); + } + + pub fn update(&mut self, context: &UpdateContext, hyd_loop: &HydraulicLoop) { + // The pressure available in brakes is the one of accumulator only if accumulator has fluid + let actual_pressure_available: Pressure; + if self.accumulator.fluid_volume() > Volume::new::(0.) { + actual_pressure_available = self.accumulator.raw_gas_press(); + } else { + actual_pressure_available = hyd_loop.pressure(); + } + + self.update_brake_actuators(context, actual_pressure_available); + + let delta_vol = + self.left_brake_actuator.used_volume() + self.right_brake_actuator.used_volume(); + + if self.has_accumulator { + let mut volume_into_accumulator = Volume::new::(0.); + self.accumulator.update( + context, + &mut volume_into_accumulator, + hyd_loop.loop_pressure, + ); + + // Volume that just came into accumulator is taken from hydraulic loop through volume_to_actuator interface + self.volume_to_actuator_accumulator += volume_into_accumulator.abs(); + + if delta_vol > Volume::new::(0.) { + let volume_from_acc = self.accumulator.get_delta_vol(delta_vol); + let remaining_vol_after_accumulator_empty = delta_vol - volume_from_acc; + self.volume_to_actuator_accumulator += remaining_vol_after_accumulator_empty; + } + } else { + // Else case if no accumulator: we just take deltavol needed or return it back to res + self.volume_to_actuator_accumulator += delta_vol; + } + + self.volume_to_res_accumulator += self.left_brake_actuator.reservoir_return(); + self.volume_to_res_accumulator += self.right_brake_actuator.reservoir_return(); + + self.left_brake_actuator.reset_accumulators(); + self.right_brake_actuator.reset_accumulators(); + + self.pressure_applied_left = self.left_brake_actuator.get_applied_brake_pressure(); + self.pressure_applied_right = self.right_brake_actuator.get_applied_brake_pressure(); + + self.accumulator_fluid_pressure_sensor_filtered = self + .accumulator_fluid_pressure_sensor_filtered + + (actual_pressure_available - self.accumulator_fluid_pressure_sensor_filtered) + * (1. + - E.powf( + -context.delta_as_secs_f64() + / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, + )); + } + + pub fn set_brake_demand_left(&mut self, brake_ratio: f64) { + self.demanded_brake_position_left = brake_ratio.min(1.0).max(0.0); + } + + pub fn set_brake_demand_right(&mut self, brake_ratio: f64) { + self.demanded_brake_position_right = brake_ratio.min(1.0).max(0.0); + } + + pub fn left_brake_pressure(&self) -> Pressure { + self.pressure_applied_left + } + + pub fn right_brake_pressure(&self) -> Pressure { + self.pressure_applied_right + } + + fn accumulator_pressure(&self) -> Pressure { + self.accumulator_fluid_pressure_sensor_filtered + } + + pub fn accumulator_fluid_volume(&self) -> Volume { + self.accumulator.fluid_volume() + } + + pub fn reset_accumulators(&mut self) { + self.volume_to_res_accumulator = Volume::new::(0.); + self.volume_to_actuator_accumulator = Volume::new::(0.); + } +} +impl Actuator for BrakeCircuit { + fn used_volume(&self) -> Volume { + self.volume_to_actuator_accumulator + } + fn reservoir_return(&self) -> Volume { + self.volume_to_res_accumulator + } +} +impl SimulationElement for BrakeCircuit { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.id_left_press, self.left_brake_pressure().get::()); + writer.write_f64( + &self.id_right_press, + self.right_brake_pressure().get::(), + ); + if self.has_accumulator { + writer.write_f64(&self.id_acc_press, self.accumulator_pressure().get::()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + hydraulic::{Fluid, HydraulicLoop}, + simulation::UpdateContext, + }; + use std::time::Duration; + use uom::si::{ + acceleration::foot_per_second_squared, + length::foot, + pressure::{pascal, psi}, + thermodynamic_temperature::degree_celsius, + velocity::knot, + volume::gallon, + }; + + #[test] + fn brake_actuator_movement() { + let mut brake_actuator = BrakeActuator::new(Volume::new::(0.04)); + + assert!(brake_actuator.current_position == 0.); + assert!(brake_actuator.required_position == 0.); + + brake_actuator.set_position_demand(1.2); + + for loop_idx in 0..15 { + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(BrakeActuator::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI), + ); + println!( + "Loop {}, position: {}", + loop_idx, brake_actuator.current_position + ); + } + + assert!(brake_actuator.current_position >= 0.99); + assert!( + brake_actuator.volume_to_actuator_accumulator >= Volume::new::(0.04 - 0.0001) + ); + assert!( + brake_actuator.volume_to_actuator_accumulator <= Volume::new::(0.04 + 0.0001) + ); + assert!(brake_actuator.volume_to_res_accumulator <= Volume::new::(0.0001)); + + brake_actuator.reset_accumulators(); + + brake_actuator.set_position_demand(-2.); + for _ in 0..15 { + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(3000.), + ); + } + + assert!(brake_actuator.current_position <= 0.01); + assert!(brake_actuator.current_position >= 0.); + + assert!(brake_actuator.volume_to_res_accumulator >= Volume::new::(0.04 - 0.0001)); + assert!(brake_actuator.volume_to_res_accumulator <= Volume::new::(0.04 + 0.0001)); + assert!(brake_actuator.volume_to_actuator_accumulator <= Volume::new::(0.0001)); + + // Now same brake increase but with ultra low pressure + brake_actuator.reset_accumulators(); + brake_actuator.set_position_demand(1.2); + + for _ in 0..15 { + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(20.), + ); + } + + // We should not be able to move actuator + assert!(brake_actuator.current_position <= 0.1); + assert!(brake_actuator.volume_to_actuator_accumulator >= Volume::new::(-0.0001)); + assert!(brake_actuator.volume_to_actuator_accumulator <= Volume::new::(0.0001)); + assert!(brake_actuator.volume_to_res_accumulator <= Volume::new::(0.0001)); + } + + #[test] + fn brake_actuator_movement_medium_pressure() { + let mut brake_actuator = BrakeActuator::new(Volume::new::(0.04)); + + brake_actuator.set_position_demand(1.2); + + let medium_pressure = Pressure::new::(1500.); + // Update position with 1500psi only: should not reach max displacement. + for loop_idx in 0..15 { + brake_actuator.update(&context(Duration::from_secs_f64(0.1)), medium_pressure); + println!( + "Loop {}, position: {}", + loop_idx, brake_actuator.current_position + ); + } + + assert!( + brake_actuator.current_position + <= brake_actuator.get_max_position_reachable(medium_pressure) + ); + + // Now same max demand but pressure so low so actuator should get back to 0 + brake_actuator.reset_accumulators(); + brake_actuator.set_position_demand(1.2); + + for _loop_idx in 0..15 { + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(20.), + ); + println!( + "Loop {}, Low pressure: position: {}", + _loop_idx, brake_actuator.current_position + ); + } + + // We should have actuator back to 0 + assert!(brake_actuator.current_position <= 0.1); + } + + #[test] + fn brake_state_at_init() { + let init_max_vol = Volume::new::(1.5); + let brake_circuit_unprimed = BrakeCircuit::new( + "altn", + init_max_vol, + Volume::new::(0.0), + Volume::new::(0.1), + ); + + assert!( + brake_circuit_unprimed.left_brake_pressure() + + brake_circuit_unprimed.right_brake_pressure() + < Pressure::new::(10.0) + ); + assert!(brake_circuit_unprimed.accumulator.total_volume == init_max_vol); + assert!(brake_circuit_unprimed.accumulator.fluid_volume() == Volume::new::(0.0)); + assert!(brake_circuit_unprimed.accumulator.gas_volume == init_max_vol); + + let brake_circuit_primed = BrakeCircuit::new( + "altn", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); + + assert!( + brake_circuit_unprimed.left_brake_pressure() + + brake_circuit_unprimed.right_brake_pressure() + < Pressure::new::(10.0) + ); + assert!(brake_circuit_primed.accumulator.total_volume == init_max_vol); + assert!(brake_circuit_primed.accumulator.fluid_volume() == init_max_vol / 2.0); + assert!(brake_circuit_primed.accumulator.gas_volume < init_max_vol); + } + + #[test] + fn brake_pressure_rise() { + let init_max_vol = Volume::new::(1.5); + let mut hyd_loop = hydraulic_loop("YELLOW"); + hyd_loop.loop_pressure = Pressure::new::(2500.0); + + let mut brake_circuit_primed = BrakeCircuit::new( + "Altn", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); + + assert!( + brake_circuit_primed.left_brake_pressure() + + brake_circuit_primed.right_brake_pressure() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); + + assert!( + brake_circuit_primed.left_brake_pressure() + + brake_circuit_primed.right_brake_pressure() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.set_brake_demand_left(1.0); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); + + assert!(brake_circuit_primed.left_brake_pressure() >= Pressure::new::(1000.)); + assert!(brake_circuit_primed.right_brake_pressure() <= Pressure::new::(50.)); + assert!(brake_circuit_primed.accumulator.fluid_volume() >= Volume::new::(0.1)); + + brake_circuit_primed.set_brake_demand_left(0.0); + brake_circuit_primed.set_brake_demand_right(1.0); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); + assert!(brake_circuit_primed.right_brake_pressure() >= Pressure::new::(1000.)); + assert!(brake_circuit_primed.left_brake_pressure() <= Pressure::new::(50.)); + assert!(brake_circuit_primed.accumulator.fluid_volume() >= Volume::new::(0.1)); + } + + #[test] + fn brake_pressure_rise_no_accumulator() { + let init_max_vol = Volume::new::(0.0); + let mut hyd_loop = hydraulic_loop("GREEN"); + hyd_loop.loop_pressure = Pressure::new::(2500.0); + + let mut brake_circuit_primed = BrakeCircuit::new( + "norm", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); + + assert!( + brake_circuit_primed.left_brake_pressure() + + brake_circuit_primed.right_brake_pressure() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); + + assert!( + brake_circuit_primed.left_brake_pressure() + + brake_circuit_primed.right_brake_pressure() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.set_brake_demand_left(1.0); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); + + assert!(brake_circuit_primed.left_brake_pressure() >= Pressure::new::(2500.)); + assert!(brake_circuit_primed.right_brake_pressure() <= Pressure::new::(50.)); + + brake_circuit_primed.set_brake_demand_left(0.0); + brake_circuit_primed.set_brake_demand_right(1.0); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); + assert!(brake_circuit_primed.right_brake_pressure() >= Pressure::new::(2500.)); + assert!(brake_circuit_primed.left_brake_pressure() <= Pressure::new::(50.)); + assert!(brake_circuit_primed.accumulator.fluid_volume() == Volume::new::(0.0)); + } + + #[test] + fn brake_pressure_limitation() { + let init_max_vol = Volume::new::(0.0); + let mut hyd_loop = hydraulic_loop("GREEN"); + hyd_loop.loop_pressure = Pressure::new::(3100.0); + + let mut brake_circuit_primed = BrakeCircuit::new( + "norm", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); + + brake_circuit_primed.update(&context(Duration::from_secs_f64(5.)), &hyd_loop); + + assert!( + brake_circuit_primed.left_brake_pressure() + + brake_circuit_primed.right_brake_pressure() + < Pressure::new::(1.0) + ); + + brake_circuit_primed.set_brake_demand_left(1.0); + brake_circuit_primed.set_brake_demand_right(1.0); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); + + assert!(brake_circuit_primed.left_brake_pressure() >= Pressure::new::(2900.)); + assert!(brake_circuit_primed.right_brake_pressure() >= Pressure::new::(2900.)); + + let pressure_limit = Pressure::new::(1200.); + brake_circuit_primed.set_brake_press_limit(pressure_limit); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); + assert!(brake_circuit_primed.left_brake_pressure() >= Pressure::new::(2900.)); + assert!(brake_circuit_primed.right_brake_pressure() >= Pressure::new::(2900.)); + + brake_circuit_primed.set_brake_limit_active(true); + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); + + // Now we limit to 1200 but pressure shouldn't drop instantly + assert!(brake_circuit_primed.left_brake_pressure() >= Pressure::new::(2500.)); + assert!(brake_circuit_primed.right_brake_pressure() >= Pressure::new::(2500.)); + + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); + + // After one second it should have reached the lower limit + assert!(brake_circuit_primed.left_brake_pressure() <= pressure_limit); + assert!(brake_circuit_primed.right_brake_pressure() <= pressure_limit); + } + + fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { + match loop_color { + "GREEN" => HydraulicLoop::new( + loop_color, + false, + true, + Volume::new::(26.00), + Volume::new::(26.41), + Volume::new::(10.0), + Volume::new::(3.83), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), + ), + "YELLOW" => HydraulicLoop::new( + loop_color, + true, + false, + Volume::new::(10.2), + Volume::new::(10.2), + Volume::new::(8.0), + Volume::new::(3.3), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), + ), + _ => HydraulicLoop::new( + loop_color, + false, + false, + Volume::new::(15.85), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.5), + Fluid::new(Pressure::new::(1450000000.0)), + false, + Pressure::new::(1450.0), + Pressure::new::(1750.0), + ), + } + } + + fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + ) + } +} diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 8b137891791..9ae84009759 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1 +1,1347 @@ +use std::string::String; +use std::time::Duration; +use uom::si::{ + f64::*, + pressure::psi, + velocity::knot, + volume::{cubic_inch, gallon}, + volume_rate::gallon_per_second, +}; + +use crate::shared::interpolation; +use crate::simulation::UpdateContext; +use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; + +pub mod brake_circuit; +use crate::hydraulic::brake_circuit::Actuator; + +pub trait PressureSource { + /// Gives the maximum available volume at that time as if it is a variable displacement + /// pump it can be adjusted by pump regulation. + fn delta_vol_max(&self) -> Volume; + + /// Gives the minimum volume that will be output no matter what. + /// For example if there is a minimal displacement or a fixed displacement (ie. elec pump). + fn delta_vol_min(&self) -> Volume; +} + +// TODO update method that can update physic constants from given temperature +// This would change pressure response to volume +pub struct Fluid { + current_bulk: Pressure, +} +impl Fluid { + pub fn new(bulk: Pressure) -> Self { + Self { current_bulk: bulk } + } + + pub fn bulk_mod(&self) -> Pressure { + self.current_bulk + } +} + +pub struct PressureSwitch { + state_is_pressurised: bool, + high_hysteresis_threshold: Pressure, + low_hysteresis_threshold: Pressure, +} +impl PressureSwitch { + pub fn new(high_threshold: Pressure, low_threshold: Pressure) -> Self { + Self { + state_is_pressurised: false, + high_hysteresis_threshold: high_threshold, + low_hysteresis_threshold: low_threshold, + } + } + + pub fn update(&mut self, current_pressure: Pressure) { + if current_pressure <= self.low_hysteresis_threshold { + self.state_is_pressurised = false; + } else if current_pressure >= self.high_hysteresis_threshold { + self.state_is_pressurised = true; + } + } + + pub fn is_pressurised(&self) -> bool { + self.state_is_pressurised + } +} + +pub trait PowerTransferUnitController { + fn should_enable(&self) -> bool; +} + +pub struct PowerTransferUnit { + is_enabled: bool, + is_active_right: bool, + is_active_left: bool, + flow_to_right: VolumeRate, + flow_to_left: VolumeRate, + last_flow: VolumeRate, +} +impl PowerTransferUnit { + // Low pass filter to handle flow dynamic: avoids instantaneous flow transient, + // simulating RPM dynamic of PTU + const FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE: f64 = 0.35; + const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.35; + + const EFFICIENCY_LEFT_TO_RIGHT: f64 = 0.8; + const EFFICIENCY_RIGHT_TO_LEFT: f64 = 0.8; + + // Part of the max total pump capacity PTU model is allowed to take. Set to 1 all capacity used + // set to 0.5 PTU will only use half of the flow that all pumps are able to generate + const AGRESSIVENESS_FACTOR: f64 = 0.78; + + pub fn new() -> Self { + Self { + is_enabled: false, + is_active_right: false, + is_active_left: false, + flow_to_right: VolumeRate::new::(0.0), + flow_to_left: VolumeRate::new::(0.0), + last_flow: VolumeRate::new::(0.0), + } + } + + pub fn flow(&self) -> VolumeRate { + self.last_flow + } + + pub fn is_enabled(&self) -> bool { + self.is_enabled + } + + pub fn is_active_left_to_right(&self) -> bool { + self.is_active_left + } + + pub fn is_active_right_to_left(&self) -> bool { + self.is_active_right + } + + pub fn update( + &mut self, + loop_left: &HydraulicLoop, + loop_right: &HydraulicLoop, + controller: &T, + ) { + self.is_enabled = controller.should_enable(); + + let delta_p = loop_left.pressure() - loop_right.pressure(); + + if !self.is_enabled + || self.is_active_right && delta_p.get::() > -5. + || self.is_active_left && delta_p.get::() < 5. + { + self.flow_to_left = VolumeRate::new::(0.0); + self.flow_to_right = VolumeRate::new::(0.0); + self.is_active_right = false; + self.is_active_left = false; + self.last_flow = VolumeRate::new::(0.0); + } else if delta_p.get::() > 500. || (self.is_active_left && delta_p.get::() > 5.) + { + // Left sends flow to right + let mut vr = 16.0f64.min(loop_left.loop_pressure.get::() * 0.0058) / 60.0; + + // Limiting available flow with maximum flow capacity of all pumps of the loop. + // This is a workaround to limit PTU greed for flow + vr = vr.min( + loop_left.current_max_flow.get::() * Self::AGRESSIVENESS_FACTOR, + ); + + // Low pass on flow + vr = Self::FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE * vr + + (1.0 - Self::FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE) + * self.last_flow.get::(); + + self.flow_to_left = VolumeRate::new::(-vr); + self.flow_to_right = + VolumeRate::new::(vr * Self::EFFICIENCY_LEFT_TO_RIGHT); + self.last_flow = VolumeRate::new::(vr); + + self.is_active_left = true; + } else if delta_p.get::() < -500. + || (self.is_active_right && delta_p.get::() < -5.) + { + // Right sends flow to left + let mut vr = 34.0f64.min(loop_right.loop_pressure.get::() * 0.0125) / 60.0; + + // Limiting available flow with maximum flow capacity of all pumps of the loop. + // This is a workaround to limit PTU greed for flow + vr = vr.min( + loop_right.current_max_flow.get::() * Self::AGRESSIVENESS_FACTOR, + ); + + // Low pass on flow + vr = Self::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE * vr + + (1.0 - Self::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE) + * self.last_flow.get::(); + + self.flow_to_left = + VolumeRate::new::(vr * Self::EFFICIENCY_RIGHT_TO_LEFT); + self.flow_to_right = VolumeRate::new::(-vr); + self.last_flow = VolumeRate::new::(vr); + + self.is_active_right = true; + } + } +} +impl SimulationElement for PowerTransferUnit { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_PTU_ACTIVE_L2R", self.is_active_left); + writer.write_bool("HYD_PTU_ACTIVE_R2L", self.is_active_right); + writer.write_f64("HYD_PTU_MOTOR_FLOW", self.flow().get::()); + writer.write_bool("HYD_PTU_VALVE_OPENED", self.is_enabled()); + } +} +impl Default for PowerTransferUnit { + fn default() -> Self { + Self::new() + } +} + +pub trait HydraulicLoopController { + fn should_open_fire_shutoff_valve(&self) -> bool; +} + +struct Accumulator { + total_volume: Volume, + gas_init_precharge: Pressure, + gas_pressure: Pressure, + gas_volume: Volume, + fluid_volume: Volume, + current_flow: VolumeRate, + current_delta_vol: Volume, + press_breakpoints: [f64; 10], + flow_carac: [f64; 10], + has_control_valve: bool, +} +impl Accumulator { + const FLOW_DYNAMIC_LOW_PASS: f64 = 0.7; + + fn new( + gas_precharge: Pressure, + total_volume: Volume, + fluid_vol_at_init: Volume, + press_breakpoints: [f64; 10], + flow_carac: [f64; 10], + has_control_valve: bool, + ) -> Self { + // Taking care of case where init volume is maxed at accumulator capacity: we can't exceed max_volume minus a margin for gas to compress + let limited_volume = fluid_vol_at_init.min(total_volume * 0.9); + + // If we don't start with empty accumulator we need to init pressure too + let gas_press_at_init = gas_precharge * total_volume / (total_volume - limited_volume); + + Self { + total_volume, + gas_init_precharge: gas_precharge, + gas_pressure: gas_press_at_init, + gas_volume: (total_volume - limited_volume), + fluid_volume: limited_volume, + current_flow: VolumeRate::new::(0.), + current_delta_vol: Volume::new::(0.), + press_breakpoints, + flow_carac, + has_control_valve, + } + } + + fn update(&mut self, context: &UpdateContext, delta_vol: &mut Volume, loop_pressure: Pressure) { + let accumulator_delta_press = self.gas_pressure - loop_pressure; + let mut flow_variation = VolumeRate::new::(interpolation( + &self.press_breakpoints, + &self.flow_carac, + accumulator_delta_press.get::().abs(), + )); + flow_variation = flow_variation * Self::FLOW_DYNAMIC_LOW_PASS + + (1. - Self::FLOW_DYNAMIC_LOW_PASS) * self.current_flow; + + // TODO HANDLE OR CHECK IF RESERVOIR AVAILABILITY is OK + // TODO check if accumulator can be used as a min/max flow producer to + // avoid it being a consumer that might unsettle pressure + if accumulator_delta_press.get::() > 0.0 && !self.has_control_valve { + let volume_from_acc = self + .fluid_volume + .min(flow_variation * context.delta_as_time()); + self.fluid_volume -= volume_from_acc; + self.gas_volume += volume_from_acc; + self.current_delta_vol = -volume_from_acc; + + *delta_vol += volume_from_acc; + } else if accumulator_delta_press.get::() < 0.0 { + let volume_to_acc = delta_vol + .max(Volume::new::(0.0)) + .max(flow_variation * context.delta_as_time()); + self.fluid_volume += volume_to_acc; + self.gas_volume -= volume_to_acc; + self.current_delta_vol = volume_to_acc; + + *delta_vol -= volume_to_acc; + } + + self.current_flow = self.current_delta_vol / context.delta_as_time(); + self.gas_pressure = + (self.gas_init_precharge * self.total_volume) / (self.total_volume - self.fluid_volume); + } + + fn get_delta_vol(&mut self, required_delta_vol: Volume) -> Volume { + let mut volume_from_acc = Volume::new::(0.0); + if required_delta_vol > Volume::new::(0.0) { + volume_from_acc = self.fluid_volume.min(required_delta_vol); + if volume_from_acc != Volume::new::(0.0) { + self.fluid_volume -= volume_from_acc; + self.gas_volume += volume_from_acc; + + self.gas_pressure = self.gas_init_precharge * self.total_volume + / (self.total_volume - self.fluid_volume); + } + } + + volume_from_acc + } + + fn fluid_volume(&self) -> Volume { + self.fluid_volume + } + + fn raw_gas_press(&self) -> Pressure { + self.gas_pressure + } +} +pub struct HydraulicLoop { + pressure_id: String, + reservoir_id: String, + fire_valve_id: String, + fluid: Fluid, + accumulator: Accumulator, + connected_to_ptu_left_side: bool, + connected_to_ptu_right_side: bool, + loop_pressure: Pressure, + loop_volume: Volume, + max_loop_volume: Volume, + high_pressure_volume: Volume, + ptu_active: bool, + reservoir_volume: Volume, + current_delta_vol: Volume, + current_flow: VolumeRate, + /// Current total max flow available from pressure sources + current_max_flow: VolumeRate, + fire_shutoff_valve_opened: bool, + has_fire_valve: bool, + min_pressure_pressurised_lo_hyst: Pressure, + min_pressure_pressurised_hi_hyst: Pressure, + is_pressurised: bool, + total_actuators_consumed_volume: Volume, + total_actuators_returned_volume: Volume, +} +impl HydraulicLoop { + // Nitrogen PSI + const ACCUMULATOR_GAS_PRE_CHARGE_PSI: f64 = 1885.0; + // in gallons + const ACCUMULATOR_MAX_VOLUME_GALLONS: f64 = 0.264; + + // Gallon per s of flow lost to reservoir @ 3000psi + const STATIC_LEAK_FLOW_GALLON_PER_SECOND: f64 = 0.05; + + const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.4; + + const ACCUMULATOR_PRESS_BREAKPTS: [f64; 10] = [ + 0.0, 1., 5.0, 50.0, 100., 200.0, 500.0, 1000., 2000.0, 10000.0, + ]; + const ACCUMULATOR_FLOW_CARAC: [f64; 10] = + [0.0, 0.001, 0.005, 0.05, 0.08, 0.15, 0.25, 0.35, 0.5, 0.5]; + + #[allow(clippy::too_many_arguments)] + pub fn new( + id: &str, + connected_to_ptu_left_side: bool, // Is connected to PTU "left" side: non variable displacement side + connected_to_ptu_right_side: bool, // Is connected to PTU "right" side: variable displacement side + loop_volume: Volume, + max_loop_volume: Volume, + high_pressure_volume: Volume, + reservoir_volume: Volume, + fluid: Fluid, + has_fire_valve: bool, + min_pressure_pressurised_lo_hyst: Pressure, + min_pressure_pressurised_hi_hyst: Pressure, + ) -> Self { + Self { + pressure_id: format!("HYD_{}_PRESSURE", id), + reservoir_id: format!("HYD_{}_RESERVOIR", id), + fire_valve_id: format!("HYD_{}_FIRE_VALVE_OPENED", id), + + connected_to_ptu_left_side, + connected_to_ptu_right_side, + loop_pressure: Pressure::new::(14.7), + loop_volume, + max_loop_volume, + high_pressure_volume, + ptu_active: false, + reservoir_volume, + fluid, + accumulator: Accumulator::new( + Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE_PSI), + Volume::new::(Self::ACCUMULATOR_MAX_VOLUME_GALLONS), + Volume::new::(0.), + Self::ACCUMULATOR_PRESS_BREAKPTS, + Self::ACCUMULATOR_FLOW_CARAC, + false, + ), + current_delta_vol: Volume::new::(0.), + current_flow: VolumeRate::new::(0.), + current_max_flow: VolumeRate::new::(0.), + fire_shutoff_valve_opened: true, + has_fire_valve, + min_pressure_pressurised_lo_hyst, + min_pressure_pressurised_hi_hyst, + is_pressurised: false, + total_actuators_consumed_volume: Volume::new::(0.), + total_actuators_returned_volume: Volume::new::(0.), + } + } + + pub fn current_flow(&self) -> VolumeRate { + self.current_flow + } + + pub fn current_delta_vol(&self) -> Volume { + self.current_delta_vol + } + + pub fn accumulator_gas_pressure(&self) -> Pressure { + self.accumulator.gas_pressure + } + + pub fn accumulator_fluid_volume(&self) -> Volume { + self.accumulator.fluid_volume + } + + pub fn pressure(&self) -> Pressure { + self.loop_pressure + } + + pub fn reservoir_volume(&self) -> Volume { + self.reservoir_volume + } + + pub fn loop_fluid_volume(&self) -> Volume { + self.loop_volume + } + + pub fn max_volume(&self) -> Volume { + self.max_loop_volume + } + + pub fn accumulator_gas_volume(&self) -> Volume { + self.accumulator.gas_volume + } + + pub fn update_actuator_volumes(&mut self, actuator: &T) { + self.total_actuators_consumed_volume += actuator.used_volume(); + self.total_actuators_returned_volume += actuator.reservoir_return(); + } + + /// Returns the max flow that can be output from reservoir in dt time + fn get_usable_reservoir_flow(&self, amount: VolumeRate, delta_time: Time) -> VolumeRate { + let mut drawn = amount; + + let max_flow = self.reservoir_volume / delta_time; + if amount > max_flow { + drawn = max_flow; + } + drawn + } + + /// Method to update pressure of a loop. The more delta volume is added, the more pressure rises + /// directly from bulk modulus equation + fn delta_pressure_from_delta_volume(&self, delta_vol: Volume) -> Pressure { + return delta_vol / self.high_pressure_volume * self.fluid.bulk_mod(); + } + + /// Gives the exact volume of fluid needed to get to any target_press pressure + fn vol_to_target(&self, target_press: Pressure) -> Volume { + (target_press - self.loop_pressure) * (self.high_pressure_volume) / self.fluid.bulk_mod() + } + + pub fn is_fire_shutoff_valve_opened(&self) -> bool { + self.fire_shutoff_valve_opened + } + + fn update_ptu_flows( + &mut self, + context: &UpdateContext, + ptus: Vec<&PowerTransferUnit>, + delta_vol: &mut Volume, + reservoir_return: &mut Volume, + ) { + let mut ptu_act = false; + for ptu in ptus { + let actual_flow; + if self.connected_to_ptu_left_side { + if ptu.is_active_left || ptu.is_active_right { + ptu_act = true; + } + if ptu.flow_to_left > VolumeRate::new::(0.0) { + // We are left side of PTU and positive flow so we receive flow using own reservoir + actual_flow = + self.get_usable_reservoir_flow(ptu.flow_to_left, context.delta_as_time()); + self.reservoir_volume -= actual_flow * context.delta_as_time(); + } else { + // We are using own flow to power right side so we send that back + // to our own reservoir + actual_flow = ptu.flow_to_left; + *reservoir_return -= actual_flow * context.delta_as_time(); + } + *delta_vol += actual_flow * context.delta_as_time(); + } else if self.connected_to_ptu_right_side { + if ptu.is_active_left || ptu.is_active_right { + ptu_act = true; + } + if ptu.flow_to_right > VolumeRate::new::(0.0) { + // We are right side of PTU and positive flow so we receive flow using own reservoir + actual_flow = + self.get_usable_reservoir_flow(ptu.flow_to_right, context.delta_as_time()); + self.reservoir_volume -= actual_flow * context.delta_as_time(); + } else { + // We are using own flow to power left side so we send that back + // to our own reservoir + actual_flow = ptu.flow_to_right; + *reservoir_return -= actual_flow * context.delta_as_time(); + } + *delta_vol += actual_flow * context.delta_as_time(); + } + } + self.ptu_active = ptu_act; + } + + pub fn update( + &mut self, + context: &UpdateContext, + electric_pumps: Vec<&ElectricPump>, + engine_driven_pumps: Vec<&EngineDrivenPump>, + ram_air_pumps: Vec<&RamAirTurbine>, + ptus: Vec<&PowerTransferUnit>, + controller: &T, + ) { + self.fire_shutoff_valve_opened = controller.should_open_fire_shutoff_valve(); + + let mut delta_vol_max = Volume::new::(0.); + let mut delta_vol_min = Volume::new::(0.); + let mut reservoir_return = Volume::new::(0.); + let mut delta_vol = Volume::new::(0.); + + if self.fire_shutoff_valve_opened { + for p in engine_driven_pumps { + delta_vol_max += p.delta_vol_max(); + delta_vol_min += p.delta_vol_min(); + } + } + for p in electric_pumps { + delta_vol_max += p.delta_vol_max(); + delta_vol_min += p.delta_vol_min(); + } + for p in ram_air_pumps { + delta_vol_max += p.delta_vol_max(); + delta_vol_min += p.delta_vol_min(); + } + + // Storing max pump capacity available. for now used in PTU model to limit it's input flow + self.current_max_flow = delta_vol_max / context.delta_as_time(); + + // Static leaks + // TODO: separate static leaks per zone of high pressure or actuator + // TODO: Use external pressure and/or reservoir pressure instead of 14.7 psi default + let static_leaks_vol = Volume::new::( + Self::STATIC_LEAK_FLOW_GALLON_PER_SECOND + * context.delta_as_secs_f64() + * (self.loop_pressure.get::() - 14.7) + / 3000.0, + ); + + // Draw delta_vol from reservoir + delta_vol -= static_leaks_vol; + reservoir_return += static_leaks_vol; + + // Updates current delta_vol and reservoir return quantity based on current ptu flows + self.update_ptu_flows(context, ptus, &mut delta_vol, &mut reservoir_return); + + // Updates current accumulator state and updates loop delta_vol + self.accumulator + .update(context, &mut delta_vol, self.loop_pressure); + + // Priming the loop if not filled in yet + // TODO bug, ptu can't prime the loop as it is not providing flow through delta_vol_max + if self.loop_volume < self.max_loop_volume { + let difference = self.max_loop_volume - self.loop_volume; + let available_fluid_vol = self.reservoir_volume.min(delta_vol_max); + let delta_loop_vol = available_fluid_vol.min(difference); + // TODO check if we cross the deltaVolMin? + delta_vol_max -= delta_loop_vol; + self.loop_volume += delta_loop_vol; + self.reservoir_volume -= delta_loop_vol; + } + + // Actuators effect is updated here, we get their accumulated consumptions and returns, then reset local accumulators for next iteration + reservoir_return += self.total_actuators_returned_volume; + delta_vol -= self.total_actuators_consumed_volume.abs(); + self.total_actuators_consumed_volume = Volume::new::(0.); + self.total_actuators_returned_volume = Volume::new::(0.); + + // How much we need to reach target of 3000? + let mut volume_needed_to_reach_pressure_target = + self.vol_to_target(Pressure::new::(3000.0)); + // Actually we need this PLUS what is used by consumers. + volume_needed_to_reach_pressure_target -= delta_vol; + + // Now computing what we will actually use from flow providers limited by + // their min and max flows and reservoir availability + let actual_volume_added_to_pressurise = self + .reservoir_volume + .min(delta_vol_min.max(delta_vol_max.min(volume_needed_to_reach_pressure_target))); + delta_vol += actual_volume_added_to_pressurise; + + // Update reservoir + // %limit to 0 min? for case of negative added? + self.reservoir_volume -= actual_volume_added_to_pressurise; + self.reservoir_volume += reservoir_return; + + // Update Volumes + self.loop_volume += delta_vol; + // Low pass filter on final delta vol to help with stability and final flow noise + delta_vol = Self::DELTA_VOL_LOW_PASS_FILTER * delta_vol + + (1. - Self::DELTA_VOL_LOW_PASS_FILTER) * self.current_delta_vol; + + // Loop Pressure update From Bulk modulus + let press_delta = self.delta_pressure_from_delta_volume(delta_vol); + self.loop_pressure += press_delta; + // Forcing a min pressure + self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); + + self.current_delta_vol = delta_vol; + self.current_flow = delta_vol / context.delta_as_time(); + + if self.loop_pressure <= self.min_pressure_pressurised_lo_hyst { + self.is_pressurised = false; + } else if self.loop_pressure >= self.min_pressure_pressurised_hi_hyst { + self.is_pressurised = true; + } + } + + pub fn is_pressurised(&self) -> bool { + self.is_pressurised + } +} +impl SimulationElement for HydraulicLoop { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.pressure_id, self.pressure().get::()); + writer.write_f64(&self.reservoir_id, self.reservoir_volume().get::()); + if self.has_fire_valve { + writer.write_bool(&self.fire_valve_id, self.is_fire_shutoff_valve_opened()); + } + } +} + +pub trait PumpController { + fn should_pressurise(&self) -> bool; +} + +pub struct Pump { + delta_vol_max: Volume, + delta_vol_min: Volume, + current_displacement: Volume, + press_breakpoints: [f64; 9], + displacement_carac: [f64; 9], + // Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic + displacement_dynamic: f64, +} +impl Pump { + fn new( + press_breakpoints: [f64; 9], + displacement_carac: [f64; 9], + displacement_dynamic: f64, + ) -> Self { + Self { + delta_vol_max: Volume::new::(0.), + delta_vol_min: Volume::new::(0.), + current_displacement: Volume::new::(0.), + press_breakpoints, + displacement_carac, + displacement_dynamic, + } + } + + fn update( + &mut self, + context: &UpdateContext, + line: &HydraulicLoop, + rpm: f64, + controller: &T, + ) { + let theoretical_displacement = self.calculate_displacement(line.pressure(), controller); + + // Actual displacement is the calculated one with a low pass filter applied to mimic displacement transients dynamic + self.current_displacement = (1.0 - self.displacement_dynamic) * self.current_displacement + + self.displacement_dynamic * theoretical_displacement; + + let flow = Self::calculate_flow(rpm, self.current_displacement) + .max(VolumeRate::new::(0.)); + + self.delta_vol_max = flow * context.delta_as_time(); + self.delta_vol_min = Volume::new::(0.0); + } + + fn calculate_displacement( + &self, + pressure: Pressure, + controller: &T, + ) -> Volume { + if controller.should_pressurise() { + return Volume::new::(interpolation( + &self.press_breakpoints, + &self.displacement_carac, + pressure.get::(), + )); + } + Volume::new::(0.) + } + + fn calculate_flow(rpm: f64, displacement: Volume) -> VolumeRate { + VolumeRate::new::(rpm * displacement.get::() / 231.0 / 60.0) + } +} +impl PressureSource for Pump { + fn delta_vol_max(&self) -> Volume { + self.delta_vol_max + } + + fn delta_vol_min(&self) -> Volume { + self.delta_vol_min + } +} + +pub struct ElectricPump { + active_id: String, + + is_active: bool, + rpm: f64, + pump: Pump, +} +impl ElectricPump { + const SPOOLUP_TIME: f64 = 1.; + const SPOOLDOWN_TIME: f64 = 3.0; + const NOMINAL_SPEED: f64 = 7600.0; + const DISPLACEMENT_BREAKPTS: [f64; 9] = [ + 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, + ]; + const DISPLACEMENT_MAP: [f64; 9] = [0.263, 0.263, 0.263, 0.263, 0.263, 0.263, 0.163, 0.0, 0.0]; + // 1 == No filtering + const DISPLACEMENT_DYNAMICS: f64 = 1.0; + + pub fn new(id: &str) -> Self { + Self { + active_id: format!("HYD_{}_EPUMP_ACTIVE", id), + is_active: false, + rpm: 0., + pump: Pump::new( + Self::DISPLACEMENT_BREAKPTS, + Self::DISPLACEMENT_MAP, + Self::DISPLACEMENT_DYNAMICS, + ), + } + } + + pub fn rpm(&self) -> f64 { + self.rpm + } + + pub fn update( + &mut self, + context: &UpdateContext, + line: &HydraulicLoop, + controller: &T, + ) { + // TODO Simulate speed of pump depending on pump load (flow?/ current?) + // Pump startup/shutdown process + if self.is_active && self.rpm < Self::NOMINAL_SPEED { + self.rpm += (Self::NOMINAL_SPEED / Self::SPOOLUP_TIME) * context.delta_as_secs_f64(); + } else if !self.is_active && self.rpm > 0.0 { + self.rpm -= (Self::NOMINAL_SPEED / Self::SPOOLDOWN_TIME) * context.delta_as_secs_f64(); + } + + // Limiting min and max speed + self.rpm = self.rpm.min(Self::NOMINAL_SPEED).max(0.0); + + self.pump.update(context, line, self.rpm, controller); + self.is_active = controller.should_pressurise(); + } +} +impl PressureSource for ElectricPump { + fn delta_vol_max(&self) -> Volume { + self.pump.delta_vol_max() + } + fn delta_vol_min(&self) -> Volume { + self.pump.delta_vol_min() + } +} +impl SimulationElement for ElectricPump { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_id, self.is_active); + } +} + +pub struct EngineDrivenPump { + active_id: String, + + is_active: bool, + pump: Pump, +} +impl EngineDrivenPump { + const DISPLACEMENT_BREAKPTS: [f64; 9] = [ + 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3020.0, 3500.0, + ]; + const DISPLACEMENT_MAP: [f64; 9] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.9, 0.0, 0.0]; + + // 0.1 == 90% filtering on max displacement transient + const DISPLACEMENT_DYNAMICS: f64 = 0.95; + + pub fn new(id: &str) -> Self { + Self { + active_id: format!("HYD_{}_EDPUMP_ACTIVE", id), + is_active: false, + pump: Pump::new( + Self::DISPLACEMENT_BREAKPTS, + Self::DISPLACEMENT_MAP, + Self::DISPLACEMENT_DYNAMICS, + ), + } + } + + pub fn update( + &mut self, + context: &UpdateContext, + line: &HydraulicLoop, + pump_rpm: f64, + controller: &T, + ) { + self.pump.update(context, line, pump_rpm, controller); + self.is_active = controller.should_pressurise(); + } +} +impl PressureSource for EngineDrivenPump { + fn delta_vol_min(&self) -> Volume { + self.pump.delta_vol_min() + } + fn delta_vol_max(&self) -> Volume { + self.pump.delta_vol_max() + } +} +impl SimulationElement for EngineDrivenPump { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_id, self.is_active); + } +} + +struct WindTurbine { + position: f64, + speed: f64, + acceleration: f64, + rpm: f64, + torque_sum: f64, +} +impl WindTurbine { + // Low speed special calculation threshold. Under that value we compute resistant torque depending on pump angle and displacement. + const LOW_SPEED_PHYSICS_ACTIVATION: f64 = 50.; + const STOWED_ANGLE: f64 = std::f64::consts::PI / 2.; + const PROPELLER_INERTIA: f64 = 2.; + const RPM_GOVERNOR_BREAKPTS: [f64; 9] = [ + 0.0, 1000., 3000.0, 4000.0, 4800.0, 5800.0, 6250.0, 9000.0, 15000.0, + ]; + const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 1., 1., 1.]; + + fn new() -> Self { + Self { + position: Self::STOWED_ANGLE, + speed: 0., + acceleration: 0., + rpm: 0., + torque_sum: 0., + } + } + + fn rpm(&self) -> f64 { + self.rpm + } + + fn update_generated_torque(&mut self, indicated_speed: &Velocity, stow_pos: f64) { + let cur_aplha = interpolation( + &Self::RPM_GOVERNOR_BREAKPTS, + &Self::PROP_ALPHA_MAP, + self.rpm, + ); + + // Simple model. stow pos sin simulates the angle of the blades vs wind while deploying + let air_speed_torque = cur_aplha.to_radians().sin() + * (indicated_speed.get::() * indicated_speed.get::() / 100.) + * 0.5 + * (std::f64::consts::PI / 2. * stow_pos).sin(); + self.torque_sum += air_speed_torque; + } + + fn update_friction_torque(&mut self, displacement_ratio: f64) { + let mut pump_torque = 0.; + if self.rpm < Self::LOW_SPEED_PHYSICS_ACTIVATION { + pump_torque += (self.position * 4.).cos() * displacement_ratio.max(0.35) * 35.; + pump_torque += -self.speed * 15.; + } else { + pump_torque += displacement_ratio.max(0.35) * 1. * -self.speed; + } + pump_torque -= self.speed * 0.05; + // Static air drag of the propeller + self.torque_sum += pump_torque; + } + + fn update_physics(&mut self, delta_time: &Duration) { + self.acceleration = self.torque_sum / Self::PROPELLER_INERTIA; + self.speed += self.acceleration * delta_time.as_secs_f64(); + self.position += self.speed * delta_time.as_secs_f64(); + + // rad/s to RPM + self.rpm = self.speed * 30. / std::f64::consts::PI; + + // Reset torque accumulator at end of update + self.torque_sum = 0.; + } + + fn update( + &mut self, + delta_time: &Duration, + indicated_speed: &Velocity, + stow_pos: f64, + displacement_ratio: f64, + ) { + if stow_pos > 0.1 { + // Do not update anything on the propeller if still stowed + self.update_generated_torque(indicated_speed, stow_pos); + self.update_friction_torque(displacement_ratio); + self.update_physics(delta_time); + } + } +} +impl SimulationElement for WindTurbine { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64("HYD_RAT_RPM", self.rpm()); + } +} +impl Default for WindTurbine { + fn default() -> Self { + Self::new() + } +} + +struct AlwaysPressurisePumpController {} +impl AlwaysPressurisePumpController { + fn new() -> Self { + Self {} + } +} +impl PumpController for AlwaysPressurisePumpController { + fn should_pressurise(&self) -> bool { + true + } +} +impl Default for AlwaysPressurisePumpController { + fn default() -> Self { + Self::new() + } +} + +pub trait RamAirTurbineController { + fn should_deploy(&self) -> bool; +} + +pub struct RamAirTurbine { + deployment_commanded: bool, + pump: Pump, + pump_controller: AlwaysPressurisePumpController, + wind_turbine: WindTurbine, + position: f64, + max_displacement: f64, +} +impl RamAirTurbine { + const DISPLACEMENT_BREAKPTS: [f64; 9] = [ + 0.0, 500.0, 1000.0, 1500.0, 2100.0, 2300.0, 2600.0, 3050.0, 3500.0, + ]; + const DISPLACEMENT_MAP: [f64; 9] = [1.15, 1.15, 1.15, 1.15, 1.15, 0.5, 0.0, 0.0, 0.0]; + + // 1 == no filtering. 0.1 == 90% filtering. 0.2==80%... !!Warning, filter frequency is time delta dependant. + const DISPLACEMENT_DYNAMICS: f64 = 0.2; + + // Speed to go from 0 to 1 stow position per sec. 1 means full deploying in 1s + const STOWING_SPEED: f64 = 1.; + + pub fn new() -> Self { + let mut max_disp = 0.; + for v in Self::DISPLACEMENT_MAP.iter() { + if v > &max_disp { + max_disp = *v; + } + } + + Self { + deployment_commanded: false, + pump: Pump::new( + Self::DISPLACEMENT_BREAKPTS, + Self::DISPLACEMENT_MAP, + Self::DISPLACEMENT_DYNAMICS, + ), + pump_controller: AlwaysPressurisePumpController::new(), + wind_turbine: WindTurbine::new(), + position: 0., + max_displacement: max_disp, + } + } + + pub fn update( + &mut self, + context: &UpdateContext, + line: &HydraulicLoop, + controller: &T, + ) { + self.deployment_commanded = controller.should_deploy(); + + self.pump.update( + context, + line, + self.wind_turbine.rpm(), + &self.pump_controller, + ); + + // Now forcing min to max to force a true real time regulation. + // TODO: handle this properly by calculating who produced what volume at end of hyd loop update + self.pump.delta_vol_min = self.pump.delta_vol_max; + } + + pub fn update_physics(&mut self, delta_time: &Duration, indicated_airspeed: &Velocity) { + // Calculate the ratio of current displacement vs max displacement as an image of the load of the pump + let displacement_ratio = self.delta_vol_max().get::() / self.max_displacement; + self.wind_turbine.update( + &delta_time, + &indicated_airspeed, + self.position, + displacement_ratio, + ); + } + + pub fn update_position(&mut self, delta_time: &Duration) { + if self.deployment_commanded { + self.position += delta_time.as_secs_f64() * Self::STOWING_SPEED; + + // Finally limiting pos in [0:1] range + if self.position < 0. { + self.position = 0.; + } else if self.position > 1. { + self.position = 1.; + } + } + } +} +impl PressureSource for RamAirTurbine { + fn delta_vol_max(&self) -> Volume { + self.pump.delta_vol_max() + } + + fn delta_vol_min(&self) -> Volume { + self.pump.delta_vol_min() + } +} +impl SimulationElement for RamAirTurbine { + fn accept(&mut self, visitor: &mut T) { + self.wind_turbine.accept(visitor); + + visitor.visit(self); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64("HYD_RAT_STOW_POSITION", self.position); + } +} +impl Default for RamAirTurbine { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use crate::simulation::UpdateContext; + use uom::si::{ + acceleration::foot_per_second_squared, + f64::*, + length::foot, + pressure::{pascal, psi}, + thermodynamic_temperature::degree_celsius, + volume::gallon, + }; + + struct TestHydraulicLoopController { + should_open_fire_shutoff_valve: bool, + } + impl TestHydraulicLoopController { + fn commanding_open_fire_shutoff_valve() -> Self { + Self { + should_open_fire_shutoff_valve: true, + } + } + } + impl HydraulicLoopController for TestHydraulicLoopController { + fn should_open_fire_shutoff_valve(&self) -> bool { + self.should_open_fire_shutoff_valve + } + } + + struct TestPumpController { + should_pressurise: bool, + } + impl TestPumpController { + fn commanding_pressurise() -> Self { + Self { + should_pressurise: true, + } + } + } + impl PumpController for TestPumpController { + fn should_pressurise(&self) -> bool { + self.should_pressurise + } + } + + struct TestRamAirTurbineController { + should_deploy: bool, + } + impl TestRamAirTurbineController { + fn new() -> Self { + Self { + should_deploy: false, + } + } + + fn command_deployment(&mut self) { + self.should_deploy = true; + } + } + impl RamAirTurbineController for TestRamAirTurbineController { + fn should_deploy(&self) -> bool { + self.should_deploy + } + } + + use super::*; + #[test] + /// Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s + fn blue_loop_rat_deploy_simulation() { + let mut rat = RamAirTurbine::new(); + let mut rat_controller = TestRamAirTurbineController::new(); + let mut blue_loop = hydraulic_loop("BLUE"); + let blue_loop_controller = + TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); + + let timestep = 0.05; + let context = context(Duration::from_secs_f64(timestep)); + let mut indicated_airspeed = context.indicated_airspeed(); + + let mut time = 0.0; + for x in 0..1500 { + rat.update_position(&context.delta()); + if time >= 10. && time < 10. + timestep { + println!("ASSERT RAT STOWED"); + assert!(blue_loop.loop_pressure <= Pressure::new::(50.0)); + rat.deployment_commanded = false; + assert!(rat.position == 0.); + } + + if time >= 20. && time < 20. + timestep { + println!("ASSERT RAT STOWED STILL NO PRESS"); + assert!(blue_loop.loop_pressure <= Pressure::new::(50.0)); + rat_controller.command_deployment(); + } + + if time >= 30. && time < 30. + timestep { + println!("ASSERT RAT OUT AND SPINING"); + assert!(blue_loop.loop_pressure >= Pressure::new::(2000.0)); + assert!(rat.position >= 0.999); + assert!(rat.wind_turbine.rpm >= 1000.); + } + if time >= 60. && time < 60. + timestep { + println!("ASSERT RAT AT SPEED"); + assert!(blue_loop.loop_pressure >= Pressure::new::(2000.0)); + assert!(rat.wind_turbine.rpm >= 4500.); + } + + if time >= 70. && time < 70. + timestep { + println!("STOPING THE PLANE"); + indicated_airspeed = Velocity::new::(0.); + } + + if time >= 120. && time < 120. + timestep { + println!("ASSERT RAT SLOWED DOWN"); + assert!(rat.wind_turbine.rpm <= 2500.); + } + + rat.update_physics(&context.delta(), &indicated_airspeed); + rat.update(&context, &blue_loop, &rat_controller); + blue_loop.update( + &context, + Vec::new(), + Vec::new(), + vec![&rat], + Vec::new(), + &blue_loop_controller, + ); + if x % 20 == 0 { + println!("Iteration {} Time {}", x, time); + println!("-------------------------------------------"); + println!("---PSI: {}", blue_loop.loop_pressure.get::()); + println!("---RAT stow pos: {}", rat.position); + println!("---RAT RPM: {}", rat.wind_turbine.rpm); + println!("---RAT volMax: {}", rat.delta_vol_max().get::()); + println!( + "--------Reservoir Volume (g): {}", + blue_loop.reservoir_volume.get::() + ); + println!( + "--------Loop Volume (g): {}", + blue_loop.loop_volume.get::() + ); + } + time += timestep; + } + } + + fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { + match loop_color { + "GREEN" => HydraulicLoop::new( + loop_color, + true, + false, + Volume::new::(26.41), + Volume::new::(26.41), + Volume::new::(10.0), + Volume::new::(3.83), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), + ), + "YELLOW" => HydraulicLoop::new( + loop_color, + false, + true, + Volume::new::(10.2), + Volume::new::(10.2), + Volume::new::(8.0), + Volume::new::(3.3), + Fluid::new(Pressure::new::(1450000000.0)), + true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), + ), + _ => HydraulicLoop::new( + loop_color, + false, + false, + Volume::new::(15.85), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.5), + Fluid::new(Pressure::new::(1450000000.0)), + false, + Pressure::new::(1450.0), + Pressure::new::(1750.0), + ), + } + } + + fn engine_driven_pump() -> EngineDrivenPump { + EngineDrivenPump::new("DEFAULT") + } + + fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + ) + } + + #[cfg(test)] + mod edp_tests { + use super::*; + + #[test] + fn starts_inactive() { + assert!(!engine_driven_pump().is_active); + } + + #[test] + fn zero_flow_above_3000_psi_after_25ms() { + let pump_rpm = 3300.; + let pressure = Pressure::new::(3100.); + let context = context(Duration::from_millis(25)); + let displacement = Volume::new::(0.); + assert!(delta_vol_equality_check( + pump_rpm, + displacement, + pressure, + &context + )) + } + + fn delta_vol_equality_check( + pump_rpm: f64, + displacement: Volume, + pressure: Pressure, + context: &UpdateContext, + ) -> bool { + let actual = get_edp_actual_delta_vol_when(pump_rpm, pressure, context); + let predicted = get_edp_predicted_delta_vol_when(pump_rpm, displacement, context); + println!("Actual: {}", actual.get::()); + println!("Predicted: {}", predicted.get::()); + actual == predicted + } + + fn get_edp_actual_delta_vol_when( + pump_rpm: f64, + pressure: Pressure, + context: &UpdateContext, + ) -> Volume { + let mut edp = engine_driven_pump(); + let mut line = hydraulic_loop("GREEN"); + + let engine_driven_pump_controller = TestPumpController::commanding_pressurise(); + line.loop_pressure = pressure; + edp.update( + &context.with_delta(Duration::from_secs(1)), + &line, + pump_rpm, + &engine_driven_pump_controller, + ); // Update 10 times to stabilize displacement + + edp.update(context, &line, pump_rpm, &engine_driven_pump_controller); + edp.delta_vol_max() + } + + fn get_edp_predicted_delta_vol_when( + pump_rpm: f64, + displacement: Volume, + context: &UpdateContext, + ) -> Volume { + let expected_flow = Pump::calculate_flow(pump_rpm, displacement); + expected_flow * context.delta_as_time() + } + } +} diff --git a/src/systems/systems/src/hydraulic/study/Docs/Knowledge_Data_Base.xlsx b/src/systems/systems/src/hydraulic/study/Docs/Knowledge_Data_Base.xlsx new file mode 100644 index 00000000000..7087c0f4b6c Binary files /dev/null and b/src/systems/systems/src/hydraulic/study/Docs/Knowledge_Data_Base.xlsx differ diff --git a/src/systems/systems/src/hydraulic/study/test_Hy_CompConcept.m b/src/systems/systems/src/hydraulic/study/test_Hy_CompConcept.m new file mode 100644 index 00000000000..f8147a9fb12 --- /dev/null +++ b/src/systems/systems/src/hydraulic/study/test_Hy_CompConcept.m @@ -0,0 +1,486 @@ + +actuator=[]; + +%GREEN LOOP%% +loopG.bulkModulus=1450000000; %Bulk for fluid NSA307110 +loopG.length=10; +loopG.volume=26.41; +loopG.maxVolume=26.41; +loopG.maxVolumeHighPressureSide=8; %Considering 10gal is the high pressure volume TODO get realistic value +loopG.res=3.83; % 14.5L +loopG.press=14.7; +loopG.delta_vol=0; + +loopG.accumulator_fluid_volume=0; +loopG.ACCUMULATOR_MAX_VOLUME=0.264; %gallons +loopG.ACCUMULATOR_GAS_NRT=128.26; +loopG.ACCUMULATOR_GAS_PRECHARGE=1885; +loopG.accumulator_gas_pressure=loopG.ACCUMULATOR_GAS_PRECHARGE; +loopG.accumulator_gas_volume=loopG.ACCUMULATOR_MAX_VOLUME; +loopG.accumulator_DeltaPressBreakpoints= [0 5 10 50 100 200 500 1000 10000]; +loopG.accumulator_DeltaPressFlowCarac =[0 0.005 0.008 0.01 0.02 0.08 0.15 0.35 0.5]; +loopG.isLeft=1; %%Connected to left side of a PTU +%END GREEN% + +%YELLOW LOOP%% +loopY.bulkModulus=1450000000;%Bulk for fluid NSA307110 +loopY.length=10; +loopY.volume=10.2; +loopY.maxVolume=10.2; +loopY.maxVolumeHighPressureSide=8; +loopY.res=3.3; % +loopY.press=14.7; +loopY.delta_vol=0; + +loopY.accumulator_fluid_volume=0; +loopY.ACCUMULATOR_MAX_VOLUME=0.264; %gallons +loopY.ACCUMULATOR_GAS_NRT=128.26; +loopY.ACCUMULATOR_GAS_PRECHARGE=1885; +loopY.accumulator_gas_pressure=loopY.ACCUMULATOR_GAS_PRECHARGE; +loopY.accumulator_gas_volume=loopY.ACCUMULATOR_MAX_VOLUME; +loopY.accumulator_DeltaPressBreakpoints= [0 5 10 50 100 200 500 1000 10000]; +loopY.accumulator_DeltaPressFlowCarac =[0 0.005 0.008 0.01 0.02 0.08 0.15 0.35 0.5]; +loopY.isLeft=0;%%Connected to right side of a PTU +%YELLOW END% + +PTU.flowToLeftLoop=0; +PTU.flowToRightLoop=0; +PTU.resReturnLeftLoop=0; +PTU.resReturnRightLoop=0; +PTU.isActiveRight=0; +PTU.isActiveLeft=0; +PTU.isEnabled=0; +% Yellow to Green +% /// --------------- +% /// 34 GPM (130 L/min) from Yellow system +% /// 24 GPM (90 L/min) to Green system +% /// Maintains constant pressure near 3000PSI in green +% /// +% /// Green to Yellow +% /// --------------- +% /// 16 GPM (60 L/min) from Green system +% /// 13 GPM (50 L/min) to Yellow system +% /// Maintains constant pressure near 3000PSI in yellow + + +pumpED1.rpm=0; +pumpED1.flowRate=0; +pumpED1.max_displacement=2.4; +pumpED1.delta_vol=0; +pumpED1.pressBreakpoints=[0 500 1000 1500 2800 2900 3000 3050 3500]; +pumpED1.displacementCarac=[2.4 2.4 2.4 2.4 2.4 2.4 2.0 0 0 ]; +pumpED1.minVol=0; +pumpED1.maxVol=0; + + +pumpED2.rpm=0; +pumpED2.flowRate=0; +pumpED2.max_displacement=2.4; +pumpED2.delta_vol=0; +pumpED2.pressBreakpoints=[0 500 1000 1500 2800 2900 3000 3050 3500]; +pumpED2.displacementCarac=[2.4 2.4 2.4 2.4 2.4 2.4 2.0 0 0 ]; +pumpED2.minVol=0; +pumpED2.maxVol=0; + +dt=0.1; + +displacementTab=[]; + +pressTabG=[]; +deltaVolTabG=[]; +volTabG=[]; +loopFlowG=[]; +resTabG=[]; +accFluidVolumeTabG=[]; +accGasVolumeTabG=[]; +accGasPressTabG=[]; + +pressTabY=[]; +deltaVolTabY=[]; +volTabY=[]; +loopFlowY=[]; +resTabY=[]; +accFluidVolumeTabY=[]; +accGasVolumeTabY=[]; +accGasPressTabY=[]; + +PTU_Flow_GToY_tab=[]; +PTU_Flow_YToG_tab=[]; +PTU_DeltaP_tab=[]; +PTU_isActive_tab=[]; + + +close all; + +lastT=0; +maxTime=100; + + + + +%%%%%STARTING SCRIPT%%%%%%%%% +for t=0:dt:maxTime + + if t==0 + pumpED2.rpm=0; + pumpED1.rpm=0; + end + + if t==1 + pumpED2.rpm=4000; + pumpED1.rpm=4000; + %loop.press =1; + %loop.volume=loop.volume*0.25; + %pumpED2.rpm=0; + end + + if t==5 + pumpED1.rpm=0; + PTU.isEnabled = 1; + end + + + PTU=updatePTU(PTU,dt, loopG, loopY) ; + + [pumpED1,loopG,actuator]= updateL(dt,pumpED1,PTU,loopG,actuator); + [pumpED2,loopY,actuator]= updateL(dt,pumpED2,PTU,loopY,actuator); + +% physicsFixedStep=0.05; +% numOfPasses=dt/physicsFixedStep; +% +% for passNum=1:numOfPasses +% gear=updateActuator(gear,physicsFixedStep); +% end + + + %MATLAB DISPLAY ONLY + pressTabG(end+1)=loopG.press; + deltaVolTabG(end+1)=loopG.delta_vol; + volTabG(end+1)=loopG.volume; + loopFlowG(end+1)=loopG.delta_vol/dt; + resTabG(end+1)=loopG.res; + + accFluidVolumeTabG(end+1)=loopG.accumulator_fluid_volume; + accGasVolumeTabG(end+1) = loopG.accumulator_gas_volume ; + accGasPressTabG(end+1) = loopG.accumulator_gas_pressure ; + + pressTabY(end+1)=loopY.press; + deltaVolTabY(end+1)=loopY.delta_vol; + volTabY(end+1)=loopY.volume; + loopFlowY(end+1)=loopY.delta_vol/dt; + resTabY(end+1)=loopY.res; + + accFluidVolumeTabY(end+1)=loopY.accumulator_fluid_volume; + accGasVolumeTabY(end+1) = loopY.accumulator_gas_volume ; + accGasPressTabY(end+1) = loopY.accumulator_gas_pressure ; + + PTU_Flow_GToY_tab(end+1)=PTU.flowToRightLoop; + PTU_Flow_YToG_tab(end+1)=PTU.flowToLeftLoop; + PTU_DeltaP_tab(end+1)=loopG.press-loopY.press; + PTU_isActive_tab(end+1)=PTU.isActiveLeft||PTU.isActiveRight; +end + +t=0:dt:maxTime ; +%figure; plot(angleTab,displacementTab); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +figure; +ax_PTU_flow=subplot(5,1,1); +hold(ax_PTU_flow,'on');grid; +title('PTUflow'); +ax_deltaP=subplot(5,1,2); +hold(ax_deltaP,'on');grid; +title('DeltaP'); +ax_active=subplot(5,1,3); +hold(ax_active,'on');grid; +title('PTU active'); +ax_loopPress=subplot(5,1,4); +hold(ax_loopPress,'on');grid; +title('LoopPressures'); +ax_ResVol=subplot(5,1,5); +hold(ax_ResVol,'on');grid; +title('ResVol'); + +plot(ax_PTU_flow,t,PTU_Flow_GToY_tab,'color','red'); +plot(ax_PTU_flow,t,PTU_Flow_YToG_tab,'color','green'); + +plot(ax_deltaP,t,PTU_DeltaP_tab); + +plot(ax_active,t,PTU_isActive_tab); + +plot(ax_loopPress,t,pressTabG,'color','green'); +plot(ax_loopPress,t,pressTabY,'color','red'); + +plot(ax_ResVol,t,resTabG,'color','green'); +plot(ax_ResVol,t,resTabY,'color','red'); +linkaxes([ax_PTU_flow,ax_deltaP,ax_active,ax_loopPress,ax_ResVol],'x'); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +figure; +ax_Press=subplot(5,1,1); +hold(ax_Press,'on');grid; +title('Press'); +ax_deltaVol=subplot(5,1,2); +hold(ax_deltaVol,'on');grid; +title('DeltaVol'); +ax_Volume=subplot(5,1,3); +hold(ax_Volume,'on');grid; +title('Vol'); +ax_deltaFlow=subplot(5,1,4); +hold(ax_deltaFlow,'on');grid; +title('deltaFlow'); +ax_ResVol=subplot(5,1,5); +hold(ax_ResVol,'on');grid; +title('ResVol'); + + +plot(ax_Press,t,pressTabG,'color','green');hold on; +plot(ax_Press,t,pressTabY,'color','red'); + +plot(ax_deltaVol,t,deltaVolTabG,'color','green'); +plot(ax_deltaVol,t,deltaVolTabY,'color','red'); + +plot(ax_Volume,t,volTabG,'color','green'); +plot(ax_Volume,t,volTabY,'color','red'); + +plot(ax_deltaFlow,t,loopFlowG,'color','green'); +plot(ax_deltaFlow,t,loopFlowY,'color','red'); + +plot(ax_ResVol,t,resTabG,'color','green'); +plot(ax_ResVol,t,resTabY,'color','red'); + +linkaxes([ax_Press,ax_deltaVol,ax_Volume,ax_PumpVol,ax_ResVol],'x'); + + + +time=0:dt:maxTime ; +figure; +ax_accVol=subplot(2,1,1); +hold(ax_accVol,'on');grid; +title('Accumulator Volumes'); +ax_accPress=subplot(2,1,2); +hold(ax_accPress,'on');grid; +title('Accumulator pressure'); + +plot(ax_accVol,time,accFluidVolumeTabG); +plot(ax_accVol,time,accGasVolumeTabG); +legend(ax_accVol,{'Acc FluidVol' 'Acc Gas Vol'}); +plot(ax_accPress,time,accGasPressTabG); +plot(ax_accPress,time,pressTabG); +legend(ax_accPress,{'Acc Press' 'Loop press'}); +linkaxes([ax_accVol,ax_accPress],'x'); + + + +function [pump]=updateP_min_max(dt, pump, loop) + displacement = calculate_displacement(pump,loop); + flow_Gall_per_s = calculate_flow(pump.rpm, displacement); + delta_vol_gall = flow_Gall_per_s * dt; + + pump.maxVol=delta_vol_gall; + pump.minVol=0; %Min is 0 as it can cut off displacement to 0 for EDP +end + +function PTU=updatePTU(PTU,dt, loopLeft, loopRight) + + if PTU.isEnabled + deltaP=loopLeft.press-loopRight.press; + + %TODO: use maped characteristics for PTU? + %TODO Use variable displacement available on one side? + %TODO Handle RPM of ptu so transient are bit slower? + %TODO Handle it as a min/max flow producer + if PTU.isActiveLeft || deltaP>500 %Left sends flow to right + vr = min(16,loopLeft.press * 0.01133) / 60.0; + PTU.flowToLeftLoop= -vr; + PTU.flowToRightLoop= vr * 0.81; + + PTU.isActiveLeft=1; + elseif PTU.isActiveRight || deltaP<-500 %Right sends flow to left + vr = min(34,loopRight.press * 0.0245) / 60.0; + PTU.flowToLeftLoop = vr * 0.70; + PTU.flowToRightLoop= -vr; + + PTU.isActiveRight=1; + end + + if PTU.isActiveRight && loopLeft.press > 2950 || PTU.isActiveLeft && loopRight.press > 2950 ... + || PTU.isActiveRight && loopRight.press < 200 ... + || PTU.isActiveLeft && loopLeft.press < 200 + PTU.flowToLeftLoop=0; + PTU.flowToRightLoop=0; + PTU.isActiveRight=0; + PTU.isActiveLeft=0; + end + end + + end + +function disp=calculate_displacement(pump,loop) + disp=interp1(pump.pressBreakpoints,pump.displacementCarac,loop.press); +end + +function flow= calculate_flow(rpm, displacement) + flow= (rpm * displacement / 231.0 / 60.0); +end + + +function [pump,loop,aileron]= updateL(dt,pump,PTU,loop,aileron) + + %init + deltavol=0; + deltaMaxVol=0; + deltaMinVol=0; + %deltaVolConsumers=0; %%Total volume consumed this iteration + reservoirReturn=0; %%total volume back to res for that iteration + + + %FOR EACH PUMP getting max and min flow available. Will be used at end + %of iteration to fullfill if possible the regulation to 3000 nominal + %pressure + pump=updateP_min_max(dt,pump,loop); + + deltaMaxVol=deltaMaxVol+pump.maxVol; + deltaMinVol=deltaMinVol+pump.minVol; + %END FOREACH PUMP + + + + %Static leaks, random formula to depend on pressure + staticLeakVol=0.04*dt*(loop.press-14.7)/3000; + deltavol=deltavol-staticLeakVol; + %if !leakFailure + reservoirReturn=reservoirReturn+staticLeakVol; %Static leaks are back to reservoir unless failure case + %%%end static leaks + + %Adding ptu flows after pump + %TODO Handle it as a min/max flow producer if possible? + if loop.isLeft + if PTU.flowToLeftLoop > 0 + %were are left side of PTU and positive flow so we receive flow using own reservoir + actualFlow=min(loop.res/dt,PTU.flowToLeftLoop); + loop.res=loop.res-actualFlow* dt; + else + %we are using own flow to power right side so we send that back + %to our own reservoir + actualFlow=PTU.flowToLeftLoop; + reservoirReturn=reservoirReturn-actualFlow* dt; + end + deltavol=deltavol+actualFlow * dt; + %reservoirReturn=reservoirReturn+actualFlow; + else + if PTU.flowToRightLoop > 0 + %were are right side of PTU and positive flow so we receive flow using own reservoir + actualFlow=min(loop.res/dt,PTU.flowToRightLoop); + loop.res=loop.res-actualFlow* dt; + else + %we are using own flow to power left side so we send that back + %to our own reservoir + actualFlow=PTU.flowToRightLoop; + reservoirReturn=reservoirReturn-actualFlow* dt; + end + deltavol=deltavol+actualFlow* dt; + %reservoirReturn=reservoirReturn+actualFlow; + end + + + %Unprimed case + %Here we handle starting with air in the loop + if loop.volume < loop.maxVolume %TODO what to do if we are back under max volume and unprime the loop? + difference = loop.maxVolume - loop.volume; + availableFluidVol=min(loop.res,deltaMaxVol); + delta_loop_vol = min(availableFluidVol,difference); + deltaMaxVol = deltaMaxVol-delta_loop_vol; %TODO check if we cross the deltaVolMin? + loop.volume = loop.volume+ delta_loop_vol; + loop.res=loop.res-delta_loop_vol; + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%ACCUMULATOR%%%%%%%%%%%% + accumulatorDeltaPress = loop.accumulator_gas_pressure - loop.press; + flowVariation = interp1(loop.accumulator_DeltaPressBreakpoints,loop.accumulator_DeltaPressFlowCarac,abs(accumulatorDeltaPress)) ; + + %%TODO HANDLE OR CHECK IF RESERVOIR AVAILABILITY is OK + %%TODO check if accumulator can be used as a min/max flow producer to + %%avoid it being a consumer that might unsettle pressure + if ( accumulatorDeltaPress > 0 ) + volumeFromAcc = min(loop.accumulator_fluid_volume, flowVariation * dt ); + loop.accumulator_fluid_volume=loop.accumulator_fluid_volume-volumeFromAcc; + loop.accumulator_gas_volume=loop.accumulator_gas_volume+volumeFromAcc; + deltavol = deltavol + volumeFromAcc; + else + volumeToAcc = max(max(0,deltavol), flowVariation * dt ); + %volumeToAcc = flowVariation * dt ;%TODO handle if flow actually available: maybe using deltavolMAX + loop.accumulator_fluid_volume=loop.accumulator_fluid_volume+volumeToAcc; + loop.accumulator_gas_volume=loop.accumulator_gas_volume-volumeToAcc; + deltavol = deltavol - volumeToAcc; + end + + loop.accumulator_gas_pressure = (loop.ACCUMULATOR_GAS_PRECHARGE * loop.ACCUMULATOR_MAX_VOLUME) / (loop.ACCUMULATOR_MAX_VOLUME - loop.accumulator_fluid_volume); + %%%%%%%%%%%%%%%%%%%END ACCUMULATOR%%%%%%%%%%%%%%%% + + + + %%%%UPDATE ALL ACTUATORS OF THIS LOOP + used_fluidQty=0; %%total fluid used + pressUsedForForce=0; + + %FOR EACH MOVINGPART +% % % used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated*264.172; %264.172 is m^3 to gallons +% % % reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated*264.172; +% % % +% % % %Reseting vars for next loop: +% % % aileron.volumeToActuatorAccumulated=0; +% % % aileron.volumeToResAccumulated=0; +% % % %%%%% +% % % +% % % %Setting press usable by the actuator for this iteration +% % % aileron.loopPressAvailable = loop.press; +% % % +% % % %%%%%%%%End FOREACH%%%%%%%%%%%%%%% +% % % +% % % %Simuating the 3 gears by multiplying used quantities +% % % used_fluidQty=used_fluidQty*2.5; +% % % reservoirReturn=reservoirReturn*2.5; + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %Update pressure and vol from last used flow by actuators + deltavol=deltavol-used_fluidQty; + + + + %How much we need to reach target of 3000? + volume_needed_to_reach_pressure_target = vol_to_target(loop,3000); + + %Actually we need this PLUS what is used by consumers. + volume_needed_to_reach_pressure_target = volume_needed_to_reach_pressure_target - deltavol; + + %Now computing what we will actually use from flow providers limited by + %their min and max flows and reservoir availability + actual_volume_added_to_pressurise = min(loop.res,max(deltaMinVol,min(deltaMaxVol,volume_needed_to_reach_pressure_target))); + deltavol=deltavol+actual_volume_added_to_pressurise; + + %Loop Pressure update From Bulk modulus + loop.press = loop.press + deltaPress_from_deltaVolume(deltavol,loop); + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + %Update res + loop.res=loop.res-actual_volume_added_to_pressurise; %limit to 0 min? for case of negative added? + loop.res=loop.res+reservoirReturn; + + %Update Volumes + loop.delta_vol=deltavol; + loop.volume=loop.volume+deltavol; +end + + +function deltaPress = deltaPress_from_deltaVolume(deltaV,loop) + delta_vol_m3=deltaV * 0.00378541178; %Convert to m3 + deltaP_pascal=((delta_vol_m3) / (loop.maxVolumeHighPressureSide* 0.00378541178)) * loop.bulkModulus; + + deltaPress = deltaP_pascal*0.0001450377; +end + +function volume_needed_to_reach_pressure_target_gal = vol_to_target(loop,targetPress) + volume_needed_to_reach_pressure_target_m3 = (targetPress-loop.press)/0.0001450377 * (loop.maxVolumeHighPressureSide* 0.00378541178) / loop.bulkModulus; + volume_needed_to_reach_pressure_target_gal = volume_needed_to_reach_pressure_target_m3 / 0.00378541178; +end diff --git a/src/systems/systems/src/hydraulic/study/test_Hy_CompConcept_V2.m b/src/systems/systems/src/hydraulic/study/test_Hy_CompConcept_V2.m new file mode 100644 index 00000000000..7dad7588d67 --- /dev/null +++ b/src/systems/systems/src/hydraulic/study/test_Hy_CompConcept_V2.m @@ -0,0 +1,531 @@ + +actuator=[]; + +%GREEN LOOP%% +loopG.bulkModulus=1450000000; %Bulk for fluid NSA307110 +loopG.length=10; +loopG.volume=26.41; +loopG.maxVolume=26.41; +loopG.maxVolumeHighPressureSide=10; %Considering 10gal is the high pressure volume TODO get realistic value +loopG.res=3.83; % 14.5L +loopG.press=14.7; +loopG.delta_vol=0; + +loopG.accumulator_fluid_volume=0; +loopG.ACCUMULATOR_MAX_VOLUME=0.264; %gallons +loopG.ACCUMULATOR_GAS_NRT=128.26; +loopG.ACCUMULATOR_GAS_PRECHARGE=1885; +loopG.accumulator_gas_pressure=loopG.ACCUMULATOR_GAS_PRECHARGE; +loopG.accumulator_gas_volume=loopG.ACCUMULATOR_MAX_VOLUME; +loopG.accumulator_DeltaPressBreakpoints= [0 5 10 50 100 200 500 1000 10000]; +loopG.accumulator_DeltaPressFlowCarac =[0 0.005 0.008 0.01 0.02 0.08 0.15 0.35 0.5]; +loopG.isLeft=1; %%Connected to left side of a PTU +loopG.lastMaxFlow=0; +%END GREEN% + +%YELLOW LOOP%% +loopY.bulkModulus=1450000000;%Bulk for fluid NSA307110 +loopY.length=10; +loopY.volume=10.2; +loopY.maxVolume=10.2; +loopY.maxVolumeHighPressureSide=7; +loopY.res=3.3; % +loopY.press=14.7; +loopY.delta_vol=0; + +loopY.accumulator_fluid_volume=0; +loopY.ACCUMULATOR_MAX_VOLUME=0.264; %gallons +loopY.ACCUMULATOR_GAS_NRT=128.26; +loopY.ACCUMULATOR_GAS_PRECHARGE=1885; +loopY.accumulator_gas_pressure=loopY.ACCUMULATOR_GAS_PRECHARGE; +loopY.accumulator_gas_volume=loopY.ACCUMULATOR_MAX_VOLUME; +loopY.accumulator_DeltaPressBreakpoints= [0 5 10 50 100 200 500 1000 10000]; +loopY.accumulator_DeltaPressFlowCarac =[0 0.005 0.008 0.01 0.02 0.08 0.15 0.35 0.5]; +loopY.isLeft=0;%%Connected to right side of a PTU +loopY.lastMaxFlow=0; +%YELLOW END% + +PTU.flowToLeftLoop=0; +PTU.flowToRightLoop=0; +PTU.last_flow=0; +PTU.resReturnLeftLoop=0; +PTU.resReturnRightLoop=0; +PTU.isActiveRight=0; +PTU.isActiveLeft=0; +PTU.isEnabled=0; +PTU.last_left_press = 0; +PTU.last_right_press = 0; +PTU.last_press_rate = 0; +% Yellow to Green +% /// --------------- +% /// 34 GPM (130 L/min) from Yellow system +% /// 24 GPM (90 L/min) to Green system +% /// Maintains constant pressure near 3000PSI in green +% /// +% /// Green to Yellow +% /// --------------- +% /// 16 GPM (60 L/min) from Green system +% /// 13 GPM (50 L/min) to Yellow system +% /// Maintains constant pressure near 3000PSI in yellow + + +pumpED1.rpm=0; +pumpED1.flowRate=0; +pumpED1.delta_vol=0; +pumpED1.pressBreakpoints=[0 500 1000 1500 2800 2900 3000 3050 3500]; +pumpED1.displacementCarac=[2.4 2.4 2.4 2.4 2.4 2.4 2.0 0 0 ]; +pumpED1.minVol=0; +pumpED1.maxVol=0; + + +pumpED2.rpm=0; +pumpED2.flowRate=0; +pumpED2.delta_vol=0; +pumpED2.pressBreakpoints=[0 500 1000 1500 2800 2900 3000 3050 3500]; +pumpED2.displacementCarac=[2.4 2.4 2.4 2.4 2.4 2.4 2.0 0 0 ]; +pumpED2.minVol=0; +pumpED2.maxVol=0; + +epump.rpm=0; +epump.flowRate=0; +epump.delta_vol=0; +epump.pressBreakpoints= [0 1000 2250 2500 2750 2900 2925 3000 3050 3500]; +epump.displacementCarac=[0 0.263 0.263 0.224 0.195 0.195 0.125 0.120 0 0 ]; +epump.minVol=0; +epump.maxVol=0; +epump.rpmMax=7600; +epump.spooltime=4; +epump.isactive=0; + +dt=0.1; + +displacementTab=[]; + +pressTabG=[]; +deltaVolTabG=[]; +volTabG=[]; +loopFlowG=[]; +resTabG=[]; +accFluidVolumeTabG=[]; +accGasVolumeTabG=[]; +accGasPressTabG=[]; + +pressTabY=[]; +deltaVolTabY=[]; +volTabY=[]; +loopFlowY=[]; +resTabY=[]; +accFluidVolumeTabY=[]; +accGasVolumeTabY=[]; +accGasPressTabY=[]; + +PTU_Flow_GToY_tab=[]; +PTU_Flow_YToG_tab=[]; +PTU_DeltaP_tab=[]; +PTU_isActive_tab=[]; + + +close all; + +lastT=0; +maxTime=100; + + + + +%%%%%STARTING SCRIPT%%%%%%%%% +for t=0:dt:maxTime + + if t==1 + pumpED2.rpm=0; + %pumpED1.rpm=4000; + epump.rpm=epump.rpmMax; + %epump.rpm=0; + end + + if t==11 + % pumpED2.rpm=0000; + %pumpED1.rpm=0000; + PTU.isEnabled = 1; + %loop.press =1; + %loop.volume=loop.volume*0.25; + %pumpED2.rpm=0; + end + + if t==70 + pumpED1.rpm=4000; + PTU.isEnabled = 1; + end + + if t==80 + pumpED1.rpm=0000; + epump.rpm=0; + PTU.isEnabled = 1; + end + + + PTU=updatePTU(PTU,dt, loopG, loopY) ; + + [pumpED1,loopG,actuator]= updateL(dt,pumpED1,PTU,loopG,actuator); + [epump,loopY,actuator]= updateL(dt,epump,PTU,loopY,actuator); + +% physicsFixedStep=0.05; +% numOfPasses=dt/physicsFixedStep; +% +% for passNum=1:numOfPasses +% gear=updateActuator(gear,physicsFixedStep); +% end + + + %MATLAB DISPLAY ONLY + pressTabG(end+1)=loopG.press; + deltaVolTabG(end+1)=loopG.delta_vol; + volTabG(end+1)=loopG.volume; + loopFlowG(end+1)=loopG.delta_vol/dt; + resTabG(end+1)=loopG.res; + + accFluidVolumeTabG(end+1)=loopG.accumulator_fluid_volume; + accGasVolumeTabG(end+1) = loopG.accumulator_gas_volume ; + accGasPressTabG(end+1) = loopG.accumulator_gas_pressure ; + + pressTabY(end+1)=loopY.press; + deltaVolTabY(end+1)=loopY.delta_vol; + volTabY(end+1)=loopY.volume; + loopFlowY(end+1)=loopY.delta_vol/dt; + resTabY(end+1)=loopY.res; + + accFluidVolumeTabY(end+1)=loopY.accumulator_fluid_volume; + accGasVolumeTabY(end+1) = loopY.accumulator_gas_volume ; + accGasPressTabY(end+1) = loopY.accumulator_gas_pressure ; + + PTU_Flow_GToY_tab(end+1)=PTU.flowToRightLoop; + PTU_Flow_YToG_tab(end+1)=PTU.flowToLeftLoop; + PTU_DeltaP_tab(end+1)=loopG.press-loopY.press; + PTU_isActive_tab(end+1)=PTU.isActiveLeft||PTU.isActiveRight; +end + +t=0:dt:maxTime ; +%figure; plot(angleTab,displacementTab); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +figure; +ax_PTU_flow=subplot(5,1,1); +hold(ax_PTU_flow,'on');grid; +title('PTUflow'); +ax_deltaP=subplot(5,1,2); +hold(ax_deltaP,'on');grid; +title('DeltaP'); +ax_active=subplot(5,1,3); +hold(ax_active,'on');grid; +title('PTU active'); +ax_loopPress=subplot(5,1,4); +hold(ax_loopPress,'on');grid; +title('LoopPressures'); +ax_ResVol=subplot(5,1,5); +hold(ax_ResVol,'on');grid; +title('ResVol'); + +plot(ax_PTU_flow,t,PTU_Flow_GToY_tab,'color','red'); +plot(ax_PTU_flow,t,PTU_Flow_YToG_tab,'color','green'); + +plot(ax_deltaP,t,PTU_DeltaP_tab); + +plot(ax_active,t,PTU_isActive_tab); + +plot(ax_loopPress,t,pressTabG,'color','green'); +plot(ax_loopPress,t,pressTabY,'color','red'); + +plot(ax_ResVol,t,resTabG,'color','green'); +plot(ax_ResVol,t,resTabY,'color','red'); +linkaxes([ax_PTU_flow,ax_deltaP,ax_active,ax_loopPress,ax_ResVol],'x'); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +figure; +ax_Press=subplot(5,1,1); +hold(ax_Press,'on');grid; +title('Press'); +ax_deltaVol=subplot(5,1,2); +hold(ax_deltaVol,'on');grid; +title('DeltaVol'); +ax_Volume=subplot(5,1,3); +hold(ax_Volume,'on');grid; +title('Vol'); +ax_deltaFlow=subplot(5,1,4); +hold(ax_deltaFlow,'on');grid; +title('deltaFlow'); +ax_ResVol=subplot(5,1,5); +hold(ax_ResVol,'on');grid; +title('ResVol'); + + +plot(ax_Press,t,pressTabG,'color','green');hold on; +plot(ax_Press,t,pressTabY,'color','red'); + +plot(ax_deltaVol,t,deltaVolTabG,'color','green'); +plot(ax_deltaVol,t,deltaVolTabY,'color','red'); + +plot(ax_Volume,t,volTabG,'color','green'); +plot(ax_Volume,t,volTabY,'color','red'); + +plot(ax_deltaFlow,t,loopFlowG,'color','green'); +plot(ax_deltaFlow,t,loopFlowY,'color','red'); + +plot(ax_ResVol,t,resTabG,'color','green'); +plot(ax_ResVol,t,resTabY,'color','red'); + +linkaxes([ax_Press,ax_deltaVol,ax_Volume,ax_deltaFlow,ax_ResVol],'x'); + + + +time=0:dt:maxTime ; +figure; +ax_accVol=subplot(2,1,1); +hold(ax_accVol,'on');grid; +title('Accumulator Volumes'); +ax_accPress=subplot(2,1,2); +hold(ax_accPress,'on');grid; +title('Accumulator pressure'); + +plot(ax_accVol,time,accFluidVolumeTabG); +plot(ax_accVol,time,accGasVolumeTabG); +legend(ax_accVol,{'Acc FluidVol' 'Acc Gas Vol'}); +plot(ax_accPress,time,accGasPressTabG); +plot(ax_accPress,time,pressTabG); +legend(ax_accPress,{'Acc Press' 'Loop press'}); +linkaxes([ax_accVol,ax_accPress],'x'); + + + +function [pump]=updateP_min_max(dt, pump, loop) + displacement = calculate_displacement(pump,loop); + flow_Gall_per_s = calculate_flow(pump.rpm, displacement); + delta_vol_gall = flow_Gall_per_s * dt; + + pump.maxVol=delta_vol_gall; + pump.minVol=0; %Min is 0 as it can cut off displacement to 0 for EDP +end + +function PTU=updatePTU(PTU,dt, loopLeft, loopRight) + + if PTU.isEnabled + deltaP=loopLeft.press-loopRight.press; + + %TODO: use maped characteristics for PTU? + %TODO Use variable displacement available on one side? + %TODO Handle RPM of ptu so transient are bit slower? + %TODO Handle it as a min/max flow producer + if PTU.isActiveLeft || (~PTU.isActiveRight && deltaP>500) %Left sends flow to right + vr = min(16,loopLeft.press * 0.0058) / 60.0 ; + vr=min(vr,loopLeft.lastMaxFlow*0.6); + + vr=0.05*vr+0.95*PTU.last_flow; + + PTU.flowToLeftLoop= -vr; + PTU.flowToRightLoop= vr * 0.9; + PTU.last_flow=vr; + + + PTU.isActiveLeft=1; + elseif PTU.isActiveRight || (~PTU.isActiveLeft && deltaP<-500) %Right sends flow to left + vr = min(34,loopRight.press * 0.0125) / 60.0 ; + vr=min(vr,loopRight.lastMaxFlow*0.6); + + vr=0.05*vr+0.95*PTU.last_flow; + + PTU.flowToLeftLoop = vr * 0.9; + PTU.flowToRightLoop= -vr; + PTU.last_flow=vr; + + + PTU.isActiveRight=1; + end + + + if PTU.isActiveRight && loopLeft.press >= 3000 || PTU.isActiveLeft && loopRight.press >= 3000 ... + || PTU.isActiveRight && loopRight.press < 200 ... + || PTU.isActiveLeft && loopLeft.press < 200 + PTU.flowToLeftLoop=0; + PTU.flowToRightLoop=0; + PTU.isActiveRight=0; + PTU.isActiveLeft=0; + PTU.last_flow=0; + PTU.last_flow_rate=0; + end + end + + PTU.last_right_press=loopRight.press; + PTU.last_left_press=loopLeft.press; + + end + +function disp=calculate_displacement(pump,loop) + disp=interp1(pump.pressBreakpoints,pump.displacementCarac,loop.press); +end + +function flow= calculate_flow(rpm, displacement) + flow= (rpm * displacement / 231.0 / 60.0); +end + + +function [pump,loop,aileron]= updateL(dt,pump,PTU,loop,aileron) + + %init + deltavol=0; + deltaMaxVol=0; + deltaMinVol=0; + %deltaVolConsumers=0; %%Total volume consumed this iteration + reservoirReturn=0; %%total volume back to res for that iteration + + + %FOR EACH PUMP getting max and min flow available. Will be used at end + %of iteration to fullfill if possible the regulation to 3000 nominal + %pressure + pump=updateP_min_max(dt,pump,loop); + + deltaMaxVol=deltaMaxVol+pump.maxVol; + deltaMinVol=deltaMinVol+pump.minVol; + %END FOREACH PUMP + loop.lastMaxFlow=deltaMaxVol/dt; + + + %Static leaks, random formula to depend on pressure + staticLeakVol=0.04*dt*(loop.press-14.7)/3000; + deltavol=deltavol-staticLeakVol; + %if !leakFailure + reservoirReturn=reservoirReturn+staticLeakVol; %Static leaks are back to reservoir unless failure case + %%%end static leaks + + %Adding ptu flows after pump + %TODO Handle it as a min/max flow producer if possible? + if loop.isLeft + if PTU.flowToLeftLoop > 0 + %were are left side of PTU and positive flow so we receive flow using own reservoir + actualFlow=min(loop.res/dt,PTU.flowToLeftLoop); + loop.res=loop.res-actualFlow* dt; + else + %we are using own flow to power right side so we send that back + %to our own reservoir + actualFlow=PTU.flowToLeftLoop; + reservoirReturn=reservoirReturn-actualFlow* dt; + end + deltavol=deltavol+actualFlow * dt; + %reservoirReturn=reservoirReturn+actualFlow; + else + if PTU.flowToRightLoop > 0 + %were are right side of PTU and positive flow so we receive flow using own reservoir + actualFlow=min(loop.res/dt,PTU.flowToRightLoop); + loop.res=loop.res-actualFlow* dt; + else + %we are using own flow to power left side so we send that back + %to our own reservoir + actualFlow=PTU.flowToRightLoop; + reservoirReturn=reservoirReturn-actualFlow* dt; + end + deltavol=deltavol+actualFlow* dt; + %reservoirReturn=reservoirReturn+actualFlow; + end + + + %Unprimed case + %Here we handle starting with air in the loop + if loop.volume < loop.maxVolume %TODO what to do if we are back under max volume and unprime the loop? + difference = loop.maxVolume - loop.volume; + availableFluidVol=min(loop.res,deltaMaxVol); + delta_loop_vol = min(availableFluidVol,difference); + deltaMaxVol = deltaMaxVol-delta_loop_vol; %TODO check if we cross the deltaVolMin? + loop.volume = loop.volume+ delta_loop_vol; + loop.res=loop.res-delta_loop_vol; + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%ACCUMULATOR%%%%%%%%%%%% + accumulatorDeltaPress = loop.accumulator_gas_pressure - loop.press; + flowVariation = interp1(loop.accumulator_DeltaPressBreakpoints,loop.accumulator_DeltaPressFlowCarac,abs(accumulatorDeltaPress)) ; + +% %%TODO HANDLE OR CHECK IF RESERVOIR AVAILABILITY is OK +% %%TODO check if accumulator can be used as a min/max flow producer to +% %%avoid it being a consumer that might unsettle pressure + if ( accumulatorDeltaPress > 0 ) + volumeFromAcc = min(loop.accumulator_fluid_volume, flowVariation * dt ); + loop.accumulator_fluid_volume=loop.accumulator_fluid_volume-volumeFromAcc; + loop.accumulator_gas_volume=loop.accumulator_gas_volume+volumeFromAcc; + deltavol = deltavol + volumeFromAcc; + else + volumeToAcc = max(max(0,deltavol), flowVariation * dt ); + %volumeToAcc = flowVariation * dt ;%TODO handle if flow actually available: maybe using deltavolMAX + loop.accumulator_fluid_volume=loop.accumulator_fluid_volume+volumeToAcc; + loop.accumulator_gas_volume=loop.accumulator_gas_volume-volumeToAcc; + deltavol = deltavol - volumeToAcc; + end + + loop.accumulator_gas_pressure = (loop.ACCUMULATOR_GAS_PRECHARGE * loop.ACCUMULATOR_MAX_VOLUME) / (loop.ACCUMULATOR_MAX_VOLUME - loop.accumulator_fluid_volume); + %%%%%%%%%%%%%%%%%%%END ACCUMULATOR%%%%%%%%%%%%%%%% + + + + %%%%UPDATE ALL ACTUATORS OF THIS LOOP + used_fluidQty=0; %%total fluid used + pressUsedForForce=0; + + %FOR EACH MOVINGPART +% % % used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated*264.172; %264.172 is m^3 to gallons +% % % reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated*264.172; +% % % +% % % %Reseting vars for next loop: +% % % aileron.volumeToActuatorAccumulated=0; +% % % aileron.volumeToResAccumulated=0; +% % % %%%%% +% % % +% % % %Setting press usable by the actuator for this iteration +% % % aileron.loopPressAvailable = loop.press; +% % % +% % % %%%%%%%%End FOREACH%%%%%%%%%%%%%%% +% % % +% % % %Simuating the 3 gears by multiplying used quantities +% % % used_fluidQty=used_fluidQty*2.5; +% % % reservoirReturn=reservoirReturn*2.5; + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %Update pressure and vol from last used flow by actuators + deltavol=deltavol-used_fluidQty; + + + + + %How much we need to reach target of 3000? + volume_needed_to_reach_pressure_target = vol_to_target(loop,3000); + + %Actually we need this PLUS what is used by consumers. + volume_needed_to_reach_pressure_target = volume_needed_to_reach_pressure_target - deltavol; + + %Now computing what we will actually use from flow providers limited by + %their min and max flows and reservoir availability + actual_volume_added_to_pressurise = min(loop.res,max(deltaMinVol,min(deltaMaxVol,volume_needed_to_reach_pressure_target))); + deltavol=deltavol+actual_volume_added_to_pressurise; + + %Loop Pressure update From Bulk modulus +% loop.press = max(14.7,loop.press + deltaPress_from_deltaVolume(deltavol,loop)); + tempPress = max(14.7,loop.press + deltaPress_from_deltaVolume(deltavol,loop)); + loop.press=0.3*loop.press+0.7*tempPress; + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + %Update res + loop.res=loop.res-actual_volume_added_to_pressurise; %limit to 0 min? for case of negative added? + loop.res=loop.res+reservoirReturn; + + %Update Volumes + loop.delta_vol=0.6*loop.delta_vol + 0.4*deltavol; + loop.volume=loop.volume+deltavol; +end + + +function deltaPress = deltaPress_from_deltaVolume(deltaV,loop) + delta_vol_m3=deltaV * 0.00378541178; %Convert to m3 + deltaP_pascal=((delta_vol_m3) / (loop.maxVolumeHighPressureSide* 0.00378541178)) * loop.bulkModulus; + + deltaPress = deltaP_pascal*0.0001450377; +end + +function volume_needed_to_reach_pressure_target_gal = vol_to_target(loop,targetPress) + volume_needed_to_reach_pressure_target_m3 = (targetPress-loop.press)/0.0001450377 * (loop.maxVolumeHighPressureSide* 0.00378541178) / loop.bulkModulus; + volume_needed_to_reach_pressure_target_gal = volume_needed_to_reach_pressure_target_m3 / 0.00378541178; +end diff --git a/src/systems/systems/src/overhead/mod.rs b/src/systems/systems/src/overhead/mod.rs index 374d7c16324..5103da36fbe 100644 --- a/src/systems/systems/src/overhead/mod.rs +++ b/src/systems/systems/src/overhead/mod.rs @@ -240,7 +240,7 @@ impl AutoOffFaultPushButton { self.has_fault } - fn set_fault(&mut self, value: bool) { + pub fn set_fault(&mut self, value: bool) { self.has_fault = value; } } @@ -256,6 +256,71 @@ impl SimulationElement for AutoOffFaultPushButton { } } +pub struct AutoOnFaultPushButton { + is_auto_id: String, + has_fault_id: String, + + is_auto: bool, + has_fault: bool, +} +impl AutoOnFaultPushButton { + pub fn new_auto(name: &str) -> Self { + Self::new(name, true) + } + + pub fn new_on(name: &str) -> Self { + Self::new(name, false) + } + + fn new(name: &str, is_auto: bool) -> Self { + Self { + is_auto_id: format!("OVHD_{}_PB_IS_AUTO", name), + has_fault_id: format!("OVHD_{}_PB_HAS_FAULT", name), + is_auto, + has_fault: false, + } + } + + pub fn push_on(&mut self) { + self.is_auto = false; + } + + pub fn push_auto(&mut self) { + self.is_auto = true; + } + + pub fn is_auto(&self) -> bool { + self.is_auto + } + + pub fn is_on(&self) -> bool { + !self.is_auto + } + + pub fn set_auto(&mut self, value: bool) { + self.is_auto = value; + } + + pub fn has_fault(&self) -> bool { + self.has_fault + } + + pub fn set_fault(&mut self, value: bool) { + self.has_fault = value; + } +} +impl SimulationElement for AutoOnFaultPushButton { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.is_auto_id, self.is_auto()); + writer.write_bool(&self.has_fault_id, self.has_fault()); + } + + fn read(&mut self, reader: &mut SimulatorReader) { + self.set_auto(reader.read_bool(&self.is_auto_id)); + self.set_fault(reader.read_bool(&self.has_fault_id)); + } +} + pub struct FaultReleasePushButton { is_released_id: String, has_fault_id: String, diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index c5381538bef..276cbe81218 100644 --- a/src/systems/systems/src/shared/mod.rs +++ b/src/systems/systems/src/shared/mod.rs @@ -65,6 +65,37 @@ impl DelayedTrueLogicGate { } } +/// The delay logic gate delays the false result of a given expression by the given amount of time. +/// True results are output immediately. Starts with a false result state. +pub struct DelayedFalseLogicGate { + delay: Duration, + expression_result: bool, + false_duration: Duration, +} +impl DelayedFalseLogicGate { + pub fn new(delay: Duration) -> Self { + Self { + delay, + expression_result: false, + false_duration: delay, + } + } + + pub fn update(&mut self, context: &UpdateContext, expression_result: bool) { + if !expression_result { + self.false_duration += context.delta(); + } else { + self.false_duration = Duration::from_millis(0); + } + + self.expression_result = expression_result; + } + + pub fn output(&self) -> bool { + self.expression_result || self.delay > self.false_duration + } +} + /// Given a current and target temperature, takes a coefficient and delta to /// determine the new temperature after a certain duration has passed. pub(crate) fn calculate_towards_target_temperature( @@ -88,6 +119,31 @@ pub(crate) fn calculate_towards_target_temperature( } } +// Interpolate values_map_y at point value_at_point in breakpoints break_points_x +pub(crate) fn interpolation(xs: &[f64], ys: &[f64], intermediate_x: f64) -> f64 { + debug_assert!(xs.len() == ys.len()); + debug_assert!(xs.len() >= 2); + debug_assert!(ys.len() >= 2); + + if intermediate_x <= xs[0] { + *ys.first().unwrap() + } else if intermediate_x >= xs[xs.len() - 1] { + *ys.last().unwrap() + } else { + let mut idx: usize = 1; + + while idx < xs.len() - 1 { + if intermediate_x < xs[idx] { + break; + } + idx += 1; + } + + ys[idx - 1] + + (intermediate_x - xs[idx - 1]) / (xs[idx] - xs[idx - 1]) * (ys[idx] - ys[idx - 1]) + } +} + #[cfg(test)] mod delayed_true_logic_gate_tests { use super::*; @@ -185,10 +241,181 @@ mod delayed_true_logic_gate_tests { } #[cfg(test)] -mod calculate_towards_target_temperature_tests { - use ntest::assert_about_eq; +mod delayed_false_logic_gate_tests { + use super::*; + use crate::simulation::test::SimulationTestBed; + use crate::simulation::{Aircraft, SimulationElement}; + + struct TestAircraft { + gate: DelayedFalseLogicGate, + expression_result: bool, + } + impl TestAircraft { + fn new(gate: DelayedFalseLogicGate) -> Self { + Self { + gate, + expression_result: false, + } + } + + fn set_expression(&mut self, value: bool) { + self.expression_result = value; + } + + fn gate_output(&self) -> bool { + self.gate.output() + } + } + impl Aircraft for TestAircraft { + fn update_before_power_distribution(&mut self, context: &UpdateContext) { + self.gate.update(context, self.expression_result); + } + } + impl SimulationElement for TestAircraft {} + #[test] + fn when_the_expression_is_false_initially_returns_false() { + let mut aircraft = + TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(100))); + let mut test_bed = SimulationTestBed::new_with_delta(Duration::from_millis(1_000)); + + test_bed.run_aircraft(&mut aircraft); + + assert_eq!(aircraft.gate_output(), false); + } + + #[test] + fn when_the_expression_is_true_returns_true() { + let mut aircraft = + TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(100))); + let mut test_bed = SimulationTestBed::new_with_delta(Duration::from_millis(1_000)); + + aircraft.set_expression(true); + test_bed.run_aircraft(&mut aircraft); + + assert_eq!(aircraft.gate_output(), true); + } + + #[test] + fn when_the_expression_is_false_and_delay_hasnt_passed_returns_true() { + let mut aircraft = + TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(10_000))); + let mut test_bed = SimulationTestBed::new_with_delta(Duration::from_millis(0)); + + aircraft.set_expression(true); + test_bed.run_aircraft(&mut aircraft); + aircraft.set_expression(false); + test_bed.set_delta(Duration::from_millis(1_000)); + test_bed.run_aircraft(&mut aircraft); + + assert_eq!(aircraft.gate_output(), true); + } + + #[test] + fn when_the_expression_is_false_and_delay_has_passed_returns_false() { + let mut aircraft = + TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(100))); + let mut test_bed = SimulationTestBed::new_with_delta(Duration::from_millis(0)); + + aircraft.set_expression(true); + test_bed.run_aircraft(&mut aircraft); + + aircraft.set_expression(false); + test_bed.set_delta(Duration::from_millis(1_000)); + test_bed.run_aircraft(&mut aircraft); + + assert_eq!(aircraft.gate_output(), false); + } + + #[test] + fn when_the_expression_is_false_and_becomes_true_before_delay_has_passed_returns_true_once_delay_passed( + ) { + let mut aircraft = + TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(1_000))); + let mut test_bed = SimulationTestBed::new_with_delta(Duration::from_millis(0)); + + aircraft.set_expression(false); + test_bed.run_aircraft(&mut aircraft); + test_bed.set_delta(Duration::from_millis(800)); + test_bed.run_aircraft(&mut aircraft); + + aircraft.set_expression(true); + test_bed.set_delta(Duration::from_millis(100)); + test_bed.run_aircraft(&mut aircraft); + test_bed.set_delta(Duration::from_millis(200)); + test_bed.run_aircraft(&mut aircraft); + + assert_eq!(aircraft.gate_output(), true); + } +} + +#[cfg(test)] +mod interpolation_tests { + use super::*; + + const XS1: [f64; 10] = [ + -100.0, -10.0, 10.0, 240.0, 320.0, 435.3, 678.9, 890.3, 10005.0, 203493.7, + ]; + + const YS1: [f64; 10] = [ + -200.0, 10.0, 40.0, -553.0, 238.4, 30423.3, 23000.2, 32000.4, 43200.2, 34.2, + ]; + + #[test] + fn interpolation_before_first_element_test() { + // We expect to get first element of YS1 + assert!((interpolation(&XS1, &YS1, -500.0) - YS1[0]).abs() < f64::EPSILON); + } + + #[test] + fn interpolation_after_last_element_test() { + // We expect to get last element of YS1 + assert!( + (interpolation(&XS1, &YS1, 100000000.0) - *YS1.last().unwrap()).abs() < f64::EPSILON + ); + } + + #[test] + fn interpolation_first_element_test() { + // Giving first element of X tab we expect first of Y tab + assert!( + (interpolation(&XS1, &YS1, *XS1.first().unwrap()) - *YS1.first().unwrap()).abs() + < f64::EPSILON + ); + } + + #[test] + fn interpolation_last_element_test() { + // Giving last element of X tab we expect last of Y tab + assert!( + (interpolation(&XS1, &YS1, *XS1.last().unwrap()) - *YS1.last().unwrap()).abs() + < f64::EPSILON + ); + } + + #[test] + fn interpolation_middle_element_test() { + let res = interpolation(&XS1, &YS1, 358.0); + assert!((res - 10186.589).abs() < 0.001); + } + + #[test] + fn interpolation_last_segment_element_test() { + let res = interpolation(&XS1, &YS1, 22200.0); + assert!((res - 40479.579).abs() < 0.001); + } + + #[test] + fn interpolation_first_segment_element_test() { + let res = interpolation(&XS1, &YS1, -50.0); + assert!((res - (-83.3333)).abs() < 0.001); + } +} + +#[cfg(test)] +mod calculate_towards_target_temperature_tests { use super::*; + use ntest::assert_about_eq; #[test] fn when_current_equals_target_returns_current() { diff --git a/src/systems/systems/src/simulation/test.rs b/src/systems/systems/src/simulation/test.rs index 0ce357377be..736385b12be 100644 --- a/src/systems/systems/src/simulation/test.rs +++ b/src/systems/systems/src/simulation/test.rs @@ -1,5 +1,8 @@ use std::{collections::HashMap, time::Duration}; -use uom::si::{f64::*, length::foot, thermodynamic_temperature::degree_celsius, velocity::knot}; +use uom::si::{ + acceleration::foot_per_second_squared, f64::*, length::foot, + thermodynamic_temperature::degree_celsius, velocity::knot, +}; use crate::electrical::consumption::SuppliedPower; @@ -167,6 +170,13 @@ impl SimulationTestBed { .write_bool(UpdateContext::IS_ON_GROUND_KEY, on_ground); } + pub fn set_long_acceleration(&mut self, accel: Acceleration) { + self.reader_writer.write_f64( + UpdateContext::ACCEL_BODY_Z_KEY, + accel.get::(), + ); + } + pub fn supplied_power_fn SuppliedPower + 'static>( mut self, supplied_power_fn: T, diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index 38299116cef..eac091a6f41 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -1,23 +1,28 @@ use std::time::Duration; -use uom::si::{f64::*, length::foot, thermodynamic_temperature::degree_celsius, velocity::knot}; +use uom::si::{ + acceleration::foot_per_second_squared, f64::*, length::foot, + thermodynamic_temperature::degree_celsius, time::second, velocity::knot, +}; use super::SimulatorReader; /// Provides data unowned by any system in the aircraft system simulation /// for the purpose of handling a simulation tick. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub struct UpdateContext { delta: Duration, indicated_airspeed: Velocity, indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, is_on_ground: bool, + longitudinal_acceleration: Acceleration, } impl UpdateContext { pub(crate) const AMBIENT_TEMPERATURE_KEY: &'static str = "AMBIENT TEMPERATURE"; pub(crate) const INDICATED_AIRSPEED_KEY: &'static str = "AIRSPEED INDICATED"; pub(crate) const INDICATED_ALTITUDE_KEY: &'static str = "INDICATED ALTITUDE"; pub(crate) const IS_ON_GROUND_KEY: &'static str = "SIM ON GROUND"; + pub(crate) const ACCEL_BODY_Z_KEY: &'static str = "ACCELERATION BODY Z"; pub fn new( delta: Duration, @@ -25,6 +30,7 @@ impl UpdateContext { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, is_on_ground: bool, + longitudinal_acceleration: Acceleration, ) -> UpdateContext { UpdateContext { delta, @@ -32,6 +38,7 @@ impl UpdateContext { indicated_altitude, ambient_temperature, is_on_ground, + longitudinal_acceleration, } } @@ -49,6 +56,9 @@ impl UpdateContext { ), is_on_ground: reader.read_bool(UpdateContext::IS_ON_GROUND_KEY), delta: delta_time, + longitudinal_acceleration: Acceleration::new::( + reader.read_f64(UpdateContext::ACCEL_BODY_Z_KEY), + ), } } @@ -60,6 +70,14 @@ impl UpdateContext { self.delta } + pub fn delta_as_secs_f64(&self) -> f64 { + self.delta.as_secs_f64() + } + + pub fn delta_as_time(&self) -> Time { + Time::new::(self.delta.as_secs_f64()) + } + pub fn indicated_airspeed(&self) -> Velocity { self.indicated_airspeed } @@ -75,4 +93,15 @@ impl UpdateContext { pub fn is_on_ground(&self) -> bool { self.is_on_ground } + + pub fn long_accel(&self) -> Acceleration { + self.longitudinal_acceleration + } + + pub fn with_delta(&self, delta: Duration) -> Self { + let mut copy: UpdateContext = *self; + copy.delta = delta; + + copy + } }