From 85a5b7744218052963a31b37ae192f5bc7fb97fb Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Wed, 3 Mar 2021 08:58:39 +0100 Subject: [PATCH 001/122] merging first PR in clean master --- .../AirPlanes/Asobo_A320_NEO/approach.FLT | 22 +- .../AirPlanes/Asobo_A320_NEO/apron.FLT | 22 +- .../AirPlanes/Asobo_A320_NEO/cruise.FLT | 22 +- .../AirPlanes/Asobo_A320_NEO/final.FLT | 22 +- .../model/A320_NEO_INTERIOR.xml | 42 +- .../AirPlanes/Asobo_A320_NEO/runway.FLT | 22 +- .../AirPlanes/Asobo_A320_NEO/taxi.flt | 2 +- .../Airliners/A320_Neo/BRK/A320_Neo_BRK.js | 16 +- Cargo.lock | 115 + src/systems/a320_systems/src/hydraulic.rs | 869 ++++++- src/systems/a320_systems/src/lib.rs | 13 +- src/systems/a320_systems_wasm/src/lib.rs | 50 + src/systems/systems/Cargo.toml | 2 + .../systems/src/hydraulic/brakecircuit.rs | 519 ++++ src/systems/systems/src/hydraulic/mod.rs | 2164 +++++++++++++++++ .../src/hydraulic/study/test_Hy_CompConcept.m | 486 ++++ .../hydraulic/study/test_Hy_CompConcept_V2.m | 531 ++++ .../systems/src/simulation/update_context.rs | 30 +- 18 files changed, 4854 insertions(+), 95 deletions(-) create mode 100644 src/systems/systems/src/hydraulic/brakecircuit.rs create mode 100644 src/systems/systems/src/hydraulic/study/test_Hy_CompConcept.m create mode 100644 src/systems/systems/src/hydraulic/study/test_Hy_CompConcept_V2.m diff --git a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/approach.FLT b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/approach.FLT index b49339bfe90..bef1195cfbd 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/approach.FLT +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/approach.FLT @@ -163,18 +163,18 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=1 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_HAS_FAULT=0 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 +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_HAS_FAULT=0 +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_HAS_FAULT=0 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -201,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/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/apron.FLT b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/apron.FLT index 5fb2596de8f..e57f0acc3d4 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/apron.FLT +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/apron.FLT @@ -169,18 +169,18 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=1 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_HAS_FAULT=0 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 +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_HAS_FAULT=0 +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_HAS_FAULT=0 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -207,7 +207,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/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/cruise.FLT b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/cruise.FLT index c8ecc716162..39d589f9b35 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/cruise.FLT +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/cruise.FLT @@ -163,18 +163,18 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=1 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_HAS_FAULT=0 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 +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_HAS_FAULT=0 +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_HAS_FAULT=0 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -201,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/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/final.FLT b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/final.FLT index 6e52d2150e2..1ec8c84ffae 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/final.FLT +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/final.FLT @@ -163,18 +163,18 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=1 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_HAS_FAULT=0 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 +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_HAS_FAULT=0 +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_HAS_FAULT=0 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -201,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/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/model/A320_NEO_INTERIOR.xml b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/model/A320_NEO_INTERIOR.xml index 89d56cd79a0..32596bd0f7c 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/model/A320_NEO_INTERIOR.xml +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/model/A320_NEO_INTERIOR.xml @@ -1937,24 +1937,24 @@ PUSH_OVHD_HYD_ENG1PUMP - L:A32NX_HYD_ENG1PUMP_TOGGLE + L:A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO - (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) ! - %((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_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) ! - %((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} @@ -1970,36 +1970,36 @@ PUSH_OVHD_HYD_ELECPUMP LOCK_OVHD_HYD_ELECPUMP - L:A32NX_HYD_ELECPUMP_TOGGLE + L:A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO - (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) ! - %((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_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) ! - %((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_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) ! - %((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} @@ -2603,7 +2603,7 @@ PUSH_OVHD_HYD_BLUEPUMP LOCK_OVHD_HYD_BLUEPUMPOVRD - L:A32NX_OVHD_HYD_BLUEPUMP_OVRD + L:A32NX_OVHD_HYD_EPUMPY_OVRD_PB_IS_ON diff --git a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/runway.FLT b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/runway.FLT index 671a6262044..d54e19caf07 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/runway.FLT +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/runway.FLT @@ -170,18 +170,18 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=1 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_HAS_FAULT=0 +A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 +A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 +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_HAS_FAULT=0 +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_HAS_FAULT=0 +A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 +A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 +A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 A32NX_ENGMANSTART1_TOGGLE=0 @@ -208,7 +208,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/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/taxi.flt b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/taxi.flt index f8c9be17e59..db6ceec4f71 100644 --- a/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/taxi.flt +++ b/A32NX/SimObjects/AirPlanes/Asobo_A320_NEO/taxi.flt @@ -182,7 +182,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/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js b/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js index 7d1229a00ff..f9546b27c9c 100644 --- a/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js +++ b/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js @@ -74,11 +74,11 @@ var A320_Neo_BRK; if (this.updateThrottler.canUpdate(_deltaTime) === -1) { return; } - const currentPKGBrakeState = SimVar.GetSimVarValue("BRAKE PARKING POSITION", "Bool"); + const powerAvailable = SimVar.GetSimVarValue("L:DCPowerAvailable","Bool"); if (this.topGauge != null) { if (powerAvailable) { - this.topGauge.setValue(3); + this.topGauge.setValue(3 * (SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS","PSI") / 3000)); } else { this.topGauge.setValue(0); } @@ -86,22 +86,14 @@ var A320_Neo_BRK; if (this.leftGauge != null) { if (powerAvailable) { - if (currentPKGBrakeState != 0) { - this.leftGauge.setValue(2); - } else { - this.leftGauge.setValue(2 * (SimVar.GetSimVarValue("BRAKE LEFT POSITION","SINT32") / 32000)); - } + this.leftGauge.setValue(3 * (SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS","PSI") / 3000)); } else { this.leftGauge.setValue(0); } } if (this.rightGauge != null) { if (powerAvailable) { - if (currentPKGBrakeState != 0) { - this.rightGauge.setValue(2); - } else { - this.rightGauge.setValue(2 * (SimVar.GetSimVarValue("BRAKE RIGHT POSITION","SINT32") / 32000)); - } + this.rightGauge.setValue(3 * (SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS","PSI") / 3000)); } else { this.rightGauge.setValue(0); } diff --git a/Cargo.lock b/Cargo.lock index a81a8d4762d..8005684a5c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,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 +68,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" @@ -149,6 +178,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" @@ -255,6 +306,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" @@ -322,6 +379,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" @@ -410,6 +477,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" @@ -434,6 +507,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" @@ -543,12 +626,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" @@ -573,6 +668,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" @@ -584,12 +685,26 @@ 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" dependencies = [ "ntest", + "plotlib", "rand", + "rustplotlib", "uom", ] diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 6760fec7ce7..e433a94983b 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,16 +1,877 @@ +use std::time::{Duration, Instant}; +use uom::si::{ + acceleration::foot_per_second_squared, area::square_meter, f64::*, force::newton, length::foot, + length::meter, mass_density::kilogram_per_cubic_meter, pressure::atmosphere, pressure::pascal, + pressure::psi, ratio::percent, thermodynamic_temperature::degree_celsius, time::second, + velocity::knot, volume::cubic_inch, volume::gallon, volume::liter, + volume_rate::cubic_meter_per_second, volume_rate::gallon_per_second, +}; + +//use crate::{ hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, PressureSource, Ptu, Pump, RatPump}, overhead::{OnOffFaultPushButton}, shared::DelayedTrueLogicGate}; +use systems::engine::Engine; +use systems::hydraulic::brakecircuit::BrakeCircuit; +use systems::hydraulic::{ + ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, PressureSource, Ptu, + RatPropeller, RatPump, +}; +use systems::overhead::{AutoOffFaultPushButton, FirePushButton, OnOffFaultPushButton}; +use systems::{ + hydraulic::brakecircuit::AutoBrakeController, + simulation::{ + SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, + UpdateContext, + }, +}; + pub struct A320Hydraulic { - // Until hydraulic is implemented, we'll fake it with this boolean. - blue_pressurised: bool, + hyd_logic_inputs: A320HydraulicLogic, + hyd_brake_logic: A320HydraulicBrakingLogic, + blue_loop: HydLoop, + green_loop: HydLoop, + yellow_loop: HydLoop, + engine_driven_pump_1: EngineDrivenPump, + engine_driven_pump_2: EngineDrivenPump, + blue_electric_pump: ElectricPump, + yellow_electric_pump: ElectricPump, + rat: RatPump, + ptu: Ptu, + braking_circuit_norm: BrakeCircuit, + //autobrake_controller : AutoBrakeController, + braking_circuit_altn: BrakeCircuit, + total_sim_time_elapsed: Duration, + lag_time_accumulator: Duration, + debug_refresh_duration: Duration, } impl A320Hydraulic { + const MIN_PRESS_PRESSURISED: f64 = 150.0; + const HYDRAULIC_SIM_TIME_STEP: u64 = 100; //refresh rate of hydraulic simulation in ms + const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; //refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update + pub fn new() -> A320Hydraulic { A320Hydraulic { - blue_pressurised: true, + hyd_logic_inputs: A320HydraulicLogic::new(), + hyd_brake_logic: A320HydraulicBrakingLogic::new(), + + blue_loop: HydLoop::new( + LoopColor::Blue, + false, + false, + Volume::new::(15.8), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.56), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + green_loop: HydLoop::new( + LoopColor::Green, + true, + false, + Volume::new::(26.38), + Volume::new::(26.41), + Volume::new::(15.), + Volume::new::(3.6), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + yellow_loop: HydLoop::new( + LoopColor::Yellow, + false, + true, + Volume::new::(19.75), + Volume::new::(19.81), + Volume::new::(10.0), + Volume::new::(3.15), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + engine_driven_pump_1: EngineDrivenPump::new(), + engine_driven_pump_2: EngineDrivenPump::new(), + blue_electric_pump: ElectricPump::new(), + yellow_electric_pump: ElectricPump::new(), + rat: RatPump::new(), + ptu: Ptu::new(), + + braking_circuit_norm: BrakeCircuit::new( + Volume::new::(0.), + Volume::new::(0.), + Volume::new::(0.08), + ), + + //autobrake_controller : AutoBrakeController::new(vec![Acceleration::new::(-5.),Acceleration::new::(-10.),Acceleration::new::(-20.)]), + braking_circuit_altn: BrakeCircuit::new( + Volume::new::(1.5), + Volume::new::(0.0), + Volume::new::(0.08), + ), + + total_sim_time_elapsed: Duration::new(0, 0), + lag_time_accumulator: Duration::new(0, 0), + debug_refresh_duration: Duration::new(0, 0), } } pub fn is_blue_pressurised(&self) -> bool { - self.blue_pressurised + self.blue_loop.get_pressure().get::() >= A320Hydraulic::MIN_PRESS_PRESSURISED + } + + pub fn is_green_pressurised(&self) -> bool { + self.green_loop.get_pressure().get::() >= A320Hydraulic::MIN_PRESS_PRESSURISED + } + + pub fn is_yellow_pressurised(&self) -> bool { + self.yellow_loop.get_pressure().get::() >= A320Hydraulic::MIN_PRESS_PRESSURISED + } + + pub fn update( + &mut self, + ct: &UpdateContext, + engine1: &Engine, + engine2: &Engine, + overhead_panel: &A320HydraulicOverheadPanel, + ) { + let min_hyd_loop_timestep = Duration::from_millis(A320Hydraulic::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz + + self.total_sim_time_elapsed += ct.delta; + + //time to catch up in our simulation = new delta + time not updated last iteration + let time_to_catch = ct.delta + self.lag_time_accumulator; + + //Number of time steps to do according to required time step + let number_of_steps_f64 = time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); + + self.debug_refresh_duration += ct.delta; + if self.debug_refresh_duration > Duration::from_secs_f64(0.3) { + println!( + "---HYDRAULIC UPDATE : t={}", + self.total_sim_time_elapsed.as_secs_f64() + ); + println!( + "---G: {:.0} B: {:.0} Y: {:.0}", + self.green_loop.get_pressure().get::(), + self.blue_loop.get_pressure().get::(), + self.yellow_loop.get_pressure().get::() + ); + println!( + "---RAT stow {:.0} Rat rpm: {:.0}", + self.rat.get_stow_position(), + self.rat.prop.get_rpm(), + ); + // println!("---BRAKELOGIC : Ldmn={} Rdmn={} PbrakInd={} Askid={}",self.hyd_brake_logic.left_brake_command,self.hyd_brake_logic.right_brake_command,self.hyd_brake_logic.parking_brake_demand,self.hyd_brake_logic.anti_skid_activated); + // println!("---BRAKEDMNDS : LG={} RG={} LY={} RY={}",self.hyd_brake_logic.left_brake_green_command,self.hyd_brake_logic.right_brake_green_command,self.hyd_brake_logic.left_brake_yellow_command,self.hyd_brake_logic.right_brake_yellow_command); + //println!("---L={:.0} C={:.0} R={:.0}",ct.wheel_left_rpm,ct.wheel_center_rpm,ct.wheel_right_rpm); + + // let leftLock = ct.wheel_center_rpm / ct.wheel_left_rpm.max(1.); + // let rightLock = ct.wheel_center_rpm / ct.wheel_right_rpm.max(1.); + // println!("---LS={:.0} RS={:.0}",leftLock,rightLock); + // println!("---EDP1 n2={} EDP2 n2={}", engine1.n2.get::(), engine2.n2.get::()); + // println!("---EDP1 flowMax={:.1}gpm EDP2 flowMax={:.1}gpm", (self.engine_driven_pump_1.get_delta_vol_max().get::() / min_hyd_loop_timestep.as_secs_f64() )* 60.0, (self.engine_driven_pump_2.get_delta_vol_max().get::()/min_hyd_loop_timestep.as_secs_f64())*60.0); + //println!("---AutoBrakes={:.0} command={:0.2} accel={:0.2} accerror={:0.2}",self.hyd_brake_logic.autobrakes_setting,self.autobrake_controller.get_brake_command(),self.autobrake_controller.current_filtered_accel.get::(),self.autobrake_controller.current_accel_error.get::()); + // println!("---steps required: {:.2}", number_of_steps_f64); + self.debug_refresh_duration = Duration::from_secs_f64(0.0); + } + + //updating rat stowed pos on all frames in case it's used for graphics + self.rat.update_stow_pos(&ct.delta); + + if number_of_steps_f64 < 1.0 { + //Can't do a full time step + //we can either do an update with smaller step or wait next iteration + + self.lag_time_accumulator = + Duration::from_secs_f64(number_of_steps_f64 * min_hyd_loop_timestep.as_secs_f64()); + //Time lag is float part of num of steps * fixed time step to get a result in time + } else { + let num_of_update_loops = number_of_steps_f64.floor() as u32; //Int part is the actual number of loops to do + //Rest of floating part goes into accumulator + self.lag_time_accumulator = Duration::from_secs_f64( + (number_of_steps_f64 - (num_of_update_loops as f64)) + * min_hyd_loop_timestep.as_secs_f64(), + ); //Keep track of time left after all fixed loop are done + + //UPDATING HYDRAULICS AT FIXED STEP + for cur_loop in 0..num_of_update_loops { + //Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly + self.update_hyd_logic_inputs(&min_hyd_loop_timestep, &overhead_panel, &ct); + + //Process brake logic (which circuit brakes) and send brake demands (how much) + self.hyd_brake_logic + .update_brake_demands(&min_hyd_loop_timestep, &self.green_loop); + self.hyd_brake_logic.send_brake_demands( + &mut self.braking_circuit_norm, + &mut self.braking_circuit_altn, + ); + + //UPDATE HYDRAULICS FIXED TIME STEP + self.ptu.update(&self.green_loop, &self.yellow_loop); + self.engine_driven_pump_1.update( + &min_hyd_loop_timestep, + &ct, + &self.green_loop, + &engine1, + ); + self.engine_driven_pump_2.update( + &min_hyd_loop_timestep, + &ct, + &self.yellow_loop, + &engine2, + ); + self.yellow_electric_pump + .update(&min_hyd_loop_timestep, &ct, &self.yellow_loop); + self.blue_electric_pump + .update(&min_hyd_loop_timestep, &ct, &self.blue_loop); + + self.rat + .update(&min_hyd_loop_timestep, &ct, &self.blue_loop); + self.green_loop.update( + &min_hyd_loop_timestep, + &ct, + Vec::new(), + vec![&self.engine_driven_pump_1], + Vec::new(), + vec![&self.ptu], + ); + self.yellow_loop.update( + &min_hyd_loop_timestep, + &ct, + vec![&self.yellow_electric_pump], + vec![&self.engine_driven_pump_2], + Vec::new(), + vec![&self.ptu], + ); + self.blue_loop.update( + &min_hyd_loop_timestep, + &ct, + vec![&self.blue_electric_pump], + Vec::new(), + vec![&self.rat], + Vec::new(), + ); + + //self.autobrake_controller.update(&ct.delta, &ct); + self.braking_circuit_norm + .update(&min_hyd_loop_timestep, &self.green_loop); + self.braking_circuit_altn + .update(&min_hyd_loop_timestep, &self.yellow_loop); + self.braking_circuit_norm.reset_accumulators(); + self.braking_circuit_altn.reset_accumulators(); + } + + //UPDATING ACTUATOR PHYSICS AT "FIXED STEP / ACTUATORS_SIM_TIME_STEP_MULT" + //Here put everything that needs higher simulation rates + let num_of_actuators_update_loops = + num_of_update_loops * A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; + let delta_time_physics = + min_hyd_loop_timestep / A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X + for cur_loop in 0..num_of_actuators_update_loops { + self.rat + .update_physics(&delta_time_physics, &ct.indicated_airspeed); + } + } + } + + pub fn update_hyd_logic_inputs( + &mut self, + delta_time_update: &Duration, + overhead_panel: &A320HydraulicOverheadPanel, + ct: &UpdateContext, + ) { + let mut cargo_operated_ptu = false; + let mut cargo_operated_ypump = false; + let mut nsw_pin_inserted = false; + + //Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update + if self.hyd_logic_inputs.weight_on_wheels { + cargo_operated_ptu = self + .hyd_logic_inputs + .is_cargo_operation_ptu_flag(&delta_time_update); + cargo_operated_ypump = self + .hyd_logic_inputs + .is_cargo_operation_flag(&delta_time_update); + nsw_pin_inserted = self + .hyd_logic_inputs + .is_nsw_pin_inserted_flag(&delta_time_update); + } + + //Basic faults of pumps + if self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() { + self.hyd_logic_inputs.yellow_epump_has_fault = true; + } else { + self.hyd_logic_inputs.yellow_epump_has_fault = false; + } + if self.engine_driven_pump_1.is_active() && !self.is_green_pressurised() { + self.hyd_logic_inputs.green_edp_has_fault = true; + } else { + self.hyd_logic_inputs.green_edp_has_fault = false; + } + if self.engine_driven_pump_2.is_active() && !self.is_yellow_pressurised() { + self.hyd_logic_inputs.yellow_edp_has_fault = true; + } else { + self.hyd_logic_inputs.yellow_edp_has_fault = false; + } + if self.blue_electric_pump.is_active() && !self.is_blue_pressurised() { + self.hyd_logic_inputs.blue_epump_has_fault = true; + } else { + self.hyd_logic_inputs.blue_epump_has_fault = false; + } + + //RAT Deployment //Todo check all other needed conditions + if !self.hyd_logic_inputs.eng_1_master_on + && !self.hyd_logic_inputs.eng_2_master_on + && ct.indicated_airspeed > Velocity::new::(100.) + //Todo get speed from ADIRS + { + self.rat.set_active(); + } + + if overhead_panel.edp1_push_button.is_auto() + && self.hyd_logic_inputs.eng_1_master_on + && !overhead_panel.eng1_fire_pb.is_released() + { + self.engine_driven_pump_1.start(); + } else if overhead_panel.edp1_push_button.is_off() { + self.engine_driven_pump_1.stop(); + } + + //FIRE valves logic for EDP1 + if overhead_panel.eng1_fire_pb.is_released() { + self.engine_driven_pump_1.stop(); + self.green_loop.set_fire_shutoff_valve_state(false); + } else { + self.green_loop.set_fire_shutoff_valve_state(true); + } + + if overhead_panel.edp2_push_button.is_auto() + && self.hyd_logic_inputs.eng_2_master_on + && !overhead_panel.eng2_fire_pb.is_released() + { + self.engine_driven_pump_2.start(); + } else if overhead_panel.edp2_push_button.is_off() { + self.engine_driven_pump_2.stop(); + } + + //FIRE valves logic for EDP2 + if overhead_panel.eng2_fire_pb.is_released() { + self.engine_driven_pump_2.stop(); + self.yellow_loop.set_fire_shutoff_valve_state(false); + } else { + self.yellow_loop.set_fire_shutoff_valve_state(true); + } + + if overhead_panel.yellow_epump_push_button.is_off() || cargo_operated_ypump { + self.yellow_electric_pump.start(); + } else if overhead_panel.yellow_epump_push_button.is_auto() { + self.yellow_electric_pump.stop(); + } + if overhead_panel.blue_epump_push_button.is_auto() { + if self.hyd_logic_inputs.eng_1_master_on + || self.hyd_logic_inputs.eng_2_master_on + || overhead_panel.blue_epump_override_push_button.is_on() + { + self.blue_electric_pump.start(); + } else { + self.blue_electric_pump.stop(); + } + } else if overhead_panel.blue_epump_push_button.is_off() { + self.blue_electric_pump.stop(); + } + + let ptu_inhibit = cargo_operated_ptu && overhead_panel.yellow_epump_push_button.is_auto(); //TODO is auto will change once auto/on button is created in overhead library + if overhead_panel.ptu_push_button.is_auto() + && (!self.hyd_logic_inputs.weight_on_wheels + || self.hyd_logic_inputs.eng_1_master_on && self.hyd_logic_inputs.eng_2_master_on + || !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on + || (!self.hyd_logic_inputs.parking_brake_applied && !nsw_pin_inserted)) + && !ptu_inhibit + { + self.ptu.enabling(true); + } else { + self.ptu.enabling(false); + } + } +} + +impl SimulationElement for A320Hydraulic { + fn accept(&mut self, visitor: &mut T) { + visitor.visit(&mut self.hyd_logic_inputs); + visitor.visit(&mut self.hyd_brake_logic); + visitor.visit(self); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64( + "HYD_GREEN_PRESSURE", + self.green_loop.get_pressure().get::(), + ); + writer.write_f64( + "HYD_GREEN_RESERVOIR", + self.green_loop.get_reservoir_volume().get::(), + ); + writer.write_bool( + "HYD_GREEN_EDPUMP_ACTIVE", + self.engine_driven_pump_1.is_active(), + ); + writer.write_bool( + "HYD_GREEN_EDPUMP_LOW_PRESS", + self.hyd_logic_inputs.green_edp_has_fault, + ); + writer.write_bool( + "HYD_GREEN_FIRE_VALVE_OPENED", + self.green_loop.get_fire_shutoff_valve_state(), + ); + + writer.write_f64( + "HYD_BLUE_PRESSURE", + self.blue_loop.get_pressure().get::(), + ); + writer.write_f64( + "HYD_BLUE_RESERVOIR", + self.blue_loop.get_reservoir_volume().get::(), + ); + writer.write_bool("HYD_BLUE_EPUMP_ACTIVE", self.blue_electric_pump.is_active()); + writer.write_bool( + "HYD_BLUE_EPUMP_LOW_PRESS", + self.hyd_logic_inputs.blue_epump_has_fault, + ); + + writer.write_f64( + "HYD_YELLOW_PRESSURE", + self.yellow_loop.get_pressure().get::(), + ); + writer.write_f64( + "HYD_YELLOW_RESERVOIR", + self.yellow_loop.get_reservoir_volume().get::(), + ); + writer.write_bool( + "HYD_YELLOW_EDPUMP_ACTIVE", + self.engine_driven_pump_2.is_active(), + ); + writer.write_bool( + "HYD_YELLOW_EDPUMP_LOW_PRESS", + self.hyd_logic_inputs.yellow_edp_has_fault, + ); + writer.write_bool( + "HYD_YELLOW_FIRE_VALVE_OPENED", + self.yellow_loop.get_fire_shutoff_valve_state(), + ); + writer.write_bool( + "HYD_YELLOW_EPUMP_ACTIVE", + self.yellow_electric_pump.is_active(), + ); + writer.write_bool( + "HYD_YELLOW_EPUMP_LOW_PRESS", + self.hyd_logic_inputs.yellow_epump_has_fault, + ); + + writer.write_bool("HYD_PTU_VALVE_OPENED", self.ptu.is_enabled()); + writer.write_bool("HYD_PTU_ACTIVE_Y2G", self.ptu.get_is_active_right_to_left()); + writer.write_bool("HYD_PTU_ACTIVE_G2Y", self.ptu.get_is_active_left_to_right()); + writer.write_f64( + "HYD_PTU_MOTOR_FLOW", + self.ptu.get_flow().get::(), + ); + + writer.write_f64("HYD_RAT_STOW_POSITION", self.rat.get_stow_position()); + + writer.write_f64("HYD_RAT_RPM", self.rat.prop.get_rpm()); + + //BRAKES + writer.write_f64( + "HYD_BRAKE_NORM_LEFT_PRESS", + self.braking_circuit_norm + .get_brake_pressure_left() + .get::(), + ); + writer.write_f64( + "HYD_BRAKE_NORM_RIGHT_PRESS", + self.braking_circuit_norm + .get_brake_pressure_right() + .get::(), + ); + writer.write_f64( + "HYD_BRAKE_ALTN_LEFT_PRESS", + self.braking_circuit_altn + .get_brake_pressure_left() + .get::(), + ); + writer.write_f64( + "HYD_BRAKE_ALTN_RIGHT_PRESS", + self.braking_circuit_altn + .get_brake_pressure_right() + .get::(), + ); + writer.write_f64( + "HYD_BRAKE_ALTN_ACC_PRESS", + self.braking_circuit_altn.get_acc_pressure().get::(), + ); + + //Send overhead fault info + writer.write_bool( + "OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT", + self.hyd_logic_inputs.green_edp_has_fault, + ); + writer.write_bool( + "OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT", + self.hyd_logic_inputs.yellow_edp_has_fault, + ); + writer.write_bool( + "OVHD_HYD_EPUMPB_PB_HAS_FAULT", + self.hyd_logic_inputs.blue_epump_has_fault, + ); + writer.write_bool( + "OVHD_HYD_EPUMPY_PB_HAS_FAULT", + self.hyd_logic_inputs.yellow_epump_has_fault, + ); + + //TODO Write here brake position based on applied pressure? + //TODO Decide here to set parking brake position to true if it's actually the case in hydraulic? + // let mut max_left_press= self.braking_circuit_norm.get_brake_pressure_left().get::().max(self.braking_circuit_altn.get_brake_pressure_left().get::()); + // max_left_press = max_left_press / 3000.0; + // max_left_press = max_left_press.min(1.0).max(0.0); + + // let mut max_right_press= self.braking_circuit_norm.get_brake_pressure_right().get::().max(self.braking_circuit_altn.get_brake_pressure_right().get::()); + // max_right_press = max_right_press / 3000.0; + // max_right_press = max_right_press.min(1.0).max(0.0); + + // writer.write_f64("BRAKE LEFT DMND", max_left_press); + // writer.write_f64("BRAKE RIGHT DMND", max_right_press); + } +} + +pub struct A320HydraulicBrakingLogic { + parking_brake_demand: bool, + weight_on_wheels: bool, + left_brake_command: f64, + right_brake_command: f64, + left_brake_green_command: f64, //Actual command sent to left green circuit + left_brake_yellow_command: f64, //Actual command sent to left yellow circuit + right_brake_green_command: f64, //Actual command sent to right green circuit + right_brake_yellow_command: f64, //Actual command sent to right yellow circuit + anti_skid_activated: bool, + autobrakes_setting: u8, +} + +//Implements brakes computers logic +impl A320HydraulicBrakingLogic { + const PARK_BRAKE_DEMAND_DYNAMIC: f64 = 0.8; //Dynamic of the parking brake application/removal in (percent/100) per s + const LOW_PASS_FILTER_BRAKE_COMMAND: f64 = 0.85; //Low pass filter on all brake commands + + pub fn new() -> A320HydraulicBrakingLogic { + A320HydraulicBrakingLogic { + parking_brake_demand: true, //Position of parking brake lever + weight_on_wheels: true, + left_brake_command: 1.0, //Command read from pedals + right_brake_command: 1.0, //Command read from pedals + left_brake_green_command: 0.0, //Actual command sent to left green circuit + left_brake_yellow_command: 1.0, //Actual command sent to left yellow circuit + right_brake_green_command: 0.0, //Actual command sent to right green circuit + right_brake_yellow_command: 1.0, //Actual command sent to right yellow circuit + anti_skid_activated: true, + autobrakes_setting: 0, + } + } + + //Updates final brake demands per hydraulic loop based on pilot pedal demands + //TODO: think about where to build those brake demands from autobrake if not from brake pedals + pub fn update_brake_demands(&mut self, delta_time_update: &Duration, green_loop: &HydLoop) { + let green_used_for_brakes = green_loop.get_pressure() + > Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED) + && self.anti_skid_activated + && !self.parking_brake_demand; + + let dynamic_increment = + A320HydraulicBrakingLogic::PARK_BRAKE_DEMAND_DYNAMIC * delta_time_update.as_secs_f64(); + + if green_used_for_brakes { + self.left_brake_green_command = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND + * self.left_brake_command + + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) + * self.left_brake_green_command; + self.right_brake_green_command = + A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND * self.right_brake_command + + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) + * self.right_brake_green_command; + + self.left_brake_yellow_command -= dynamic_increment; + self.right_brake_yellow_command -= dynamic_increment; + } else { + if !self.parking_brake_demand { + //Normal braking but using alternate circuit + self.left_brake_yellow_command = + A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND + * self.left_brake_command + + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) + * self.left_brake_yellow_command; + self.right_brake_yellow_command = + A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND + * self.right_brake_command + + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) + * self.right_brake_yellow_command; + } else { + //Else we just use parking brake + self.left_brake_yellow_command += dynamic_increment; + self.right_brake_yellow_command += dynamic_increment; + } + self.left_brake_green_command -= dynamic_increment; + self.right_brake_green_command -= dynamic_increment; + } + + //limiting final values + self.left_brake_yellow_command = self.left_brake_yellow_command.min(1.).max(0.); + self.right_brake_yellow_command = self.right_brake_yellow_command.min(1.).max(0.); + self.left_brake_green_command = self.left_brake_green_command.min(1.).max(0.); + self.right_brake_green_command = self.right_brake_green_command.min(1.).max(0.); + } + + pub fn send_brake_demands(&mut self, norm: &mut BrakeCircuit, altn: &mut BrakeCircuit) { + norm.set_brake_demand_left(self.left_brake_green_command); + norm.set_brake_demand_right(self.right_brake_green_command); + altn.set_brake_demand_left(self.left_brake_yellow_command); + altn.set_brake_demand_right(self.right_brake_yellow_command); + } +} + +impl SimulationElement for A320HydraulicBrakingLogic { + fn accept(&mut self, visitor: &mut T) { + visitor.visit(self); + } + + fn read(&mut self, state: &mut SimulatorReader) { + self.parking_brake_demand = state.read_bool("PARK_BRAKE_DMND"); //TODO see if A32nx var exists + self.weight_on_wheels = state.read_bool("SIM ON GROUND"); + self.anti_skid_activated = state.read_bool("ANTISKID ACTIVE"); + self.left_brake_command = state.read_f64("BRAKE LEFT DMND") / 100.0; + self.right_brake_command = state.read_f64("BRAKE RIGHT DMND") / 100.0; + self.autobrakes_setting = state.read_f64("AUTOBRAKES SETTING").floor() as u8; + } + + fn write(&self, writer: &mut SimulatorWriter) { + //TODO Decide here to set parking brake position to true if it's actually the case in hydraulic? + } +} + +pub struct A320HydraulicLogic { + parking_brake_applied: bool, + weight_on_wheels: bool, + eng_1_master_on: bool, + eng_2_master_on: bool, + nws_tow_engaged_timer: Duration, + cargo_door_front_pos: f64, + cargo_door_back_pos: f64, + cargo_door_front_pos_prev: f64, + cargo_door_back_pos_prev: f64, + cargo_door_timer: Duration, + cargo_door_timer_ptu: Duration, + pushback_angle: f64, + pushback_angle_prev: f64, + pushback_state: f64, + yellow_epump_has_fault: bool, + blue_epump_has_fault: bool, + green_edp_has_fault: bool, + yellow_edp_has_fault: bool, +} + +//Implements low level logic for all hydraulics commands +impl A320HydraulicLogic { + const CARGO_OPERATED_TIMEOUT_YPUMP: f64 = 20.0; //Timeout to shut off yellow epump after cargo operation + const CARGO_OPERATED_TIMEOUT_PTU: f64 = 40.0; //Timeout to keep ptu inhibited after cargo operation + const NWS_PIN_REMOVE_TIMEOUT: f64 = 15.0; //Time for ground crew to remove pin after tow + + pub fn new() -> A320HydraulicLogic { + A320HydraulicLogic { + parking_brake_applied: true, + weight_on_wheels: true, + eng_1_master_on: false, + eng_2_master_on: false, + nws_tow_engaged_timer: Duration::from_secs_f64(0.0), + cargo_door_front_pos: 0.0, + cargo_door_back_pos: 0.0, + cargo_door_front_pos_prev: 0.0, + cargo_door_back_pos_prev: 0.0, + cargo_door_timer: Duration::from_secs_f64(0.0), + cargo_door_timer_ptu: Duration::from_secs_f64(0.0), + pushback_angle: 0.0, + pushback_angle_prev: 0.0, + pushback_state: 0.0, + yellow_epump_has_fault: false, + blue_epump_has_fault: false, + green_edp_has_fault: false, + yellow_edp_has_fault: false, + } + } + + //TODO, code duplication to handle timeouts: generic function to do + pub fn is_cargo_operation_flag(&mut self, delta_time_update: &Duration) -> bool { + let cargo_door_moved = self.cargo_door_back_pos != self.cargo_door_back_pos_prev + || self.cargo_door_front_pos != self.cargo_door_front_pos_prev; + + if cargo_door_moved { + self.cargo_door_timer = + Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP); + } else { + if self.cargo_door_timer > *delta_time_update { + self.cargo_door_timer -= *delta_time_update; + } else { + self.cargo_door_timer = Duration::from_secs(0); + } + } + + self.cargo_door_timer > Duration::from_secs_f64(0.0) + } + + pub fn is_cargo_operation_ptu_flag(&mut self, delta_time_update: &Duration) -> bool { + let cargo_door_moved = self.cargo_door_back_pos != self.cargo_door_back_pos_prev + || self.cargo_door_front_pos != self.cargo_door_front_pos_prev; + + if cargo_door_moved { + self.cargo_door_timer_ptu = + Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU); + } else { + if self.cargo_door_timer_ptu > *delta_time_update { + self.cargo_door_timer_ptu -= *delta_time_update; + } else { + self.cargo_door_timer_ptu = Duration::from_secs(0); + } + } + + self.cargo_door_timer_ptu > Duration::from_secs_f64(0.0) + } + + pub fn is_nsw_pin_inserted_flag(&mut self, delta_time_update: &Duration) -> bool { + let pushback_in_progress = + (self.pushback_angle != self.pushback_angle_prev) && self.pushback_state != 3.0; + + if pushback_in_progress { + self.nws_tow_engaged_timer = + Duration::from_secs_f64(A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT); + } else { + if self.nws_tow_engaged_timer > *delta_time_update { + self.nws_tow_engaged_timer -= *delta_time_update; //TODO CHECK if rollover issue to expect if not limiting to 0 + } else { + self.nws_tow_engaged_timer = Duration::from_secs(0); + } + } + + self.nws_tow_engaged_timer > Duration::from_secs(0) + } +} + +impl SimulationElement for A320HydraulicLogic { + fn accept(&mut self, visitor: &mut T) { + visitor.visit(self); + } + + fn read(&mut self, state: &mut SimulatorReader) { + self.parking_brake_applied = state.read_bool("PARK_BRAKE_ON"); + self.eng_1_master_on = state.read_bool("ENG MASTER 1"); + self.eng_2_master_on = state.read_bool("ENG MASTER 2"); + self.weight_on_wheels = state.read_bool("SIM ON GROUND"); + + //Handling here of the previous values of cargo doors + self.cargo_door_front_pos_prev = self.cargo_door_front_pos; + self.cargo_door_front_pos = state.read_f64("CARGO FRONT POS"); + self.cargo_door_back_pos_prev = self.cargo_door_back_pos; + self.cargo_door_back_pos = state.read_f64("CARGO BACK POS"); + + //Handling here of the previous values of pushback angle. Angle keeps moving while towed. Feel free to find better hack + self.pushback_angle_prev = self.pushback_angle; + self.pushback_angle = state.read_f64("PUSHBACK ANGLE"); + self.pushback_state = state.read_f64("PUSHBACK STATE"); + } +} + +pub struct A320HydraulicOverheadPanel { + pub edp1_push_button: AutoOffFaultPushButton, + pub edp2_push_button: AutoOffFaultPushButton, + pub blue_epump_push_button: AutoOffFaultPushButton, + pub ptu_push_button: AutoOffFaultPushButton, + pub rat_push_button: AutoOffFaultPushButton, + pub yellow_epump_push_button: AutoOffFaultPushButton, + pub blue_epump_override_push_button: OnOffFaultPushButton, + pub eng1_fire_pb: FirePushButton, + pub eng2_fire_pb: FirePushButton, +} + +impl A320HydraulicOverheadPanel { + pub 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: AutoOffFaultPushButton::new_off("HYD_EPUMPY"), + blue_epump_override_push_button: OnOffFaultPushButton::new_off("HYD_EPUMPY_OVRD"), + eng1_fire_pb: FirePushButton::new("ENG1"), + eng2_fire_pb: FirePushButton::new("ENG2"), + } + } + + pub fn update(&mut self, context: &UpdateContext) {} +} + +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); + self.eng1_fire_pb.accept(visitor); + self.eng2_fire_pb.accept(visitor); + + visitor.visit(self); + } +} + +#[cfg(test)] +pub mod tests { + use std::time::Duration; + + use uom::si::{ + acceleration::{foot_per_second_squared, Acceleration}, + f64::*, + length::foot, + thermodynamic_temperature::degree_celsius, + velocity::knot, + }; + + use super::A320HydraulicLogic; + use super::A320HydraulicOverheadPanel; + use crate::UpdateContext; + + fn overhead() -> A320HydraulicOverheadPanel { + A320HydraulicOverheadPanel::new() + } + + fn hyd_logic() -> A320HydraulicLogic { + A320HydraulicLogic::new() + } + + fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(0.), + Length::new::(2000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + 0.0, + 0.0, + 0.0, + ) + } + + #[test] + fn is_nws_pin_engaged_test() { + let mut overhead = overhead(); + let mut logic = hyd_logic(); + + let update_delta = Duration::from_secs_f64(0.08); + assert!(!logic.is_nsw_pin_inserted_flag(&update_delta)); + + logic.pushback_angle = 1.001; + logic.pushback_state = 2.0; + assert!(logic.is_nsw_pin_inserted_flag(&update_delta)); } } diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 8ac5914b4c0..2a63b621dc3 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -5,7 +5,7 @@ mod pneumatic; use self::{fuel::A320Fuel, pneumatic::A320PneumaticOverheadPanel}; use electrical::{A320Electrical, A320ElectricalOverheadPanel}; -use hydraulic::A320Hydraulic; +use hydraulic::{A320Hydraulic,A320HydraulicOverheadPanel}; use systems::{ apu::{ Aps3200ApuGenerator, AuxiliaryPowerUnit, AuxiliaryPowerUnitFactory, @@ -28,6 +28,7 @@ pub struct A320 { electrical: A320Electrical, ext_pwr: ExternalPowerSource, hydraulic: A320Hydraulic, + hydraulic_overhead: A320HydraulicOverheadPanel, } impl A320 { pub fn new() -> A320 { @@ -43,6 +44,7 @@ impl A320 { electrical: A320Electrical::new(), ext_pwr: ExternalPowerSource::new(), hydraulic: A320Hydraulic::new(), + hydraulic_overhead: A320HydraulicOverheadPanel::new(), } } } @@ -80,6 +82,13 @@ impl Aircraft for A320 { &self.electrical_overhead, ); self.electrical_overhead.update_after_elec(&self.electrical); + + self.hydraulic.update( + context, + &self.engine_1, + &self.engine_2, + &self.hydraulic_overhead, + ); } } impl SimulationElement for A320 { @@ -94,6 +103,8 @@ impl SimulationElement for A320 { self.engine_2.accept(visitor); self.electrical.accept(visitor); self.ext_pwr.accept(visitor); + self.hydraulic.accept(visitor); + self.hydraulic_overhead.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 b35059250aa..59af5a9774a 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -36,6 +36,21 @@ struct A320SimulatorReaderWriter { fuel_tank_left_main_quantity: AircraftVariable, sim_on_ground: AircraftVariable, unlimited_fuel: AircraftVariable, + parking_brake: 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, + long_accel: AircraftVariable, + wheel_rpm_center: AircraftVariable, + wheel_rpm_left: AircraftVariable, + wheel_rpm_right: AircraftVariable, } impl A320SimulatorReaderWriter { fn new() -> Result> { @@ -70,6 +85,26 @@ impl A320SimulatorReaderWriter { )?, sim_on_ground: AircraftVariable::from("SIM ON GROUND", "Bool", 0)?, unlimited_fuel: AircraftVariable::from("UNLIMITED FUEL", "Bool", 0)?, + + parking_brake: AircraftVariable::from("BRAKE PARKING POSITION", "Bool", 1)?, + 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)?, + cargo_door_back_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, //TODO It is the catering door for now + 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)?, + long_accel: AircraftVariable::from( + "ACCELERATION BODY Z", + "feet per second squared", + 0, + )?, + wheel_rpm_center: AircraftVariable::from("CENTER WHEEL RPM", "Rpm", 0)?, + wheel_rpm_left: AircraftVariable::from("LEFT WHEEL RPM", "Rpm", 0)?, + wheel_rpm_right: AircraftVariable::from("RIGHT WHEEL RPM", "Rpm", 0)?, }) } } @@ -90,6 +125,21 @@ impl SimulatorReaderWriter for A320SimulatorReaderWriter { "AIRSPEED INDICATED" => self.airspeed_indicated.get(), "INDICATED ALTITUDE" => self.indicated_altitude.get(), "SIM ON GROUND" => self.sim_on_ground.get(), + "ENG MASTER 1" => self.master_eng_1.get(), + "ENG MASTER 2" => self.master_eng_2.get(), + "PARK_BRAKE_ON" => self.parking_brake.get(), + "PARK_BRAKE_DMND" => self.parking_brake_demand.get(), + "CARGO FRONT POS" => self.cargo_door_front_pos.get(), + "CARGO BACK POS" => self.cargo_door_back_pos.get(), + "PUSHBACK ANGLE" => self.pushback_angle.get(), + "PUSHBACK STATE" => self.pushback_state.get(), + "ANTISKID ACTIVE" => self.anti_skid_activated.get(), + "BRAKE LEFT DMND" => self.left_brake_command.get(), + "BRAKE RIGHT DMND" => self.right_brake_command.get(), + "LONG ACC" => self.long_accel.get(), + "WHEEL RPM CENTER" => self.wheel_rpm_center.get(), + "WHEEL RPM LEFT" => self.wheel_rpm_left.get(), + "WHEEL RPM RIGHT" => self.wheel_rpm_right.get(), _ => { lookup_named_variable(&mut self.dynamic_named_variables, "A32NX_", name).get_value() } diff --git a/src/systems/systems/Cargo.toml b/src/systems/systems/Cargo.toml index c3f093e37b0..2cb0ee3b3a7 100644 --- a/src/systems/systems/Cargo.toml +++ b/src/systems/systems/Cargo.toml @@ -8,3 +8,5 @@ edition = "2018" uom = "0.30.0" rand = "0.8.0" ntest = "0.7.2" +plotlib = "0.5.1" +rustplotlib = "0.0.4" diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs new file mode 100644 index 00000000000..0f21bfeec74 --- /dev/null +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -0,0 +1,519 @@ +use crate::{hydraulic::HydLoop, simulation::UpdateContext}; + +use std::f64::consts::E; +use std::time::Duration; +use uom::{ + si::{ + acceleration::foot_per_second_squared, + f64::*, + force::newton, + length::foot, + length::meter, + mass_density::kilogram_per_cubic_meter, + pressure::atmosphere, + pressure::pascal, + pressure::psi, + ratio::percent, + thermodynamic_temperature::{self, degree_celsius}, + time::second, + velocity::knot, + volume::cubic_inch, + volume::gallon, + volume::liter, + volume_rate::cubic_meter_per_second, + volume_rate::gallon_per_second, + }, + typenum::private::IsLessOrEqualPrivate, +}; + +pub trait ActuatorHydInterface { + fn get_used_volume(&self) -> Volume; + fn get_reservoir_return(&self) -> Volume; +} + +//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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). +// So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position +pub struct BrakeCircuit { + //Total volume used when at max braking position. + //Simple model for now we assume at max braking we have max brake displacement + total_displacement: Volume, + + //actuator position //TODO make this more dynamic with a per wheel basis instead of left/right? + current_brake_position_left: f64, + demanded_brake_position_left: f64, + pressure_applied_left: Pressure, + current_brake_position_right: f64, + demanded_brake_position_right: f64, + pressure_applied_right: Pressure, + + //Brake accumulator variables. Accumulator can have 0 volume if no accumulator + has_accumulator: bool, + accumulator_total_volume: Volume, + accumulator_gas_pressure: Pressure, + accumulator_gas_volume: Volume, + accumulator_fluid_volume: Volume, + accumulator_press_breakpoints: [f64; 9], + accumulator_flow_carac: [f64; 9], + + //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, + + accumulator_fluid_pressure_sensor_filtered: Pressure, //Fluid pressure in brake circuit filtered for cockpit gauges +} + +impl BrakeCircuit { + const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1000.0; // Nitrogen PSI + const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = + [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; + const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.01, 0.016, 0.02, 0.04, 0.1, 0.15, 0.35, 0.5]; + + //Filtered using time constant low pass: new_val = old_val + (new_val - old_val)* (1 - e^(-dt/TCONST)) + const ACC_PRESSURE_SENSOR_FILTER_TIMECONST: f64 = 0.1; //Time constant of the filter used to measure brake circuit pressure + + pub fn new( + 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; + } + + //taking care init volume is maxed at accumulator capacity: we can't exceed max_volume minus a margin for gas to compress + let limited_volume = accumulator_fluid_volume_at_init.min(accumulator_volume * 0.9); + + //If we don't start with empty accumulator we need to init pressure too + let mut gas_press_at_init = Pressure::new::(0.); + if has_accu { + gas_press_at_init = (Pressure::new::(BrakeCircuit::ACCUMULATOR_GAS_PRE_CHARGE) + * accumulator_volume) + / (accumulator_volume - limited_volume); + } + + //Fluid pressure measure is equal to gas volume in accumulator only if there is fluid in the accumulator + let mut acc_fluid_press_init = Pressure::new::(0.); + if limited_volume > Volume::new::(0.) { + acc_fluid_press_init = gas_press_at_init; + } + + BrakeCircuit { + total_displacement: total_displacement, + current_brake_position_left: 0.0, + demanded_brake_position_left: 0.0, + pressure_applied_left: Pressure::new::(0.0), + current_brake_position_right: 0.0, + demanded_brake_position_right: 0.0, + pressure_applied_right: Pressure::new::(0.0), + has_accumulator: has_accu, + accumulator_total_volume: accumulator_volume, + accumulator_gas_pressure: gas_press_at_init, + accumulator_gas_volume: accumulator_volume - limited_volume, + accumulator_fluid_volume: limited_volume, + accumulator_press_breakpoints: BrakeCircuit::ACCUMULATOR_PRESS_BREAKPTS, + accumulator_flow_carac: BrakeCircuit::ACCUMULATOR_FLOW_CARAC, + volume_to_actuator_accumulator: Volume::new::(0.), + volume_to_res_accumulator: Volume::new::(0.), + accumulator_fluid_pressure_sensor_filtered: acc_fluid_press_init, //Pressure measured after accumulator in brake circuit + } + } + + pub fn update( + &mut self, + delta_time: &Duration, + //context: &UpdateContext, + hyd_loop: &HydLoop, + ) { + let delta_vol = ((self.demanded_brake_position_left - self.current_brake_position_left) + + (self.demanded_brake_position_right - self.current_brake_position_right)) + * self.total_displacement; + + //TODO this is a duplicate of an accumulator: refactor an accumulator object usable in all hydraulics code + if self.has_accumulator { + let accumulator_delta_press = self.accumulator_gas_pressure - hyd_loop.get_pressure(); + let flow_variation = VolumeRate::new::(super::interpolation( + &self.accumulator_press_breakpoints, + &self.accumulator_flow_carac, + accumulator_delta_press.get::().abs(), + )); + + if accumulator_delta_press.get::() < 0.0 { + let volume_to_acc = flow_variation * Time::new::(delta_time.as_secs_f64()); + self.accumulator_fluid_volume += volume_to_acc; + self.accumulator_gas_volume -= volume_to_acc; + self.volume_to_actuator_accumulator += volume_to_acc; + } + + if delta_vol > Volume::new::(0.0) { + let volume_from_acc = self.accumulator_fluid_volume.min(delta_vol); + if volume_from_acc != Volume::new::(0.0) { + self.accumulator_fluid_volume -= volume_from_acc; + self.accumulator_gas_volume += volume_from_acc; + } else { + self.demanded_brake_position_left = self.current_brake_position_left; + self.demanded_brake_position_right = self.current_brake_position_right; + } + } else { + self.volume_to_res_accumulator = self.volume_to_res_accumulator + + delta_vol.abs().min(self.accumulator_fluid_volume); + } + + self.accumulator_gas_pressure = + (Pressure::new::(BrakeCircuit::ACCUMULATOR_GAS_PRE_CHARGE) + * self.accumulator_total_volume) + / (self.accumulator_total_volume - self.accumulator_fluid_volume); + } else { + //Else case if no accumulator: we just take deltavol needed or return it back to res + if delta_vol > Volume::new::(0.0) + && hyd_loop.get_pressure() >= Pressure::new::(100.) + { + self.volume_to_actuator_accumulator += delta_vol; + } else { + self.volume_to_res_accumulator += delta_vol.abs(); + } + } + + if self.accumulator_fluid_volume > Volume::new::(0.0) { + self.pressure_applied_left = + self.accumulator_gas_pressure * self.demanded_brake_position_left; + self.pressure_applied_right = + self.accumulator_gas_pressure * self.demanded_brake_position_right; + + self.accumulator_fluid_pressure_sensor_filtered = self + .accumulator_fluid_pressure_sensor_filtered + + (self.accumulator_gas_pressure - self.accumulator_fluid_pressure_sensor_filtered) + * (1. + - E.powf( + -delta_time.as_secs_f64() + / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, + )); + } else { + self.pressure_applied_left = + hyd_loop.get_pressure() * self.demanded_brake_position_left; + self.pressure_applied_right = + hyd_loop.get_pressure() * self.demanded_brake_position_right; + + self.accumulator_fluid_pressure_sensor_filtered = self + .accumulator_fluid_pressure_sensor_filtered + + (hyd_loop.get_pressure() - self.accumulator_fluid_pressure_sensor_filtered) + * (1. + - E.powf( + -delta_time.as_secs_f64() + / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, + )); + } + self.current_brake_position_left = self.demanded_brake_position_left; + self.current_brake_position_right = self.demanded_brake_position_right; + } + + 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 get_brake_pressure_left(&self) -> Pressure { + self.pressure_applied_left + } + pub fn get_brake_pressure_right(&self) -> Pressure { + self.pressure_applied_right + } + pub fn get_acc_pressure(&self) -> Pressure { + self.accumulator_fluid_pressure_sensor_filtered + } + + pub fn reset_accumulators(&mut self) { + self.volume_to_res_accumulator = Volume::new::(0.); + self.volume_to_actuator_accumulator = Volume::new::(0.); + } +} + +impl ActuatorHydInterface for BrakeCircuit { + fn get_used_volume(&self) -> Volume { + self.volume_to_actuator_accumulator + } + fn get_reservoir_return(&self) -> Volume { + self.volume_to_res_accumulator + } +} + +pub struct AutoBrakeController { + accel_targets: Vec, + num_of_modes: usize, + + current_selected_mode: usize, + + pub current_filtered_accel: Acceleration, + + pub current_accel_error: Acceleration, + accel_error_prev: Acceleration, + current_integral_term: f64, + current_brake_dmnd: f64, //Controller brake demand to satisfy autobrake mode [0:1] + + is_enabled: bool, +} + +impl AutoBrakeController { + const LONG_ACC_FILTER_TIMECONST: f64 = 0.1; + + const CONTROLLER_P_GAIN: f64 = 0.05; + const CONTROLLER_I_GAIN: f64 = 0.001; + const CONTROLLER_D_GAIN: f64 = 0.01; + + pub fn new(accel_targets: Vec) -> AutoBrakeController { + let num = accel_targets.len(); + assert!(num > 0); + AutoBrakeController { + accel_targets: accel_targets, + num_of_modes: num, + current_selected_mode: 0, + current_filtered_accel: Acceleration::new::(0.0), + current_accel_error: Acceleration::new::(0.0), + accel_error_prev: Acceleration::new::(0.0), + current_integral_term: 0., + current_brake_dmnd: 0., + is_enabled: false, + } + } + + pub fn update(&mut self, delta_time: &Duration, ct: &UpdateContext) { + self.current_filtered_accel = self.current_filtered_accel + + (ct.acc_long - self.current_filtered_accel) + * (1. + - E.powf( + -delta_time.as_secs_f64() / AutoBrakeController::LONG_ACC_FILTER_TIMECONST, + )); + + self.current_accel_error = + self.current_filtered_accel - self.accel_targets[self.current_selected_mode]; + + if self.is_enabled && ct.is_on_ground { + let pterm = self.current_accel_error.get::() + * AutoBrakeController::CONTROLLER_P_GAIN; + let dterm = (self.current_accel_error - self.accel_error_prev) + .get::() + * AutoBrakeController::CONTROLLER_D_GAIN; + self.current_integral_term += self.current_accel_error.get::() + * AutoBrakeController::CONTROLLER_I_GAIN; + + let current_brake_dmnd = pterm + self.current_integral_term + dterm; + self.current_brake_dmnd = current_brake_dmnd.min(1.).max(0.); + } else { + self.current_brake_dmnd = 0.0; + self.current_integral_term = 0.0; + } + } + + pub fn get_brake_command(&self) -> f64 { + self.current_brake_dmnd + } + + pub fn set_mode(&mut self, selected_mode: &usize) { + self.current_selected_mode = *selected_mode.min(&self.num_of_modes); + } + + pub fn set_enable(&mut self, ena: bool) { + self.is_enabled = ena; + } +} + +#[cfg(test)] +mod tests { + //use uom::si::volume_rate::VolumeRate; + //use BrakeCircuit; + use super::*; + use crate::{ + hydraulic::{HydFluid, HydLoop, LoopColor}, + simulation::{UpdateContext, UpdateContextBuilder}, + }; + + #[test] + //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s + fn brake_state_at_init() { + let init_max_vol = Volume::new::(1.5); + let mut brake_circuit_unprimed = BrakeCircuit::new( + init_max_vol, + Volume::new::(0.0), + Volume::new::(0.1), + ); + + assert!( + brake_circuit_unprimed.get_brake_pressure_left() + + brake_circuit_unprimed.get_brake_pressure_right() + < 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 mut brake_circuit_primed = + BrakeCircuit::new(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); + + assert!( + brake_circuit_unprimed.get_brake_pressure_left() + + brake_circuit_unprimed.get_brake_pressure_right() + < 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(LoopColor::Yellow); + hyd_loop.loop_pressure = Pressure::new::(2500.0); + + let mut brake_circuit_primed = + BrakeCircuit::new(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); + + assert!( + brake_circuit_primed.get_brake_pressure_left() + + brake_circuit_primed.get_brake_pressure_right() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + + assert!( + brake_circuit_primed.get_brake_pressure_left() + + brake_circuit_primed.get_brake_pressure_right() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.set_brake_demand_left(1.0); + brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + + assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(1000.)); + assert!(brake_circuit_primed.get_brake_pressure_right() <= 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(&Duration::from_secs_f64(0.1), &hyd_loop); + assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(1000.)); + assert!(brake_circuit_primed.get_brake_pressure_left() <= 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(LoopColor::Yellow); + hyd_loop.loop_pressure = Pressure::new::(2500.0); + + let mut brake_circuit_primed = + BrakeCircuit::new(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); + + assert!( + brake_circuit_primed.get_brake_pressure_left() + + brake_circuit_primed.get_brake_pressure_right() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + + assert!( + brake_circuit_primed.get_brake_pressure_left() + + brake_circuit_primed.get_brake_pressure_right() + < Pressure::new::(10.0) + ); + + brake_circuit_primed.set_brake_demand_left(1.0); + brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + + assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2500.)); + assert!(brake_circuit_primed.get_brake_pressure_right() <= 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(&Duration::from_secs_f64(0.1), &hyd_loop); + assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); + assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); + assert!(brake_circuit_primed.accumulator_fluid_volume == Volume::new::(0.0)); + } + + #[test] + fn auto_brake_controller() { + // let init_max_vol = Volume::new::(0.0); + // let mut hyd_loop = hydraulic_loop(LoopColor::Yellow); + // hyd_loop.loop_pressure= Pressure::new::(3000.0); + + // let mut brake_circuit_primed = BrakeCircuit::new( + // init_max_vol, + // init_max_vol, + // Volume::new::(0.1) + // ); + + let mut controller = AutoBrakeController::new(vec![ + Acceleration::new::(-1.5), + Acceleration::new::(-3.), + Acceleration::new::(-15.), + ]); + let mut context = context(Duration::from_secs_f64(0.)); + context.acc_long = Acceleration::new::(0.0); + + assert!(controller.get_brake_command() <= 0.0); + + controller.update(&context.delta, &context); + assert!(controller.get_brake_command() <= 0.0); + + controller.set_enable(true); + controller.update(&context.delta, &context); + assert!(controller.get_brake_command() >= 0.0); + } + + fn hydraulic_loop(loop_color: LoopColor) -> HydLoop { + match loop_color { + LoopColor::Yellow => HydLoop::new( + loop_color, + false, + true, + Volume::new::(26.00), + Volume::new::(26.41), + Volume::new::(10.0), + Volume::new::(3.83), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + LoopColor::Green => HydLoop::new( + loop_color, + true, + false, + Volume::new::(10.2), + Volume::new::(10.2), + Volume::new::(8.0), + Volume::new::(3.3), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + _ => HydLoop::new( + loop_color, + false, + false, + Volume::new::(15.85), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.5), + HydFluid::new(Pressure::new::(1450000000.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.), + 0.0, + 0.0, + 0.0, + ) + } +} diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 8b137891791..c089b2fe62b 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1 +1,2165 @@ +use std::f64::consts; +use std::time::Duration; +use std::{borrow::Borrow, cmp::Ordering, fmt::Pointer}; +use uom::{ + si::{ + acceleration::galileo, + area::square_meter, + f64::*, + force::newton, + length::foot, + length::meter, + mass_density::kilogram_per_cubic_meter, + pressure::atmosphere, + pressure::pascal, + pressure::psi, + ratio::percent, + thermodynamic_temperature::{self, degree_celsius}, + time::second, + velocity::knot, + volume::cubic_inch, + volume::gallon, + volume::liter, + volume_rate::cubic_meter_per_second, + volume_rate::gallon_per_second, + }, + typenum::private::IsLessOrEqualPrivate, +}; + +use crate::{engine::Engine, simulation::UpdateContext}; + +pub mod brakecircuit; + +// //Interpolate values_map_y at point value_at_point in breakpoints break_points_x +pub 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); + // The function also assumes xs are ordered from small to large. Consider adding a debug_assert! for that as well. + + 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]) + } +} + +// TODO: +// - Priority valve +// - Engine fire shutoff valve +// - Leak measurement valve +// - RAT pump implementation +// - Connecting electric pumps to electric sources +// - Connecting RAT pump/blue loop to emergency generator +// - Actuators +// - Bleed air sources for reservoir/line anti-cavitation + +//////////////////////////////////////////////////////////////////////////////// +// DATA & REFERENCES +//////////////////////////////////////////////////////////////////////////////// +/// +/// On A320, the reservoir level variation can, depending on the system, +/// decrease in flight by about 3.5 l (G RSVR), 4 l (Y RSVR) and 0.5 l (B RSVR) +/// +/// Each MLG door open (2 total) uses 0.25 liters each of green hyd fluid +/// Each cargo door open (3 total) uses 0.2 liters each of yellow hyd fluid +/// +/// Reservoirs +/// ------------------------------------------ +/// Normal Qty: +/// ----------- +/// Blue: 6.5L (1.7 US Gal) +/// Yellow: 12.5L (3.3 US Gal) +/// Green: 14.5L (3.8 US Gal) +/// +/// Loops +/// ------------------------------------------ +/// Max loop volume - green: 100L 26.41gal including reservoir +/// Max loop volume - yellow: 75L 19.81gal including reservoir +/// Max loop volume - blue: 50L 15.85gal including reservoir +/// +/// EDP (Eaton PV3-240-10C/D/F (F is neo)): +/// ------------------------------------------ +/// 37.5 GPM max (100% N2) +/// 3750 RPM +/// 3000 PSI +/// Displacement: 2.40 in3/rev +/// +/// +/// Electric Pump (Eaton MPEV3-032-EA2 (neo) MPEV-032-15 (ceo)): +/// ------------------------------------------ +/// Uses 115/200 VAC, 400HZ electric motor +/// 8.45 GPM max +/// 7600 RPM at full displacement, 8000 RPM at no displacement +/// 3000 PSI +/// Displacement: 0.263 in3/rev +/// +/// +/// PTU (Eaton Vickers MPHV3-115-1C): +/// ------------------------------------------ +/// 2987 PSI +/// +/// 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 +/// +/// +/// RAT PUMP (Eaton PV3-115): +/// ------------------------------------------ +/// Max displacement: 1.15 in3/rev, 18.85 mL/rev +/// Normal speed: 6,600 RPM +/// Max. Ov. Speed: 8,250 RPM +/// Theoretical Flow at normal speed: 32.86 gpm, 124.4 l/m +/// +/// +/// Equations: +/// ------------------------------------------ +/// Flow (Q), gpm: Q = (in3/rev * rpm) / 231 +/// Velocity (V), ft/s: V = (0.3208 * flow rate, gpm) / internal area, sq in +/// Force (F), lbs: F = density * area * velocity^2 +/// Pressure (P), PSI: P = force / area +/// PressureDelta = VolumeDelta / Total_uncompressed_volume * Fluid_Bulk_Modulus +/// +/// +/// Hydraulic Fluid: EXXON HyJet IV +/// ------------------------------------------ +/// Kinematic viscosity at 40C: 10.55 mm^2 s^-1, +/- 20% +/// Density at 25C: 996 kg m^-3 +/// +/// Hydraulic Line (HP) inner diameter +/// ------------------------------------------ +/// Currently unknown. Estimating to be 7.5mm for now? +/// +/// +/// Actuator Force Simvars +/// ------------------------------------------- +/// ACCELERATION BODY X (relative to aircraft, "east/west", Feet per second squared) +/// ACCELERATION BODY Y (relative to aircraft, vertical, Feet per second squared) +/// ACCELERATION BODY Z (relative to aircraft, "north/south", Feet per second squared) +/// ROTATION VELOCITY BODY X (feet per second) +/// ROTATION VELOCITY BODY Y (feet per second) +/// ROTATION VELOCITY BODY Z (feet per second) +/// VELOCITY BODY X (feet per second) +/// VELOCITY BODY Y (feet per second) +/// VELOCITY BODY Z (feet per second) +/// WING FLEX PCT (:1 for left, :2 for right; settable) (percent over 100) +/// + +//////////////////////////////////////////////////////////////////////////////// +// ENUMERATIONS +//////////////////////////////////////////////////////////////////////////////// + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ActuatorType { + Aileron, + BrakesNormal, + BrakesAlternate, + BrakesParking, + CargoDoor, + Elevator, + EmergencyGenerator, + EngReverser, + Flaps, + LandingGearNose, + LandingGearMain, + LandingGearDoorNose, + LandingGearDoorMain, + NoseWheelSteering, + Rudder, + Slat, + Spoiler, + Stabilizer, + YawDamper, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LoopColor { + Blue, + Green, + Yellow, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PtuState { + Off, + GreenToYellow, + YellowToGreen, +} + +//////////////////////////////////////////////////////////////////////////////// +// TRAITS +//////////////////////////////////////////////////////////////////////////////// + +// Trait common to all hydraulic pumps +// Max gives maximum available volume at that time as if it is a variable displacement +// pump it can be adjusted by pump regulation +// Min will give minimum volume that will be outputed no matter what. example if there is a minimal displacement or +// a fixed displacement (ie. elec pump) +pub trait PressureSource { + fn get_delta_vol_max(&self) -> Volume; + fn get_delta_vol_min(&self) -> Volume; +} + +//////////////////////////////////////////////////////////////////////////////// +// LOOP DEFINITION - INCLUDES RESERVOIR AND ACCUMULATOR +//////////////////////////////////////////////////////////////////////////////// + +//Implements fluid structure. +//TODO update method that can update physic constants from given temperature +//This would change pressure response to volume +pub struct HydFluid { + //temp : thermodynamic_temperature, + current_bulk: Pressure, +} + +impl HydFluid { + pub fn new(bulk: Pressure) -> HydFluid { + HydFluid { + //temp:temp, + current_bulk: bulk, + } + } + + pub fn get_bulk_mod(&self) -> Pressure { + return self.current_bulk; + } +} + +//Power Transfer Unit +//TODO enhance simulation with RPM and variable displacement on one side? +pub struct Ptu { + isEnabled: bool, + isActiveRight: bool, + isActiveLeft: bool, + flow_to_right: VolumeRate, + flow_to_left: VolumeRate, + last_flow: VolumeRate, +} + +impl Ptu { + //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.1; + const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.08; + + //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.7; + + pub fn new() -> Ptu { + Ptu { + isEnabled: false, + isActiveRight: false, + isActiveLeft: false, + flow_to_right: VolumeRate::new::(0.0), + flow_to_left: VolumeRate::new::(0.0), + last_flow: VolumeRate::new::(0.0), + } + } + + pub fn get_flow(&self) -> VolumeRate { + self.last_flow + } + + pub fn get_is_active(&self) -> bool { + self.isActiveRight || self.isActiveLeft + } + + pub fn is_enabled(&self) -> bool { + self.isEnabled + } + + pub fn get_is_active_left_to_right(&self) -> bool { + self.isActiveLeft + } + + pub fn get_is_active_right_to_left(&self) -> bool { + self.isActiveRight + } + + pub fn update(&mut self, loopLeft: &HydLoop, loopRight: &HydLoop) { + let deltaP = loopLeft.get_pressure() - loopRight.get_pressure(); + + //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 using PressureSource trait? + if self.isActiveLeft || (!self.isActiveRight && deltaP.get::() > 500.0) { + //Left sends flow to right + let mut vr = 16.0f64.min(loopLeft.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( + loopLeft.current_max_flow.get::() * Ptu::AGRESSIVENESS_FACTOR, + ); + + //Low pass on flow + vr = Ptu::FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE * vr + + (1.0 - Ptu::FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE) + * self.last_flow.get::(); + + self.flow_to_left = VolumeRate::new::(-vr); + self.flow_to_right = VolumeRate::new::(vr * 0.81); + self.last_flow = VolumeRate::new::(vr); + + self.isActiveLeft = true; + } else if self.isActiveRight || (!self.isActiveLeft && deltaP.get::() < -500.0) { + //Right sends flow to left + let mut vr = 34.0f64.min(loopRight.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( + loopRight.current_max_flow.get::() * Ptu::AGRESSIVENESS_FACTOR, + ); + + //Low pass on flow + vr = Ptu::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE * vr + + (1.0 - Ptu::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE) + * self.last_flow.get::(); + + self.flow_to_left = VolumeRate::new::(vr * 0.70); + self.flow_to_right = VolumeRate::new::(-vr); + self.last_flow = VolumeRate::new::(vr); + + self.isActiveRight = true; + } + + //TODO REVIEW DEACTICATION LOGIC + if !self.isEnabled + || self.isActiveRight && loopLeft.loop_pressure.get::() > 2950.0 + || self.isActiveLeft && loopRight.loop_pressure.get::() > 2950.0 + || self.isActiveRight && loopRight.loop_pressure.get::() < 500.0 + || self.isActiveLeft && loopLeft.loop_pressure.get::() < 500.0 + { + self.flow_to_left = VolumeRate::new::(0.0); + self.flow_to_right = VolumeRate::new::(0.0); + self.isActiveRight = false; + self.isActiveLeft = false; + self.last_flow = VolumeRate::new::(0.0); + } + } + + pub fn enabling(&mut self, enable_flag: bool) { + self.isEnabled = enable_flag; + } +} + +pub struct HydLoop { + fluid: HydFluid, + accumulator_gas_pressure: Pressure, + accumulator_gas_volume: Volume, + accumulator_fluid_volume: Volume, + accumulator_press_breakpoints: [f64; 9], + accumulator_flow_carac: [f64; 9], + color: LoopColor, + 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_max_flow: VolumeRate, //Current total max flow available from pressure sources + fire_shutoff_valve_opened: bool, +} + +impl HydLoop { + const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI + const ACCUMULATOR_MAX_VOLUME: f64 = 0.264; // in gallons + //const HYDRAULIC_FLUID_DENSITY: f64 = 1000.55; // Exxon Hyjet IV, kg/m^3 + + //Low pass filter on pressure. This has to be pretty high not to modify behavior of the loop, but still dampening numerical instability + const PRESSURE_LOW_PASS_FILTER: f64 = 0.75; + + const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.1; + + const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = + [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; + const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.005, 0.008, 0.01, 0.02, 0.08, 0.15, 0.35, 0.5]; + + pub fn new( + color: LoopColor, + 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: HydFluid, + ) -> HydLoop { + HydLoop { + accumulator_gas_pressure: Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE), + accumulator_gas_volume: Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME), + accumulator_fluid_volume: Volume::new::(0.), + color, + 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, + current_delta_vol: Volume::new::(0.), + current_flow: VolumeRate::new::(0.), + accumulator_press_breakpoints: HydLoop::ACCUMULATOR_PRESS_BREAKPTS, + accumulator_flow_carac: HydLoop::ACCUMULATOR_FLOW_CARAC, + current_max_flow: VolumeRate::new::(0.), + fire_shutoff_valve_opened: true, + } + } + + pub fn get_pressure(&self) -> Pressure { + self.loop_pressure + } + + pub fn get_reservoir_volume(&self) -> Volume { + self.reservoir_volume + } + + pub fn get_usable_reservoir_fluid(&self, amount: Volume) -> Volume { + let mut drawn = amount; + if amount > self.reservoir_volume { + drawn = self.reservoir_volume; + } + drawn + } + + //Returns the max flow that can be output from reservoir in dt time + pub 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 + pub fn delta_pressure_from_delta_volume(&self, delta_vol: Volume) -> Pressure { + return delta_vol / self.high_pressure_volume * self.fluid.get_bulk_mod(); + } + + //Gives the exact volume of fluid needed to get to any target_press pressure + pub fn vol_to_target(&self, target_press: Pressure) -> Volume { + (target_press - self.loop_pressure) * (self.high_pressure_volume) + / self.fluid.get_bulk_mod() + } + + pub fn set_fire_shutoff_valve_state(&mut self, opened: bool) { + self.fire_shutoff_valve_opened = opened; + } + + pub fn get_fire_shutoff_valve_state(&self) -> bool { + self.fire_shutoff_valve_opened + } + + pub fn update( + &mut self, + delta_time: &Duration, + context: &UpdateContext, + electric_pumps: Vec<&ElectricPump>, + engine_driven_pumps: Vec<&EngineDrivenPump>, + ram_air_pumps: Vec<&RatPump>, + ptus: Vec<&Ptu>, + ) { + let mut pressure = self.loop_pressure; + 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.get_delta_vol_max(); + delta_vol_min += p.get_delta_vol_min(); + } + } + for p in electric_pumps { + delta_vol_max += p.get_delta_vol_max(); + delta_vol_min += p.get_delta_vol_min(); + } + for p in ram_air_pumps { + delta_vol_max += p.get_delta_vol_max(); + delta_vol_min += p.get_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 / Time::new::(delta_time.as_secs_f64()); + + //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::( + 0.04 * delta_time.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; + + //PTU flows handling + let mut ptu_act = false; + for ptu in ptus { + let mut actualFlow = VolumeRate::new::(0.0); + if self.connected_to_ptu_left_side { + if ptu.isActiveLeft || ptu.isActiveLeft { + ptu_act = true; + } + if ptu.flow_to_left > VolumeRate::new::(0.0) { + //were are left side of PTU and positive flow so we receive flow using own reservoir + actualFlow = self.get_usable_reservoir_flow( + ptu.flow_to_left, + Time::new::(delta_time.as_secs_f64()), + ); + self.reservoir_volume -= + actualFlow * Time::new::(delta_time.as_secs_f64()); + } else { + //we are using own flow to power right side so we send that back + //to our own reservoir + actualFlow = ptu.flow_to_left; + reservoir_return -= actualFlow * Time::new::(delta_time.as_secs_f64()); + } + delta_vol += actualFlow * Time::new::(delta_time.as_secs_f64()); + } else if self.connected_to_ptu_right_side { + if ptu.isActiveLeft || ptu.isActiveLeft { + ptu_act = true; + } + if ptu.flow_to_right > VolumeRate::new::(0.0) { + //were are right side of PTU and positive flow so we receive flow using own reservoir + actualFlow = self.get_usable_reservoir_flow( + ptu.flow_to_right, + Time::new::(delta_time.as_secs_f64()), + ); + self.reservoir_volume -= + actualFlow * Time::new::(delta_time.as_secs_f64()); + } else { + //we are using own flow to power left side so we send that back + //to our own reservoir + actualFlow = ptu.flow_to_right; + reservoir_return -= actualFlow * Time::new::(delta_time.as_secs_f64()); + } + delta_vol += actualFlow * Time::new::(delta_time.as_secs_f64()); + } + } + self.ptu_active = ptu_act; + //END PTU + + //Priming the loop if not filled in + //TODO bug, ptu can't prime the loop is it is not providing flow through delta_vol_max + if self.loop_volume < self.max_loop_volume { + //} %TODO what to do if we are back under max volume and unprime the loop? + let difference = self.max_loop_volume - self.loop_volume; + // println!("---Priming diff {}", difference.get::()); + let availableFluidVol = self.reservoir_volume.min(delta_vol_max); + let delta_loop_vol = availableFluidVol.min(difference); + delta_vol_max -= delta_loop_vol; //%TODO check if we cross the deltaVolMin? + self.loop_volume += delta_loop_vol; + self.reservoir_volume -= delta_loop_vol; + // println!("---Priming vol {} / {}", self.loop_volume.get::(),self.max_loop_volume.get::()); + } else { + // println!("---Primed {}", self.loop_volume.get::()); + } + //end priming + + //ACCUMULATOR + let accumulatorDeltaPress = self.accumulator_gas_pressure - self.loop_pressure; + let flowVariation = VolumeRate::new::(interpolation( + &self.accumulator_press_breakpoints, + &self.accumulator_flow_carac, + accumulatorDeltaPress.get::().abs(), + )); + + //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.get::() > 0.0 { + let volumeFromAcc = self + .accumulator_fluid_volume + .min(flowVariation * Time::new::(delta_time.as_secs_f64())); + self.accumulator_fluid_volume -= volumeFromAcc; + self.accumulator_gas_volume += volumeFromAcc; + delta_vol += volumeFromAcc; + } else { + let volumeToAcc = delta_vol + .max(Volume::new::(0.0)) + .max(flowVariation * Time::new::(delta_time.as_secs_f64())); + self.accumulator_fluid_volume += volumeToAcc; + self.accumulator_gas_volume -= volumeToAcc; + delta_vol -= volumeToAcc; + } + + self.accumulator_gas_pressure = (Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE) + * Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME)) + / (Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME) + - self.accumulator_fluid_volume); + //END ACCUMULATOR + + //Actuators + let used_fluidQty = Volume::new::(0.); // %%total fluid used + //foreach actuator + //used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated*264.172; %264.172 is m^3 to gallons + //reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated*264.172; + //actuator.resetVolumes() + //actuator.set_available_pressure(self.loop_pressure) + //end foreach + //end actuator + + delta_vol -= used_fluidQty; + + //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; + + //Loop Pressure update From Bulk modulus + let press_delta = self.delta_pressure_from_delta_volume(delta_vol); + let new_raw_press = self.loop_pressure + press_delta; //New raw pressure before we filter it + + self.loop_pressure = HydLoop::PRESSURE_LOW_PASS_FILTER * new_raw_press + + (1. - HydLoop::PRESSURE_LOW_PASS_FILTER) * self.loop_pressure; + self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure + + //Update reservoir + self.reservoir_volume -= actual_volume_added_to_pressurise; //%limit to 0 min? for case of negative added? + self.reservoir_volume += reservoir_return; + + //Update Volumes + + //Low pass filter on final delta vol to help with stability and final flow noise + delta_vol = HydLoop::DELTA_VOL_LOW_PASS_FILTER * delta_vol + + (1. - HydLoop::DELTA_VOL_LOW_PASS_FILTER) * self.current_delta_vol; + self.loop_volume += delta_vol; + + self.current_delta_vol = delta_vol; + self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// PUMP DEFINITION +//////////////////////////////////////////////////////////////////////////////// + +pub struct Pump { + delta_vol_max: Volume, + delta_vol_min: Volume, + pressBreakpoints: [f64; 9], + displacementCarac: [f64; 9], + displacement_dynamic: f64, //Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic +} +impl Pump { + fn new( + pressBreakpoints: [f64; 9], + displacementCarac: [f64; 9], + displacement_dynamic: f64, + ) -> Pump { + Pump { + delta_vol_max: Volume::new::(0.), + delta_vol_min: Volume::new::(0.), + pressBreakpoints: pressBreakpoints, + displacementCarac: displacementCarac, + displacement_dynamic: displacement_dynamic, + } + } + + fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop, rpm: f64) { + let displacement = self.calculate_displacement(line.get_pressure()); + + let flow = Pump::calculate_flow(rpm, displacement); + + self.delta_vol_max = (1.0 - self.displacement_dynamic) * self.delta_vol_max + + self.displacement_dynamic * flow * Time::new::(delta_time.as_secs_f64()); + self.delta_vol_min = Volume::new::(0.0); + } + + fn calculate_displacement(&self, pressure: Pressure) -> Volume { + Volume::new::(interpolation( + &self.pressBreakpoints, + &self.displacementCarac, + pressure.get::(), + )) + } + + fn calculate_flow(rpm: f64, displacement: Volume) -> VolumeRate { + VolumeRate::new::(rpm * displacement.get::() / 231.0 / 60.0) + } +} +impl PressureSource for Pump { + fn get_delta_vol_max(&self) -> Volume { + self.delta_vol_max + } + + fn get_delta_vol_min(&self) -> Volume { + self.delta_vol_min + } +} + +pub struct ElectricPump { + active: bool, + rpm: f64, + pump: Pump, +} +impl ElectricPump { + const SPOOLUP_TIME: f64 = 4.0; + const SPOOLDOWN_TIME: f64 = 4.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]; + const DISPLACEMENT_DYNAMICS: f64 = 1.0; //1 == No filtering + + pub fn new() -> ElectricPump { + ElectricPump { + active: false, + rpm: 0., + pump: Pump::new( + ElectricPump::DISPLACEMENT_BREAKPTS, + ElectricPump::DISPLACEMENT_MAP, + ElectricPump::DISPLACEMENT_DYNAMICS, + ), + } + } + + pub fn start(&mut self) { + self.active = true; + } + + pub fn stop(&mut self) { + self.active = false; + } + + pub fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop) { + //TODO Simulate speed of pump depending on pump load (flow?/ current?) + //Pump startup/shutdown process + if self.active && self.rpm < ElectricPump::NOMINAL_SPEED { + self.rpm += (ElectricPump::NOMINAL_SPEED / ElectricPump::SPOOLUP_TIME) + * delta_time.as_secs_f64(); + } else if !self.active && self.rpm > 0.0 { + self.rpm -= (ElectricPump::NOMINAL_SPEED / ElectricPump::SPOOLDOWN_TIME) + * delta_time.as_secs_f64(); + } + + //Limiting min and max speed + self.rpm = self.rpm.min(ElectricPump::NOMINAL_SPEED).max(0.0); + + self.pump.update(delta_time, context, line, self.rpm); + } + + pub fn is_active(&self) -> bool { + self.active + } +} +impl PressureSource for ElectricPump { + fn get_delta_vol_max(&self) -> Volume { + self.pump.get_delta_vol_max() + } + fn get_delta_vol_min(&self) -> Volume { + self.pump.get_delta_vol_min() + } +} + +pub struct EngineDrivenPump { + active: bool, + pump: Pump, +} +impl EngineDrivenPump { + const LEAP_1A26_MAX_N2_RPM: f64 = 16645.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] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.0, 0.0]; + const MAX_RPM: f64 = 4000.; + + const DISPLACEMENT_DYNAMICS: f64 = 0.05; //0.1 == 90% filtering on max displacement transient + + pub fn new() -> EngineDrivenPump { + EngineDrivenPump { + active: false, + pump: Pump::new( + EngineDrivenPump::DISPLACEMENT_BREAKPTS, + EngineDrivenPump::DISPLACEMENT_MAP, + EngineDrivenPump::DISPLACEMENT_DYNAMICS, + ), + } + } + + pub fn update( + &mut self, + delta_time: &Duration, + context: &UpdateContext, + line: &HydLoop, + engine: &Engine, + ) { + let mut rpm = EngineDrivenPump::MAX_RPM.min( + engine.corrected_n2().get::().powi(2) * 0.08 * EngineDrivenPump::MAX_RPM + / 100.0, + ); + + //TODO Activate pumps realistically, maybe with a displacement rate limited when activated/deactivated? + if !self.active { + //Hack for pump activation + rpm = 0.0; + } + self.pump.update(delta_time, context, line, rpm); + } + + pub fn start(&mut self) { + self.active = true; + } + + pub fn stop(&mut self) { + self.active = false; + } + + pub fn is_active(&self) -> bool { + self.active + } +} +impl PressureSource for EngineDrivenPump { + fn get_delta_vol_min(&self) -> Volume { + self.pump.get_delta_vol_min() + } + fn get_delta_vol_max(&self) -> Volume { + self.pump.get_delta_vol_max() + } +} + +pub struct RatPropeller { + pos: f64, + speed: f64, + acc: f64, + rpm: f64, + torque_sum: f64, +} + +impl RatPropeller { + const LOW_SPEED_PHYSICS_ACTIVATION: f64 = 50.; //Low speed special calculation threshold. Under that value we compute resistant torque depending on pump angle and displacement + const STOWED_ANGLE: f64 = std::f64::consts::PI / 2.; + const PROPELLER_INERTIA: f64 = 2.; + const RPM_GOVERNOR_BREAKPTS: [f64; 9] = [ + 0.0, 4000., 5000.0, 5500.0, 6000.0, 7500.0, 8000.0, 9000.0, 15000.0, + ]; + const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 5., 1., 1.]; + + pub fn new() -> RatPropeller { + RatPropeller { + pos: RatPropeller::STOWED_ANGLE, + speed: 0., + acc: 0., + rpm: 0., + torque_sum: 0., + } + } + + pub fn get_rpm(&self) -> f64 { + self.rpm + } + + fn update_generated_torque(&mut self, indicated_speed: &Velocity, stow_pos: f64) { + let cur_aplha = interpolation( + &RatPropeller::RPM_GOVERNOR_BREAKPTS, + &RatPropeller::PROP_ALPHA_MAP, + self.rpm, + ); + + 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(); //simple model. stow pos sin simulates the angle of the blades vs wind while deploying + self.torque_sum += air_speed_torque; + } + + fn update_friction_torque( + &mut self, + delta_time: &Duration, + indicated_speed: &Velocity, + displacement_ratio: f64, + ) { + let mut pump_torque = 0.; + if self.rpm < RatPropeller::LOW_SPEED_PHYSICS_ACTIVATION { + pump_torque += (self.pos * 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; + self.torque_sum += pump_torque; //Static air drag of the propeller + } + + fn update_physics(&mut self, delta_time: &Duration) { + self.acc = self.torque_sum / RatPropeller::PROPELLER_INERTIA; + self.speed += self.acc * delta_time.as_secs_f64(); + self.pos += self.speed * delta_time.as_secs_f64(); + + self.rpm = self.speed * 30. / std::f64::consts::PI; //rad/s to RPM + self.torque_sum = 0.; //Reset torque accumulator at end of update + } + + pub fn update( + &mut self, + delta_time: &Duration, + indicated_speed: &Velocity, + stow_pos: f64, + displacement_ratio: f64, + ) { + self.update_generated_torque(indicated_speed, stow_pos); + self.update_friction_torque(delta_time, indicated_speed, displacement_ratio); + self.update_physics(delta_time); + } +} +pub struct RatPump { + active: bool, + pump: Pump, + pub prop: RatPropeller, + stowed_position: f64, + max_displacement: f64, +} +impl RatPump { + 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] = [1.15, 1.15, 1.15, 1.15, 1.15, 1.15, 0.5, 0.0, 0.0]; + + const DISPLACEMENT_DYNAMICS: f64 = 0.2; //1 == no filtering. !!Warning, this will be affected by a different delta time + + const STOWING_SPEED: f64 = 1.; //Speed to go from 0 to 1 stow position per sec. 1 means full deploying in 1s + + pub fn new() -> RatPump { + let mut max_disp = 0.; + for v in RatPump::DISPLACEMENT_MAP.iter() { + if v > &max_disp { + max_disp = *v; + } + } + + RatPump { + active: false, + pump: Pump::new( + RatPump::DISPLACEMENT_BREAKPTS, + RatPump::DISPLACEMENT_MAP, + RatPump::DISPLACEMENT_DYNAMICS, + ), + prop: RatPropeller::new(), + stowed_position: 0., + max_displacement: max_disp, + } + } + + pub fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop) { + self.pump + .update(delta_time, context, line, self.prop.get_rpm()); + + //Now forcing min to max to force a true real time regulation. + self.pump.delta_vol_min = self.pump.delta_vol_max; //TODO: handle this properly by calculating who produced what volume at end of hyd loop update + } + + pub fn update_physics(&mut self, delta_time: &Duration, indicated_airspeed: &Velocity) { + let displacement_ratio = self.get_delta_vol_max().get::() / self.max_displacement; //Calculate the ratio of current displacement vs max displacement as an image of the load of the pump + self.prop.update( + &delta_time, + &indicated_airspeed, + self.stowed_position, + displacement_ratio, + ); + } + + pub fn update_stow_pos(&mut self, delta_time: &Duration) { + if self.active { + self.stowed_position += delta_time.as_secs_f64() * RatPump::STOWING_SPEED; + + //Finally limiting pos in [0:1] range + if self.stowed_position < 0. { + self.stowed_position = 0.; + } else if self.stowed_position > 1. { + self.stowed_position = 1.; + } + } + } + + pub fn set_active(&mut self) { + self.active = true; + } + + pub fn get_stow_position(&self) -> f64 { + self.stowed_position + } +} +impl PressureSource for RatPump { + fn get_delta_vol_max(&self) -> Volume { + self.pump.get_delta_vol_max() + } + + fn get_delta_vol_min(&self) -> Volume { + self.pump.get_delta_vol_min() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// ACTUATOR DEFINITION +//////////////////////////////////////////////////////////////////////////////// + +pub struct Actuator { + a_type: ActuatorType, + active: bool, + affected_by_gravity: bool, + area: Area, + line: HydLoop, + neutral_is_zero: bool, + stall_load: Force, + volume_used_at_max_deflection: Volume, +} + +// TODO +impl Actuator { + pub fn new(a_type: ActuatorType, line: HydLoop) -> Actuator { + Actuator { + a_type, + active: false, + affected_by_gravity: false, + area: Area::new::(5.0), + line, + neutral_is_zero: true, + stall_load: Force::new::(47000.), + volume_used_at_max_deflection: Volume::new::(0.), + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TESTS +//////////////////////////////////////////////////////////////////////////////// + +use plotlib::page::Page; +use plotlib::repr::Plot; +use plotlib::style::{LineStyle, PointMarker, PointStyle}; +use plotlib::view::ContinuousView; + +extern crate rustplotlib; +use rustplotlib::Figure; + +fn make_figure<'a>(h: &'a History) -> Figure<'a> { + use rustplotlib::{Axes2D, Line2D}; + + let mut allAxis: Vec> = Vec::new(); + + let mut idx = 0; + for curData in &h.dataVector { + let mut currAxis = Axes2D::new() + .add( + Line2D::new(h.nameVector[idx].as_str()) + .data(&h.timeVector, &curData) + .color("blue") + //.marker("x") + //.linestyle("--") + .linewidth(1.0), + ) + .xlabel("Time [sec]") + .ylabel(h.nameVector[idx].as_str()) + .legend("best") + .xlim(0.0, *h.timeVector.last().unwrap()); + //.ylim(-2.0, 2.0); + + currAxis = currAxis.grid(true); + idx = idx + 1; + allAxis.push(Some(currAxis)); + } + + Figure::new().subplots(allAxis.len() as u32, 1, allAxis) +} + +//History class to record a simulation +pub struct History { + timeVector: Vec, //Simulation time starting from 0 + nameVector: Vec, //Name of each var saved + dataVector: Vec>, //Vector data for each var saved + dataSize: usize, +} + +impl History { + pub fn new(names: Vec) -> History { + History { + timeVector: Vec::new(), + nameVector: names.clone(), + dataVector: Vec::new(), + dataSize: names.len(), + } + } + + //Sets initialisation values of each data before first step + pub fn init(&mut self, startTime: f64, values: Vec) { + self.timeVector.push(startTime); + for idx in 0..(values.len()) { + self.dataVector.push(vec![values[idx]]); + } + } + + //Updates all values and time vector + pub fn update(&mut self, deltaTime: f64, values: Vec) { + self.timeVector + .push(self.timeVector.last().unwrap() + deltaTime); + self.pushData(values); + } + + pub fn pushData(&mut self, values: Vec) { + for idx in 0..values.len() { + self.dataVector[idx].push(values[idx]); + } + } + + //Builds a graph using rust crate plotlib + pub fn show(self) { + let mut v = ContinuousView::new() + .x_range(0.0, *self.timeVector.last().unwrap()) + .y_range(0.0, 3500.0) + .x_label("Time (s)") + .y_label("Value"); + + for curData in self.dataVector { + //Here build the 2 by Xsamples vector + let mut newVector: Vec<(f64, f64)> = Vec::new(); + for sampleIdx in 0..self.timeVector.len() { + newVector.push((self.timeVector[sampleIdx], curData[sampleIdx])); + } + + // We create our scatter plot from the data + let s1: Plot = Plot::new(newVector).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 + pub fn showMatplotlib(&self, figure_title: &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(); + + //mpl.savefig("simple.png").unwrap(); + mpl.savefig(figure_title); + //mpl.dump_pickle("simple.fig.pickle").unwrap(); + mpl.wait().unwrap(); + } +} + +#[cfg(test)] +mod tests { + //use uom::si::volume_rate::VolumeRate; + + use uom::si::acceleration::foot_per_second_squared; + + use super::*; + #[test] + //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s + fn green_loop_edp_simulation() { + let green_loop_var_names = vec![ + "Loop Pressure".to_string(), + "Loop Volume".to_string(), + "Loop Reservoir".to_string(), + "Loop Flow".to_string(), + ]; + let mut greenLoopHistory = History::new(green_loop_var_names); + + let edp1_var_names = vec!["Delta Vol Max".to_string(), "n2 ratio".to_string()]; + let mut edp1_History = History::new(edp1_var_names); + + let mut edp1 = engine_driven_pump(); + let mut green_loop = hydraulic_loop(LoopColor::Green); + edp1.active = true; + + let init_n2 = Ratio::new::(55.0); + let mut engine1 = engine(init_n2); + let ct = 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 accuGreenHistory = History::new(green_acc_var_names); + + greenLoopHistory.init( + 0.0, + vec![ + green_loop.loop_pressure.get::(), + green_loop.loop_volume.get::(), + green_loop.reservoir_volume.get::(), + green_loop.current_flow.get::(), + ], + ); + edp1_History.init( + 0.0, + vec![ + edp1.get_delta_vol_max().get::(), + engine1.n2.get::() as f64, + ], + ); + accuGreenHistory.init( + 0.0, + vec![ + green_loop.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.loop_pressure >= Pressure::new::(2950.0)); + } + if x == 200 { + assert!(green_loop.loop_pressure >= Pressure::new::(2950.0)); + edp1.stop(); + } + if x >= 500 { + //Shutdown + 30s + assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); + } + + edp1.update(&ct.delta, &ct, &green_loop, &engine1); + green_loop.update( + &ct.delta, + &ct, + Vec::new(), + vec![&edp1], + Vec::new(), + Vec::new(), + ); + if x % 20 == 0 { + println!("Iteration {}", x); + println!("-------------------------------------------"); + println!("---PSI: {}", green_loop.loop_pressure.get::()); + println!( + "--------Reservoir Volume (g): {}", + green_loop.reservoir_volume.get::() + ); + println!( + "--------Loop Volume (g): {}", + green_loop.loop_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::() + ); + } + + greenLoopHistory.update( + ct.delta.as_secs_f64(), + vec![ + green_loop.loop_pressure.get::(), + green_loop.loop_volume.get::(), + green_loop.reservoir_volume.get::(), + green_loop.current_flow.get::(), + ], + ); + edp1_History.update( + ct.delta.as_secs_f64(), + vec![ + edp1.get_delta_vol_max().get::(), + engine1.n2.get::() as f64, + ], + ); + accuGreenHistory.update( + ct.delta.as_secs_f64(), + vec![ + green_loop.loop_pressure.get::(), + green_loop.accumulator_gas_pressure.get::(), + green_loop.accumulator_fluid_volume.get::(), + green_loop.accumulator_gas_volume.get::(), + ], + ); + } + assert!(true); + + greenLoopHistory.showMatplotlib("green_loop_edp_simulation_press"); + edp1_History.showMatplotlib("green_loop_edp_simulation_EDP1 data"); + accuGreenHistory.showMatplotlib("green_loop_edp_simulation_Green Accum data"); + } + + #[test] + //Tests fixed step mechanism as implemented in A320Hydraulics + fn fixed_step_loop_test() { + use rand::Rng; + + let mut edp1 = engine_driven_pump(); + let mut green_loop = hydraulic_loop(LoopColor::Green); + edp1.active = true; + + let init_n2 = Ratio::new::(0.5); + let mut engine1 = engine(init_n2); + + let mut rng = rand::thread_rng(); + let mut real_time = Duration::from_millis(0); + let mut ct = context(Duration::from_millis(rng.gen_range(2..110))); + + let min_hyd_loop_timestep = Duration::from_millis(100); //Hyd Sim rate = 10 Hz + let mut total_sim_time_elapsed = Duration::from_millis(0); + let mut lag_time_accumulator = Duration::from_millis(0); + + while real_time < Duration::from_secs_f64(5.0) { + ct.delta = Duration::from_millis(rng.gen_range(2..110)); + real_time += ct.delta; + //println!("CALLED DELTA {:.3}", ct.delta.as_secs_f64()); + //println!("Real time: {:.3}", real_time.as_secs_f64()); + + total_sim_time_elapsed += ct.delta; + let time_to_catch = ct.delta + lag_time_accumulator; + + let numberOfSteps_f64 = + time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); + + assert!(lag_time_accumulator.as_secs_f64() < 0.2); + assert!(numberOfSteps_f64 < 5.0); + if numberOfSteps_f64 < 1.0 { + //Can't do a full time step + //we can either do an update with smaller step or wait next iteration + //Other option is to update only actuator position based on known hydraulic + //state to avoid lag of control surfaces if sim runs really fast + lag_time_accumulator = Duration::from_secs_f64( + numberOfSteps_f64 * min_hyd_loop_timestep.as_secs_f64(), + ); //Time lag is float part of num of steps * fixed time step to get a result in time + } else { + //TRUE UPDATE LOOP HERE + let num_of_update_loops = numberOfSteps_f64.floor() as u32; //Int part is the actual number of loops to do + //Rest of floating part goes into accumulator + lag_time_accumulator = Duration::from_secs_f64( + (numberOfSteps_f64 - (num_of_update_loops as f64)) + * min_hyd_loop_timestep.as_secs_f64(), + ); //Keep track of time left after all fixed loop are done + + //UPDATING HYDRAULICS AT FIXED STEP + for curLoop in 0..num_of_update_loops { + //UPDATE HYDRAULICS FIXED TIME STEP + edp1.update(&ct.delta, &ct, &green_loop, &engine1); + green_loop.update( + &ct.delta, + &ct, + Vec::new(), + vec![&edp1], + Vec::new(), + Vec::new(), + ); + //println!("---PSI: {}", green_loop.loop_pressure.get::()); + //println!("---Sim time: {:.3}", total_sim_time_elapsed.as_secs_f64()); + //println!("---Lag time: {:.3}", lag_time_accumulator.as_secs_f64()); + //println!("---num_of_update_loops: {:.1}",num_of_update_loops); + } + } + } + + assert!(lag_time_accumulator.as_secs_f64() < 1.0); + assert!((real_time - total_sim_time_elapsed).as_secs_f64().abs() < 0.2); + } + + #[test] + //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s + fn yellow_loop_epump_simulation() { + let mut epump = electric_pump(); + let mut yellow_loop = hydraulic_loop(LoopColor::Yellow); + epump.active = true; + + let ct = context(Duration::from_millis(100)); + for x in 0..800 { + if x == 400 { + assert!(yellow_loop.loop_pressure >= Pressure::new::(2800.0)); + epump.active = false; + } + + if x >= 600 { + //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low + assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); + } + epump.update(&ct.delta, &ct, &yellow_loop); + yellow_loop.update( + &ct.delta, + &ct, + vec![&epump], + Vec::new(), + Vec::new(), + Vec::new(), + ); + if x % 20 == 0 { + println!("Iteration {}", x); + println!("-------------------------------------------"); + println!("---PSI: {}", yellow_loop.loop_pressure.get::()); + println!("---RPM: {}", epump.rpm); + println!( + "--------Reservoir Volume (g): {}", + yellow_loop.reservoir_volume.get::() + ); + println!( + "--------Loop Volume (g): {}", + yellow_loop.loop_volume.get::() + ); + println!( + "--------Acc Volume (g): {}", + yellow_loop.accumulator_gas_volume.get::() + ); + } + } + + assert!(true) + } + + #[test] + //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s + fn blue_loop_epump_simulation() { + let mut epump = electric_pump(); + let mut blue_loop = hydraulic_loop(LoopColor::Blue); + epump.active = true; + + let ct = context(Duration::from_millis(100)); + for x in 0..800 { + if x == 400 { + assert!(blue_loop.loop_pressure >= Pressure::new::(2800.0)); + epump.active = false; + } + + if x >= 600 { + //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low + assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); + } + epump.update(&ct.delta, &ct, &blue_loop); + blue_loop.update( + &ct.delta, + &ct, + vec![&epump], + Vec::new(), + Vec::new(), + Vec::new(), + ); + if x % 20 == 0 { + println!("Iteration {}", x); + println!("-------------------------------------------"); + println!("---PSI: {}", blue_loop.loop_pressure.get::()); + println!("---RPM: {}", epump.rpm); + println!( + "--------Reservoir Volume (g): {}", + blue_loop.reservoir_volume.get::() + ); + println!( + "--------Loop Volume (g): {}", + blue_loop.loop_volume.get::() + ); + println!( + "--------Acc Volume (g): {}", + blue_loop.accumulator_gas_volume.get::() + ); + } + } + + assert!(true) + } + + #[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 = RatPump::new(); + let mut blue_loop = hydraulic_loop(LoopColor::Blue); + + let timestep = 0.05; + let ct = context(Duration::from_secs_f64(timestep)); + let mut indicated_airpseed = ct.indicated_airspeed; + + let mut time = 0.0; + for x in 0..1500 { + rat.update_stow_pos(&ct.delta); + if time >= 10. && time < 10. + timestep { + println!("ASSERT RAT STOWED"); + assert!(blue_loop.loop_pressure <= Pressure::new::(50.0)); + rat.active = false; + assert!(rat.stowed_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.set_active(); + } + + if time >= 30. && time < 30. + timestep { + println!("ASSERT RAT OUT AND SPINING"); + assert!(blue_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(rat.stowed_position >= 0.999); + assert!(rat.prop.rpm >= 1000.); + } + if time >= 60. && time < 60. + timestep { + println!("ASSERT RAT AT SPEED"); + assert!(blue_loop.loop_pressure >= Pressure::new::(2500.0)); + assert!(rat.prop.rpm >= 5000.); + } + + if time >= 70. && time < 70. + timestep { + println!("STOPING THE PLANE"); + indicated_airpseed = Velocity::new::(0.); + } + + if time >= 120. && time < 120. + timestep { + println!("ASSERT RAT SLOWED DOWN"); + assert!(rat.prop.rpm <= 2500.); + } + + rat.update_physics(&ct.delta, &indicated_airpseed); + rat.update(&ct.delta, &ct, &blue_loop); + blue_loop.update( + &ct.delta, + &ct, + Vec::new(), + Vec::new(), + vec![&rat], + Vec::new(), + ); + if x % 20 == 0 { + println!("Iteration {} Time {}", x, time); + println!("-------------------------------------------"); + println!("---PSI: {}", blue_loop.loop_pressure.get::()); + println!("---RAT stow pos: {}", rat.stowed_position); + println!("---RAT RPM: {}", rat.prop.rpm); + println!("---RAT volMax: {}", rat.get_delta_vol_max().get::()); + println!( + "--------Reservoir Volume (g): {}", + blue_loop.reservoir_volume.get::() + ); + println!( + "--------Loop Volume (g): {}", + blue_loop.loop_volume.get::() + ); + } + time += timestep; + } + + assert!(true) + } + + #[test] + //Runs green edp and yellow epump, checks pressure OK, + //shut green edp off, check drop of pressure and ptu effect + //shut yellow epump, check drop of pressure in both loops + fn yellow_green_ptu_loop_simulation() { + 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 LoopHistory = History::new(loop_var_names); + + let ptu_var_names = vec![ + "GREEN side flow".to_string(), + "YELLOW side 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 accuGreenHistory = 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 accuYellowHistory = History::new(yellow_acc_var_names); + + let mut epump = electric_pump(); + epump.stop(); + let mut yellow_loop = hydraulic_loop(LoopColor::Yellow); + + let mut edp1 = engine_driven_pump(); + assert!(!edp1.active); //Is off when created? + + let mut engine1 = engine(Ratio::new::(0.0)); + + let mut green_loop = hydraulic_loop(LoopColor::Green); + + let mut ptu = Ptu::new(); + + let ct = context(Duration::from_millis(100)); + + LoopHistory.init( + 0.0, + vec![ + green_loop.loop_pressure.get::(), + yellow_loop.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_to_left.get::(), + ptu.flow_to_right.get::(), + green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), + ptu.isActiveLeft as i8 as f64, + ptu.isActiveRight as i8 as f64, + ], + ); + accuGreenHistory.init( + 0.0, + vec![ + green_loop.loop_pressure.get::(), + green_loop.accumulator_gas_pressure.get::(), + green_loop.accumulator_fluid_volume.get::(), + green_loop.accumulator_gas_volume.get::(), + ], + ); + accuYellowHistory.init( + 0.0, + vec![ + yellow_loop.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; + + engine1.n2 = Ratio::new::(100.0); + for x in 0..800 { + if x == 10 { + //After 1s powering electric pump + println!("------------YELLOW EPUMP ON------------"); + assert!(yellow_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(yellow_loop.reservoir_volume == yellow_res_at_start); + + assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume == green_res_at_start); + + epump.start(); + } + + if x == 110 { + //10s later enabling ptu + println!("--------------PTU ENABLED--------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2950.0)); + assert!(yellow_loop.reservoir_volume <= yellow_res_at_start); + + assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume == green_res_at_start); + + ptu.enabling(true); + } + + if x == 300 { + //@30s, ptu should be supplying green loop + println!("----------PTU SUPPLIES GREEN------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2400.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2400.0)); + } + + if x == 400 { + //@40s enabling edp + println!("------------GREEN EDP1 ON------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); + edp1.start(); + } + + if x >= 500 && x <= 600 { + //10s later and during 10s, ptu should stay inactive + println!("------------IS PTU ACTIVE??------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(!ptu.isActiveLeft && !ptu.isActiveRight); + } + + if x == 600 { + //@60s diabling edp and epump + println!("-------------ALL PUMPS OFF------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); + edp1.stop(); + // epump.active = false; + } + + if x == 800 { + //@80s diabling edp and epump + println!("-----------IS PRESSURE OFF?-----------"); + assert!(yellow_loop.loop_pressure < Pressure::new::(50.0)); + assert!(green_loop.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); + edp1.update(&ct.delta, &ct, &green_loop, &engine1); + epump.update(&ct.delta, &ct, &yellow_loop); + + yellow_loop.update( + &ct.delta, + &ct, + vec![&epump], + Vec::new(), + Vec::new(), + vec![&ptu], + ); + green_loop.update( + &ct.delta, + &ct, + Vec::new(), + vec![&edp1], + Vec::new(), + vec![&ptu], + ); + + LoopHistory.update( + ct.delta.as_secs_f64(), + vec![ + green_loop.loop_pressure.get::(), + yellow_loop.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( + ct.delta.as_secs_f64(), + vec![ + ptu.flow_to_left.get::(), + ptu.flow_to_right.get::(), + green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), + ptu.isActiveLeft as i8 as f64, + ptu.isActiveRight as i8 as f64, + ], + ); + + accuGreenHistory.update( + ct.delta.as_secs_f64(), + vec![ + green_loop.loop_pressure.get::(), + green_loop.accumulator_gas_pressure.get::(), + green_loop.accumulator_fluid_volume.get::(), + green_loop.accumulator_gas_volume.get::(), + ], + ); + accuYellowHistory.update( + ct.delta.as_secs_f64(), + vec![ + yellow_loop.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.loop_pressure.get::()); + println!("---RPM YELLOW: {}", epump.rpm); + println!( + "---Priming State: {}/{}", + yellow_loop.loop_volume.get::(), + yellow_loop.max_loop_volume.get::() + ); + println!("---PSI GREEN: {}", green_loop.loop_pressure.get::()); + println!("---N2 GREEN: {}", engine1.n2.get::()); + println!( + "---Priming State: {}/{}", + green_loop.loop_volume.get::(), + green_loop.max_loop_volume.get::() + ); + } + } + + LoopHistory.showMatplotlib("yellow_green_ptu_loop_simulation()_Loop_press"); + ptu_history.showMatplotlib("yellow_green_ptu_loop_simulation()_PTU"); + + accuGreenHistory.showMatplotlib("yellow_green_ptu_loop_simulation()_Green_acc"); + accuYellowHistory.showMatplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); + + assert!(true) + } + + fn hydraulic_loop(loop_color: LoopColor) -> HydLoop { + match loop_color { + LoopColor::Yellow => HydLoop::new( + loop_color, + false, + true, + Volume::new::(26.00), + Volume::new::(26.41), + Volume::new::(10.0), + Volume::new::(3.83), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + LoopColor::Green => HydLoop::new( + loop_color, + true, + false, + Volume::new::(10.2), + Volume::new::(10.2), + Volume::new::(8.0), + Volume::new::(3.3), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + _ => HydLoop::new( + loop_color, + false, + false, + Volume::new::(15.85), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.5), + HydFluid::new(Pressure::new::(1450000000.0)), + ), + } + } + + fn electric_pump() -> ElectricPump { + ElectricPump::new() + } + + fn engine_driven_pump() -> EngineDrivenPump { + EngineDrivenPump::new() + } + + fn engine(n2: Ratio) -> Engine { + let mut engine = Engine::new(1); + engine.n2 = n2; + + engine + } + + fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + 0.0, + 0.0, + 0.0, + ) + } + + #[cfg(test)] + + struct PressureCaracteristic { + pressure: Pressure, + rpmTab: Vec, + flowTab: Vec, + } + + mod characteristics_tests { + use super::*; + + fn show_carac(figure_title: &str, outputCaracteristics: &Vec) { + use rustplotlib::{Axes2D, Line2D}; + + let mut allAxis: Vec> = Vec::new(); + let colors = ["blue", "yellow", "red", "black", "cyan", "magenta", "green"]; + let linestyles = ["--", "-.", "-"]; + let mut currAxis = Axes2D::new(); + currAxis = currAxis.grid(true); + let mut colorIdx = 0; + let mut styleIdx = 0; + for curPressure in outputCaracteristics { + let press_str = format!("P={:.0}", curPressure.pressure.get::()); + currAxis = currAxis + .add( + Line2D::new(press_str.as_str()) + .data(&curPressure.rpmTab, &curPressure.flowTab) + .color(colors[colorIdx]) + //.marker("x") + .linestyle(linestyles[styleIdx]) + .linewidth(1.0), + ) + .xlabel("RPM") + .ylabel("Max Flow") + .legend("best") + .xlim(0.0, *curPressure.rpmTab.last().unwrap()); + //.ylim(-2.0, 2.0); + colorIdx = (colorIdx + 1) % colors.len(); + styleIdx = (styleIdx + 1) % linestyles.len(); + } + allAxis.push(Some(currAxis)); + let fig = Figure::new().subplots(allAxis.len() as u32, 1, allAxis); + + use rustplotlib::backend::Matplotlib; + use rustplotlib::Backend; + let mut mpl = Matplotlib::new().unwrap(); + mpl.set_style("ggplot").unwrap(); + + fig.apply(&mut mpl).unwrap(); + + mpl.savefig(figure_title); + + mpl.wait().unwrap(); + } + + #[test] + fn epump_charac() { + let mut outputCaracteristics: Vec = Vec::new(); + let mut epump = ElectricPump::new(); + let context = context(Duration::from_secs_f64(0.0001)); //Small dt to freeze spool up effect + + let mut green_loop = hydraulic_loop(LoopColor::Green); + + epump.start(); + for pressure in (0..3500).step_by(500) { + let mut rpmTab: Vec = Vec::new(); + let mut flowTab: Vec = Vec::new(); + for rpm in (0..10000).step_by(150) { + green_loop.loop_pressure = Pressure::new::(pressure as f64); + epump.rpm = rpm as f64; + epump.update(&context.delta, &context, &green_loop); + rpmTab.push(rpm as f64); + let flow = epump.get_delta_vol_max() + / Time::new::(context.delta.as_secs_f64()); + let flowGal = flow.get::() as f64; + flowTab.push(flowGal); + } + outputCaracteristics.push(PressureCaracteristic { + pressure: green_loop.loop_pressure, + rpmTab, + flowTab, + }); + } + show_carac("Epump_carac", &outputCaracteristics); + } + + #[test] + //TODO broken until rpm relation repaired + fn engine_d_pump_charac() { + let mut outputCaracteristics: Vec = Vec::new(); + let mut edpump = EngineDrivenPump::new(); + //let context = context(Duration::from_secs_f64(0.0001) ); //Small dt to freeze spool up effect + + let mut green_loop = hydraulic_loop(LoopColor::Green); + let mut engine1 = engine(Ratio::new::(0.0)); + + edpump.start(); + let context = context(Duration::from_secs_f64(1.0)); //Small dt to freeze spool up effect + + edpump.update(&context.delta, &context, &green_loop, &engine1); + for pressure in (0..3500).step_by(500) { + let mut rpmTab: Vec = Vec::new(); + let mut flowTab: Vec = Vec::new(); + for rpm in (0..10000).step_by(150) { + green_loop.loop_pressure = Pressure::new::(pressure as f64); + engine1.n2 = + Ratio::new::((rpm as f64) / (4.0 * EngineDrivenPump::MAX_RPM)); + edpump.update(&context.delta, &context, &green_loop, &engine1); + rpmTab.push(rpm as f64); + let flow = edpump.get_delta_vol_max() + / Time::new::(context.delta.as_secs_f64()); + let flowGal = flow.get::() as f64; + flowTab.push(flowGal); + } + outputCaracteristics.push(PressureCaracteristic { + pressure: green_loop.loop_pressure, + rpmTab, + flowTab, + }); + } + show_carac("Eng_Driv_pump_carac", &outputCaracteristics); + } + } + + #[cfg(test)] + mod utility_tests { + use crate::hydraulic::interpolation; + use rand::Rng; + use std::time::{Duration, Instant}; + + #[test] + fn interp_test() { + let xs1 = [ + -100.0, -10.0, 10.0, 240.0, 320.0, 435.3, 678.9, 890.3, 10005.0, 203493.7, + ]; + let ys1 = [ + -200.0, 10.0, 40.0, -553.0, 238.4, 30423.3, 23000.2, 32000.4, 43200.2, 34.2, + ]; + + //Check before first element + assert!(interpolation(&xs1, &ys1, -500.0) == ys1[0]); + + //Check after last + assert!(interpolation(&xs1, &ys1, 100000000.0) == *ys1.last().unwrap()); + + //Check equal first + assert!(interpolation(&xs1, &ys1, *xs1.first().unwrap()) == *ys1.first().unwrap()); + + //Check equal last + assert!(interpolation(&xs1, &ys1, *xs1.last().unwrap()) == *ys1.last().unwrap()); + + //Check interp middle + let res = interpolation(&xs1, &ys1, 358.0); + assert!((res - 10186.589).abs() < 0.001); + + //Check interp last segment + let res = interpolation(&xs1, &ys1, 22200.0); + assert!((res - 40479.579).abs() < 0.001); + + //Check interp first segment + let res = interpolation(&xs1, &ys1, -50.0); + assert!((res - (-83.3333)).abs() < 0.001); + + //Speed check + let mut rng = rand::thread_rng(); + let timeStart = Instant::now(); + for idx in 0..1000000 { + let testVal = rng.gen_range(xs1[0]..*xs1.last().unwrap()); + let mut res = interpolation(&xs1, &ys1, testVal); + res = res + 2.78; + } + let time_elapsed = timeStart.elapsed(); + + println!( + "Time elapsed for 1000000 calls {} s", + time_elapsed.as_secs_f64() + ); + + //assert!(time_elapsed < Duration::from_millis(1500) ); + } + } + #[cfg(test)] + mod loop_tests {} + + #[cfg(test)] + mod epump_tests {} + + //TODO to update according to new caracteristics, spoolup times and displacement dynamic + // #[cfg(test)] + // mod edp_tests { + // use super::*; + // use uom::si::ratio::percent; + + // #[test] + // fn starts_inactive() { + // assert!(engine_driven_pump().active == false); + // } + + // #[test] + // fn max_flow_under_2500_psi_after_100ms() { + // let n2 = Ratio::new::(60.0); + // let pressure = Pressure::new::(2000.); + // let time = Duration::from_millis(100); + // let displacement = Volume::new::(EngineDrivenPump::DISPLACEMENT_MAP.iter().cloned().fold(-1./0. /* -inf */, f64::max)); + // assert!(delta_vol_equality_check(n2, displacement, pressure, time)) + // } + + // #[test] + // fn zero_flow_above_3000_psi_after_25ms() { + // let n2 = Ratio::new::(60.0); + // let pressure = Pressure::new::(3100.); + // let time = Duration::from_millis(25); + // let displacement = Volume::new::(0.); + // assert!(delta_vol_equality_check(n2, displacement, pressure, time)) + // } + + // fn delta_vol_equality_check( + // n2: Ratio, + // displacement: Volume, + // pressure: Pressure, + // time: Duration, + // ) -> bool { + // let actual = get_edp_actual_delta_vol_when(n2, pressure, time); + // let predicted = get_edp_predicted_delta_vol_when(n2, displacement, time); + // println!("Actual: {}", actual.get::()); + // println!("Predicted: {}", predicted.get::()); + // actual == predicted + // } + + // fn get_edp_actual_delta_vol_when(n2: Ratio, pressure: Pressure, time: Duration) -> Volume { + // let eng = engine(n2); + // let mut edp = engine_driven_pump(); + // let mut line = hydraulic_loop(LoopColor::Green); + // let mut context = context((time)); + // line.loop_pressure = pressure; + // edp.update(&time,&context, &line, &eng); + // edp.get_delta_vol_max() + // } + + // fn get_edp_predicted_delta_vol_when( + // n2: Ratio, + // displacement: Volume, + // time: Duration, + // ) -> Volume { + // let edp_rpm = (1.0f64.min(4.0 * n2.get::())) * EngineDrivenPump::MAX_RPM; + // let expected_flow = Pump::calculate_flow(edp_rpm, displacement); + // expected_flow * Time::new::(time.as_secs_f64()) + // } + // } +} 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/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index a34d83d8511..8f910bb63d4 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -1,5 +1,5 @@ use std::time::Duration; -use uom::si::{f64::*, length::foot, thermodynamic_temperature::degree_celsius, velocity::knot}; +use uom::si::{f64::*, length::foot, thermodynamic_temperature::degree_celsius, velocity::knot, acceleration::foot_per_second_squared}; use super::SimulatorReader; @@ -12,6 +12,10 @@ pub struct UpdateContext { pub indicated_altitude: Length, pub ambient_temperature: ThermodynamicTemperature, pub is_on_ground: bool, + pub acc_long: Acceleration, + pub wheel_center_rpm: f64, + pub wheel_left_rpm: f64, + pub wheel_right_rpm: f64, } impl UpdateContext { pub fn new( @@ -20,6 +24,10 @@ impl UpdateContext { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, is_on_ground: bool, + acc_long: Acceleration, + wheel_center_rpm: f64, + wheel_left_rpm: f64, + wheel_right_rpm: f64, ) -> UpdateContext { UpdateContext { delta, @@ -27,6 +35,10 @@ impl UpdateContext { indicated_altitude, ambient_temperature, is_on_ground, + acc_long: Acceleration::new::(0.), + wheel_center_rpm: 0.0, + wheel_left_rpm: 0.0, + wheel_right_rpm: 0.0, } } @@ -40,6 +52,10 @@ impl UpdateContext { indicated_altitude: Length::new::(reader.read_f64("INDICATED ALTITUDE")), is_on_ground: reader.read_bool("SIM ON GROUND"), delta: delta_time, + acc_long: Acceleration::new::(reader.read_f64("LONG ACC")), + wheel_center_rpm: reader.read_f64("WHEEL RPM CENTER"), + wheel_left_rpm: reader.read_f64("WHEEL RPM LEFT"), + wheel_right_rpm: reader.read_f64("WHEEL RPM RIGHT"), } } @@ -62,6 +78,10 @@ pub struct UpdateContextBuilder { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, on_ground: bool, + acc_long: Acceleration, + wheel_center_rpm: f64, + wheel_left_rpm: f64, + wheel_right_rpm: f64, } impl UpdateContextBuilder { fn new() -> UpdateContextBuilder { @@ -71,6 +91,10 @@ impl UpdateContextBuilder { indicated_altitude: Length::new::(5000.), ambient_temperature: ThermodynamicTemperature::new::(0.), on_ground: false, + acc_long: Acceleration::new::(0.), + wheel_center_rpm: 0.0, + wheel_left_rpm: 0.0, + wheel_right_rpm: 0.0, } } @@ -81,6 +105,10 @@ impl UpdateContextBuilder { self.indicated_altitude, self.ambient_temperature, self.on_ground, + self.acc_long, + self.wheel_center_rpm, + self.wheel_left_rpm, + self.wheel_right_rpm, ) } From ebff2c1566f0152423f06bdcab9f23f70ca62cb2 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Wed, 3 Mar 2021 09:01:26 +0100 Subject: [PATCH 002/122] doc updated --- .github/CHANGELOG.md | 1 + docs/a320-simvars.md | 114 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 104 insertions(+), 11 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index c6abdfea87b..2f6eae7bd0c 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.6.0 +1. [HYD] First hydraulics systems integration - @crocket63 (crocket) 1. [CDU] Added WIND page - @tyler58546 (tyler58546) 1. [SOUND] Improved engine startup and fly by sound - @hotshotp (Boris#9134) 1. [SOUND] Added new vent test and ext pwr relay random sounds - @hotshotp (Boris#9134) diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index d1f492e56bc..fb117e1d7b2 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -99,43 +99,43 @@ - Bool - True if pedestal door video button is being held -- A32NX_HYD_ENG1PUMP_FAULT +- A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT - Bool - True if engine 1 hyd pump fault -- A32NX_HYD_ENG1PUMP_TOGGLE +- A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO - Bool - True if engine 1 hyd pump is on -- A32NX_HYD_ENG2PUMP_FAULT +- A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT - Bool - True if engine 2 hyd pump fault -- A32NX_HYD_ENG2PUMP_TOGGLE +- A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO - Bool - True if engine 2 hyd pump is on -- A32NX_HYD_ELECPUMP_FAULT +- A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT - Bool - True if elec hyd pump fault -- A32NX_HYD_ELECPUMP_TOGGLE +- A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO - Bool - True if elec hyd pump is on/auto -- 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 +- A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT - Bool - True if yellow elec hyd pump fault -- A32NX_HYD_ELECPUMPY_TOGGLE +- A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO - Bool - True if yellow elec hyd pump is on/auto @@ -223,7 +223,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 @@ -649,3 +649,95 @@ - {number} - 1 - 2 + +- A32NX_HYD_{loop_name}_PRESSURE + - Pressure + - Current pressure in the {loop_name} hydraulic circuit + - {loop_name} + - GREEN + - BLUE + - YELLOW + +- A32NX_HYD_{loop_name}_RESERVOIR + - Volume + - 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 or green to yellow circuits + - {motor_side} + - Y2G + - G2Y + +- A32NX_HYD_PTU_MOTOR_FLOW + - Gallon per second + - Power Transfer Unit instantaneous flow in motor side + +- A32NX_HYD_RAT_STOW_POSITION + - Position [0.0 : 1.0] + - 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 From 175244c13a427b7d509268160a97760b63a25cb3 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Wed, 3 Mar 2021 10:23:32 +0100 Subject: [PATCH 003/122] Pumps PB faults added --- src/systems/a320_systems/src/hydraulic.rs | 8 +++++++- src/systems/a320_systems/src/lib.rs | 2 ++ src/systems/systems/src/overhead/mod.rs | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index e433a94983b..604a7fa55db 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -805,9 +805,15 @@ impl A320HydraulicOverheadPanel { } } - pub fn update(&mut self, context: &UpdateContext) {} + pub fn update_pb_faults(&mut self, context: &UpdateContext, hyd : &A320Hydraulic ) { + self.edp1_push_button.set_fault(hyd.hyd_logic_inputs.green_edp_has_fault); + self.edp2_push_button.set_fault(hyd.hyd_logic_inputs.yellow_edp_has_fault); + self.blue_epump_push_button.set_fault(hyd.hyd_logic_inputs.blue_epump_has_fault); + self.yellow_epump_push_button.set_fault(hyd.hyd_logic_inputs.yellow_epump_has_fault); + } } + impl SimulationElement for A320HydraulicOverheadPanel { fn accept(&mut self, visitor: &mut T) { self.edp1_push_button.accept(visitor); diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 2a63b621dc3..6d64d19803b 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -89,6 +89,8 @@ impl Aircraft for A320 { &self.engine_2, &self.hydraulic_overhead, ); + + self.hydraulic_overhead.update_pb_faults(context, &self.hydraulic); } } impl SimulationElement for A320 { diff --git a/src/systems/systems/src/overhead/mod.rs b/src/systems/systems/src/overhead/mod.rs index d4ee73c7cfd..4d4ab0a28eb 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; } } From cc4f8c296fd4e39088e182a3754e4c72748956c6 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Wed, 3 Mar 2021 11:02:01 +0100 Subject: [PATCH 004/122] New const for ALTN brake logic Brakes were switching to alternate way too late / at low green press --- src/systems/a320_systems/src/hydraulic.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 604a7fa55db..b2c42df7dc4 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -553,6 +553,7 @@ pub struct A320HydraulicBrakingLogic { impl A320HydraulicBrakingLogic { const PARK_BRAKE_DEMAND_DYNAMIC: f64 = 0.8; //Dynamic of the parking brake application/removal in (percent/100) per s const LOW_PASS_FILTER_BRAKE_COMMAND: f64 = 0.85; //Low pass filter on all brake commands + const MIN_PRESSURE_BRAKE_ALTN:f64 = 1500.; //Minimum pressure until main switched on ALTN brakes pub fn new() -> A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { @@ -572,8 +573,8 @@ impl A320HydraulicBrakingLogic { //Updates final brake demands per hydraulic loop based on pilot pedal demands //TODO: think about where to build those brake demands from autobrake if not from brake pedals pub fn update_brake_demands(&mut self, delta_time_update: &Duration, green_loop: &HydLoop) { - let green_used_for_brakes = green_loop.get_pressure() - > Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED) + let green_used_for_brakes = green_loop.get_pressure() //TODO Check this logic + > Pressure::new::(A320HydraulicBrakingLogic::MIN_PRESSURE_BRAKE_ALTN ) && self.anti_skid_activated && !self.parking_brake_demand; From 5c61a0a4b8c7ca114d631aa06de4b1f8a1ed0af5 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:27:33 +0100 Subject: [PATCH 005/122] Pump flow low limit to 0 (no negative flow) --- src/systems/systems/src/hydraulic/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index c089b2fe62b..b588994d485 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -702,7 +702,7 @@ impl Pump { fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop, rpm: f64) { let displacement = self.calculate_displacement(line.get_pressure()); - let flow = Pump::calculate_flow(rpm, displacement); + let flow = Pump::calculate_flow(rpm, displacement).max(VolumeRate::new::(0.)); self.delta_vol_max = (1.0 - self.displacement_dynamic) * self.delta_vol_max + self.displacement_dynamic * flow * Time::new::(delta_time.as_secs_f64()); From ebf14548144e0b2025c37748b1ed4d3f9ee125f5 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:46:45 +0100 Subject: [PATCH 006/122] various cleaning --- src/systems/a320_systems/src/hydraulic.rs | 6 ++--- src/systems/a320_systems/src/lib.rs | 2 +- src/systems/systems/src/hydraulic/mod.rs | 31 ++++++++++------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index b2c42df7dc4..b100f3d93f2 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -188,7 +188,7 @@ impl A320Hydraulic { ); //Keep track of time left after all fixed loop are done //UPDATING HYDRAULICS AT FIXED STEP - for cur_loop in 0..num_of_update_loops { + for _cur_loop in 0..num_of_update_loops { //Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly self.update_hyd_logic_inputs(&min_hyd_loop_timestep, &overhead_panel, &ct); @@ -261,7 +261,7 @@ impl A320Hydraulic { num_of_update_loops * A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; let delta_time_physics = min_hyd_loop_timestep / A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X - for cur_loop in 0..num_of_actuators_update_loops { + for _cur_loop in 0..num_of_actuators_update_loops { self.rat .update_physics(&delta_time_physics, &ct.indicated_airspeed); } @@ -806,7 +806,7 @@ impl A320HydraulicOverheadPanel { } } - pub fn update_pb_faults(&mut self, context: &UpdateContext, hyd : &A320Hydraulic ) { + pub fn update_pb_faults(&mut self, hyd : &A320Hydraulic ) { self.edp1_push_button.set_fault(hyd.hyd_logic_inputs.green_edp_has_fault); self.edp2_push_button.set_fault(hyd.hyd_logic_inputs.yellow_edp_has_fault); self.blue_epump_push_button.set_fault(hyd.hyd_logic_inputs.blue_epump_has_fault); diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 6d64d19803b..a1aaf4acfa8 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -90,7 +90,7 @@ impl Aircraft for A320 { &self.hydraulic_overhead, ); - self.hydraulic_overhead.update_pb_faults(context, &self.hydraulic); + self.hydraulic_overhead.update_pb_faults(&self.hydraulic); } } impl SimulationElement for A320 { diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index b588994d485..7076740b7df 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -680,21 +680,21 @@ impl HydLoop { pub struct Pump { delta_vol_max: Volume, delta_vol_min: Volume, - pressBreakpoints: [f64; 9], - displacementCarac: [f64; 9], + press_breakpoints: [f64; 9], + displacement_carac: [f64; 9], displacement_dynamic: f64, //Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic } impl Pump { fn new( - pressBreakpoints: [f64; 9], - displacementCarac: [f64; 9], + press_breakpoints: [f64; 9], + displacement_carac: [f64; 9], displacement_dynamic: f64, ) -> Pump { Pump { delta_vol_max: Volume::new::(0.), delta_vol_min: Volume::new::(0.), - pressBreakpoints: pressBreakpoints, - displacementCarac: displacementCarac, + press_breakpoints: press_breakpoints, + displacement_carac: displacement_carac, displacement_dynamic: displacement_dynamic, } } @@ -711,8 +711,8 @@ impl Pump { fn calculate_displacement(&self, pressure: Pressure) -> Volume { Volume::new::(interpolation( - &self.pressBreakpoints, - &self.displacementCarac, + &self.press_breakpoints, + &self.displacement_carac, pressure.get::(), )) } @@ -801,7 +801,6 @@ pub struct EngineDrivenPump { pump: Pump, } impl EngineDrivenPump { - const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; const DISPLACEMENT_BREAKPTS: [f64; 9] = [ 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, ]; @@ -909,8 +908,6 @@ impl RatPropeller { fn update_friction_torque( &mut self, - delta_time: &Duration, - indicated_speed: &Velocity, displacement_ratio: f64, ) { let mut pump_torque = 0.; @@ -941,7 +938,7 @@ impl RatPropeller { displacement_ratio: f64, ) { self.update_generated_torque(indicated_speed, stow_pos); - self.update_friction_torque(delta_time, indicated_speed, displacement_ratio); + self.update_friction_torque( displacement_ratio); self.update_physics(delta_time); } } @@ -1152,15 +1149,15 @@ impl History { .x_label("Time (s)") .y_label("Value"); - for curData in self.dataVector { + for cur_data in self.dataVector { //Here build the 2 by Xsamples vector - let mut newVector: Vec<(f64, f64)> = Vec::new(); - for sampleIdx in 0..self.timeVector.len() { - newVector.push((self.timeVector[sampleIdx], curData[sampleIdx])); + let mut new_vector: Vec<(f64, f64)> = Vec::new(); + for sample_idx in 0..self.timeVector.len() { + new_vector.push((self.timeVector[sample_idx], cur_data[sample_idx])); } // We create our scatter plot from the data - let s1: Plot = Plot::new(newVector).line_style(LineStyle::new().colour("#DD3355")); + let s1: Plot = Plot::new(new_vector).line_style(LineStyle::new().colour("#DD3355")); v = v.add(s1); } From 6d9226a2f4b447060ec24ab48455b50da2e813f2 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Fri, 5 Mar 2021 08:34:24 +0100 Subject: [PATCH 007/122] cleaning.... --- src/systems/a320_systems/src/hydraulic.rs | 17 ++++++++++------- src/systems/a320_systems/src/lib.rs | 2 +- src/systems/a320_systems_wasm/src/lib.rs | 2 +- .../systems/src/hydraulic/brakecircuit.rs | 18 +----------------- src/systems/systems/src/hydraulic/mod.rs | 16 +++++++--------- .../systems/src/simulation/update_context.rs | 5 ++++- 6 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index b100f3d93f2..0f8baa36f1f 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -553,7 +553,7 @@ pub struct A320HydraulicBrakingLogic { impl A320HydraulicBrakingLogic { const PARK_BRAKE_DEMAND_DYNAMIC: f64 = 0.8; //Dynamic of the parking brake application/removal in (percent/100) per s const LOW_PASS_FILTER_BRAKE_COMMAND: f64 = 0.85; //Low pass filter on all brake commands - const MIN_PRESSURE_BRAKE_ALTN:f64 = 1500.; //Minimum pressure until main switched on ALTN brakes + const MIN_PRESSURE_BRAKE_ALTN: f64 = 1500.; //Minimum pressure until main switched on ALTN brakes pub fn new() -> A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { @@ -806,15 +806,18 @@ impl A320HydraulicOverheadPanel { } } - pub fn update_pb_faults(&mut self, hyd : &A320Hydraulic ) { - self.edp1_push_button.set_fault(hyd.hyd_logic_inputs.green_edp_has_fault); - self.edp2_push_button.set_fault(hyd.hyd_logic_inputs.yellow_edp_has_fault); - self.blue_epump_push_button.set_fault(hyd.hyd_logic_inputs.blue_epump_has_fault); - self.yellow_epump_push_button.set_fault(hyd.hyd_logic_inputs.yellow_epump_has_fault); + pub fn update_pb_faults(&mut self, hyd: &A320Hydraulic) { + self.edp1_push_button + .set_fault(hyd.hyd_logic_inputs.green_edp_has_fault); + self.edp2_push_button + .set_fault(hyd.hyd_logic_inputs.yellow_edp_has_fault); + self.blue_epump_push_button + .set_fault(hyd.hyd_logic_inputs.blue_epump_has_fault); + self.yellow_epump_push_button + .set_fault(hyd.hyd_logic_inputs.yellow_epump_has_fault); } } - impl SimulationElement for A320HydraulicOverheadPanel { fn accept(&mut self, visitor: &mut T) { self.edp1_push_button.accept(visitor); diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index a1aaf4acfa8..0d9a7cd1417 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -5,7 +5,7 @@ mod pneumatic; use self::{fuel::A320Fuel, pneumatic::A320PneumaticOverheadPanel}; use electrical::{A320Electrical, A320ElectricalOverheadPanel}; -use hydraulic::{A320Hydraulic,A320HydraulicOverheadPanel}; +use hydraulic::{A320Hydraulic, A320HydraulicOverheadPanel}; use systems::{ apu::{ Aps3200ApuGenerator, AuxiliaryPowerUnit, AuxiliaryPowerUnitFactory, diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index 59af5a9774a..e42ab14201e 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -85,7 +85,7 @@ impl A320SimulatorReaderWriter { )?, sim_on_ground: AircraftVariable::from("SIM ON GROUND", "Bool", 0)?, unlimited_fuel: AircraftVariable::from("UNLIMITED FUEL", "Bool", 0)?, - + parking_brake: AircraftVariable::from("BRAKE PARKING POSITION", "Bool", 1)?, parking_brake_demand: AircraftVariable::from("BRAKE PARKING INDICATOR", "Bool", 0)?, master_eng_1: AircraftVariable::from("GENERAL ENG STARTER ACTIVE", "Bool", 1)?, diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 0f21bfeec74..558babd3982 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -4,23 +4,7 @@ use std::f64::consts::E; use std::time::Duration; use uom::{ si::{ - acceleration::foot_per_second_squared, - f64::*, - force::newton, - length::foot, - length::meter, - mass_density::kilogram_per_cubic_meter, - pressure::atmosphere, - pressure::pascal, - pressure::psi, - ratio::percent, - thermodynamic_temperature::{self, degree_celsius}, - time::second, - velocity::knot, - volume::cubic_inch, - volume::gallon, - volume::liter, - volume_rate::cubic_meter_per_second, + acceleration::foot_per_second_squared, f64::*, pressure::psi, time::second, volume::gallon, volume_rate::gallon_per_second, }, typenum::private::IsLessOrEqualPrivate, diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 7076740b7df..cfc397c4ab1 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -693,16 +693,17 @@ impl Pump { Pump { delta_vol_max: Volume::new::(0.), delta_vol_min: Volume::new::(0.), - press_breakpoints: press_breakpoints, - displacement_carac: displacement_carac, - displacement_dynamic: displacement_dynamic, + press_breakpoints, + displacement_carac, + displacement_dynamic, } } fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop, rpm: f64) { let displacement = self.calculate_displacement(line.get_pressure()); - let flow = Pump::calculate_flow(rpm, displacement).max(VolumeRate::new::(0.)); + let flow = + Pump::calculate_flow(rpm, displacement).max(VolumeRate::new::(0.)); self.delta_vol_max = (1.0 - self.displacement_dynamic) * self.delta_vol_max + self.displacement_dynamic * flow * Time::new::(delta_time.as_secs_f64()); @@ -906,10 +907,7 @@ impl RatPropeller { self.torque_sum += air_speed_torque; } - fn update_friction_torque( - &mut self, - displacement_ratio: f64, - ) { + fn update_friction_torque(&mut self, displacement_ratio: f64) { let mut pump_torque = 0.; if self.rpm < RatPropeller::LOW_SPEED_PHYSICS_ACTIVATION { pump_torque += (self.pos * 4.).cos() * displacement_ratio.max(0.35) * 35.; @@ -938,7 +936,7 @@ impl RatPropeller { displacement_ratio: f64, ) { self.update_generated_torque(indicated_speed, stow_pos); - self.update_friction_torque( displacement_ratio); + self.update_friction_torque(displacement_ratio); self.update_physics(delta_time); } } diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index 8f910bb63d4..f2581cfc098 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -1,5 +1,8 @@ use std::time::Duration; -use uom::si::{f64::*, length::foot, thermodynamic_temperature::degree_celsius, velocity::knot, acceleration::foot_per_second_squared}; +use uom::si::{ + acceleration::foot_per_second_squared, f64::*, length::foot, + thermodynamic_temperature::degree_celsius, velocity::knot, +}; use super::SimulatorReader; From d744489a190b67ecdcd6a75953de43897f94067a Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Fri, 5 Mar 2021 09:14:28 +0100 Subject: [PATCH 008/122] Updated RPM edp calculation fixed ratio --- .../green_loop_edp_simulation_EDP1 data.png | Bin 0 -> 21320 bytes ...n_loop_edp_simulation_Green Accum data.png | Bin 0 -> 32671 bytes .../green_loop_edp_simulation_press.png | Bin 0 -> 35346 bytes src/systems/systems/src/engine/mod.rs | 2 +- .../systems/src/hydraulic/brakecircuit.rs | 5 +- src/systems/systems/src/hydraulic/mod.rs | 104 +++--------------- 6 files changed, 21 insertions(+), 90 deletions(-) create mode 100644 src/systems/systems/green_loop_edp_simulation_EDP1 data.png create mode 100644 src/systems/systems/green_loop_edp_simulation_Green Accum data.png create mode 100644 src/systems/systems/green_loop_edp_simulation_press.png diff --git a/src/systems/systems/green_loop_edp_simulation_EDP1 data.png b/src/systems/systems/green_loop_edp_simulation_EDP1 data.png new file mode 100644 index 0000000000000000000000000000000000000000..7fef1c05397b123a8e820958f711155fbec038f8 GIT binary patch literal 21320 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 literal 0 HcmV?d00001 diff --git a/src/systems/systems/green_loop_edp_simulation_press.png b/src/systems/systems/green_loop_edp_simulation_press.png new file mode 100644 index 0000000000000000000000000000000000000000..e6764f3bdab7f6471ec6e3704ba1b5edc0ee8f08 GIT binary patch literal 35346 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 Engine { diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 558babd3982..0c37c1ae813 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -4,8 +4,11 @@ use std::f64::consts::E; use std::time::Duration; use uom::{ si::{ - acceleration::foot_per_second_squared, f64::*, pressure::psi, time::second, volume::gallon, + acceleration::foot_per_second_squared, f64::*, pressure::{psi,pascal}, time::second, volume::gallon, volume_rate::gallon_per_second, + thermodynamic_temperature::degree_celsius, + velocity::knot, + length::foot, }, typenum::private::IsLessOrEqualPrivate, }; diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index cfc397c4ab1..42f919353be 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -802,13 +802,18 @@ pub struct EngineDrivenPump { pump: Pump, } impl EngineDrivenPump { + const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; //according to the Type Certificate Data Sheet of LEAP 1A26 + //max N2 rpm is 116.5% @ 19391 RPM + //100% @ 16645 RPM + const PUMP_N2_GEAR_RATIO: f64 = 0.211; + 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] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.0, 0.0]; const MAX_RPM: f64 = 4000.; - const DISPLACEMENT_DYNAMICS: f64 = 0.05; //0.1 == 90% filtering on max displacement transient + const DISPLACEMENT_DYNAMICS: f64 = 0.3; //0.1 == 90% filtering on max displacement transient pub fn new() -> EngineDrivenPump { EngineDrivenPump { @@ -828,17 +833,15 @@ impl EngineDrivenPump { line: &HydLoop, engine: &Engine, ) { - let mut rpm = EngineDrivenPump::MAX_RPM.min( - engine.corrected_n2().get::().powi(2) * 0.08 * EngineDrivenPump::MAX_RPM - / 100.0, - ); + let n2_rpm = engine.corrected_n2().get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; + let mut pump_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; //TODO Activate pumps realistically, maybe with a displacement rate limited when activated/deactivated? if !self.active { //Hack for pump activation - rpm = 0.0; + pump_rpm = 0.0; } - self.pump.update(delta_time, context, line, rpm); + self.pump.update(delta_time, context, line, pump_rpm); } pub fn start(&mut self) { @@ -1232,7 +1235,7 @@ mod tests { 0.0, vec![ edp1.get_delta_vol_max().get::(), - engine1.n2.get::() as f64, + engine1.corrected_n2.get::() as f64, ], ); accuGreenHistory.init( @@ -1306,7 +1309,7 @@ mod tests { ct.delta.as_secs_f64(), vec![ edp1.get_delta_vol_max().get::(), - engine1.n2.get::() as f64, + engine1.corrected_n2.get::() as f64, ], ); accuGreenHistory.update( @@ -1326,81 +1329,6 @@ mod tests { accuGreenHistory.showMatplotlib("green_loop_edp_simulation_Green Accum data"); } - #[test] - //Tests fixed step mechanism as implemented in A320Hydraulics - fn fixed_step_loop_test() { - use rand::Rng; - - let mut edp1 = engine_driven_pump(); - let mut green_loop = hydraulic_loop(LoopColor::Green); - edp1.active = true; - - let init_n2 = Ratio::new::(0.5); - let mut engine1 = engine(init_n2); - - let mut rng = rand::thread_rng(); - let mut real_time = Duration::from_millis(0); - let mut ct = context(Duration::from_millis(rng.gen_range(2..110))); - - let min_hyd_loop_timestep = Duration::from_millis(100); //Hyd Sim rate = 10 Hz - let mut total_sim_time_elapsed = Duration::from_millis(0); - let mut lag_time_accumulator = Duration::from_millis(0); - - while real_time < Duration::from_secs_f64(5.0) { - ct.delta = Duration::from_millis(rng.gen_range(2..110)); - real_time += ct.delta; - //println!("CALLED DELTA {:.3}", ct.delta.as_secs_f64()); - //println!("Real time: {:.3}", real_time.as_secs_f64()); - - total_sim_time_elapsed += ct.delta; - let time_to_catch = ct.delta + lag_time_accumulator; - - let numberOfSteps_f64 = - time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); - - assert!(lag_time_accumulator.as_secs_f64() < 0.2); - assert!(numberOfSteps_f64 < 5.0); - if numberOfSteps_f64 < 1.0 { - //Can't do a full time step - //we can either do an update with smaller step or wait next iteration - //Other option is to update only actuator position based on known hydraulic - //state to avoid lag of control surfaces if sim runs really fast - lag_time_accumulator = Duration::from_secs_f64( - numberOfSteps_f64 * min_hyd_loop_timestep.as_secs_f64(), - ); //Time lag is float part of num of steps * fixed time step to get a result in time - } else { - //TRUE UPDATE LOOP HERE - let num_of_update_loops = numberOfSteps_f64.floor() as u32; //Int part is the actual number of loops to do - //Rest of floating part goes into accumulator - lag_time_accumulator = Duration::from_secs_f64( - (numberOfSteps_f64 - (num_of_update_loops as f64)) - * min_hyd_loop_timestep.as_secs_f64(), - ); //Keep track of time left after all fixed loop are done - - //UPDATING HYDRAULICS AT FIXED STEP - for curLoop in 0..num_of_update_loops { - //UPDATE HYDRAULICS FIXED TIME STEP - edp1.update(&ct.delta, &ct, &green_loop, &engine1); - green_loop.update( - &ct.delta, - &ct, - Vec::new(), - vec![&edp1], - Vec::new(), - Vec::new(), - ); - //println!("---PSI: {}", green_loop.loop_pressure.get::()); - //println!("---Sim time: {:.3}", total_sim_time_elapsed.as_secs_f64()); - //println!("---Lag time: {:.3}", lag_time_accumulator.as_secs_f64()); - //println!("---num_of_update_loops: {:.1}",num_of_update_loops); - } - } - } - - assert!(lag_time_accumulator.as_secs_f64() < 1.0); - assert!((real_time - total_sim_time_elapsed).as_secs_f64().abs() < 0.2); - } - #[test] //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s fn yellow_loop_epump_simulation() { @@ -1679,7 +1607,7 @@ mod tests { let yellow_res_at_start = yellow_loop.reservoir_volume; let green_res_at_start = green_loop.reservoir_volume; - engine1.n2 = Ratio::new::(100.0); + engine1.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { //After 1s powering electric pump @@ -1826,7 +1754,7 @@ mod tests { yellow_loop.max_loop_volume.get::() ); println!("---PSI GREEN: {}", green_loop.loop_pressure.get::()); - println!("---N2 GREEN: {}", engine1.n2.get::()); + println!("---N2 GREEN: {}", engine1.corrected_n2.get::()); println!( "---Priming State: {}/{}", green_loop.loop_volume.get::(), @@ -1889,7 +1817,7 @@ mod tests { fn engine(n2: Ratio) -> Engine { let mut engine = Engine::new(1); - engine.n2 = n2; + engine.corrected_n2 = n2; engine } @@ -2013,7 +1941,7 @@ mod tests { let mut flowTab: Vec = Vec::new(); for rpm in (0..10000).step_by(150) { green_loop.loop_pressure = Pressure::new::(pressure as f64); - engine1.n2 = + engine1.corrected_n2 = Ratio::new::((rpm as f64) / (4.0 * EngineDrivenPump::MAX_RPM)); edpump.update(&context.delta, &context, &green_loop, &engine1); rpmTab.push(rpm as f64); From a16c82779505e1ba7685a97d517f0a1c6ec052c2 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+davydecorps@users.noreply.github.com> Date: Sat, 6 Mar 2021 12:35:43 +0100 Subject: [PATCH 009/122] Updated loop availability state --- src/systems/a320_systems/src/hydraulic.rs | 44 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 0f8baa36f1f..edc4580ef5c 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -41,10 +41,16 @@ pub struct A320Hydraulic { total_sim_time_elapsed: Duration, lag_time_accumulator: Duration, debug_refresh_duration: Duration, + + is_green_pressurised : bool, + is_blue_pressurised : bool, + is_yellow_pressurised : bool, } impl A320Hydraulic { - const MIN_PRESS_PRESSURISED: f64 = 150.0; + const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1450.0; + const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 1750.0; + const HYDRAULIC_SIM_TIME_STEP: u64 = 100; //refresh rate of hydraulic simulation in ms const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; //refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update @@ -106,19 +112,44 @@ impl A320Hydraulic { total_sim_time_elapsed: Duration::new(0, 0), lag_time_accumulator: Duration::new(0, 0), debug_refresh_duration: Duration::new(0, 0), + + is_green_pressurised : false, + is_blue_pressurised : false, + is_yellow_pressurised : false, + } + } + + //Updates pressure available state based on pressure switches + fn update_hyd_avail_states (&mut self) { + if self.green_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) { + self.is_green_pressurised = false; + } else if self.green_loop.get_pressure() >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) { + self.is_green_pressurised = true; + } + + if self.blue_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) { + self.is_blue_pressurised = false; + } else if self.blue_loop.get_pressure() >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) { + self.is_blue_pressurised = true; + } + + if self.yellow_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) { + self.is_yellow_pressurised = false; + } else if self.yellow_loop.get_pressure() >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) { + self.is_yellow_pressurised = true; } } pub fn is_blue_pressurised(&self) -> bool { - self.blue_loop.get_pressure().get::() >= A320Hydraulic::MIN_PRESS_PRESSURISED + self.is_blue_pressurised } pub fn is_green_pressurised(&self) -> bool { - self.green_loop.get_pressure().get::() >= A320Hydraulic::MIN_PRESS_PRESSURISED + self.is_green_pressurised } pub fn is_yellow_pressurised(&self) -> bool { - self.yellow_loop.get_pressure().get::() >= A320Hydraulic::MIN_PRESS_PRESSURISED + self.is_yellow_pressurised } pub fn update( @@ -246,6 +277,8 @@ impl A320Hydraulic { Vec::new(), ); + self.update_hyd_avail_states(); + //self.autobrake_controller.update(&ct.delta, &ct); self.braking_circuit_norm .update(&min_hyd_loop_timestep, &self.green_loop); @@ -291,7 +324,8 @@ impl A320Hydraulic { .is_nsw_pin_inserted_flag(&delta_time_update); } - //Basic faults of pumps + //Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + //At current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong if self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() { self.hyd_logic_inputs.yellow_epump_has_fault = true; } else { From d37ebe1d77ac7dc52e58b999a990e900ff8d989e Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 7 Mar 2021 19:59:29 +0100 Subject: [PATCH 010/122] cleaning --- src/systems/systems/src/hydraulic/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 42f919353be..65382c880d9 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1122,17 +1122,17 @@ impl History { } //Sets initialisation values of each data before first step - pub fn init(&mut self, startTime: f64, values: Vec) { - self.timeVector.push(startTime); + pub fn init(&mut self, start_time: f64, values: Vec) { + self.timeVector.push(start_time); for idx in 0..(values.len()) { self.dataVector.push(vec![values[idx]]); } } //Updates all values and time vector - pub fn update(&mut self, deltaTime: f64, values: Vec) { + pub fn update(&mut self, delta_time: f64, values: Vec) { self.timeVector - .push(self.timeVector.last().unwrap() + deltaTime); + .push(self.timeVector.last().unwrap() + delta_time); self.pushData(values); } From c42ea450a635916b0cc71a2faf0894ea7a4b7b68 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:55:28 +0100 Subject: [PATCH 011/122] Basic brake press limiter --- .../Airliners/A320_Neo/BRK/A320_Neo_BRK.js | 6 +-- src/systems/a320_systems/src/hydraulic.rs | 44 +++++++++++++------ .../systems/src/hydraulic/brakecircuit.rs | 10 +++-- src/systems/systems/src/hydraulic/mod.rs | 7 +-- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js b/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js index f9546b27c9c..0c5a3935d29 100644 --- a/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js +++ b/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js @@ -78,7 +78,7 @@ var A320_Neo_BRK; const powerAvailable = SimVar.GetSimVarValue("L:DCPowerAvailable","Bool"); if (this.topGauge != null) { if (powerAvailable) { - this.topGauge.setValue(3 * (SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS","PSI") / 3000)); + this.topGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS","PSI") / 1000); } else { this.topGauge.setValue(0); } @@ -86,14 +86,14 @@ var A320_Neo_BRK; if (this.leftGauge != null) { if (powerAvailable) { - this.leftGauge.setValue(3 * (SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS","PSI") / 3000)); + this.leftGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS","PSI") / 1000); } else { this.leftGauge.setValue(0); } } if (this.rightGauge != null) { if (powerAvailable) { - this.rightGauge.setValue(3 * (SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS","PSI") / 3000)); + this.rightGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS","PSI") / 1000); } else { this.rightGauge.setValue(0); } diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index edc4580ef5c..5dc8ea17b5a 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -42,9 +42,9 @@ pub struct A320Hydraulic { lag_time_accumulator: Duration, debug_refresh_duration: Duration, - is_green_pressurised : bool, - is_blue_pressurised : bool, - is_yellow_pressurised : bool, + is_green_pressurised: bool, + is_blue_pressurised: bool, + is_yellow_pressurised: bool, } impl A320Hydraulic { @@ -113,29 +113,41 @@ impl A320Hydraulic { lag_time_accumulator: Duration::new(0, 0), debug_refresh_duration: Duration::new(0, 0), - is_green_pressurised : false, - is_blue_pressurised : false, - is_yellow_pressurised : false, + is_green_pressurised: false, + is_blue_pressurised: false, + is_yellow_pressurised: false, } } //Updates pressure available state based on pressure switches - fn update_hyd_avail_states (&mut self) { - if self.green_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) { + fn update_hyd_avail_states(&mut self) { + if self.green_loop.get_pressure() + <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) + { self.is_green_pressurised = false; - } else if self.green_loop.get_pressure() >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) { + } else if self.green_loop.get_pressure() + >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) + { self.is_green_pressurised = true; } - if self.blue_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) { + if self.blue_loop.get_pressure() + <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) + { self.is_blue_pressurised = false; - } else if self.blue_loop.get_pressure() >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) { + } else if self.blue_loop.get_pressure() + >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) + { self.is_blue_pressurised = true; } - if self.yellow_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) { + if self.yellow_loop.get_pressure() + <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) + { self.is_yellow_pressurised = false; - } else if self.yellow_loop.get_pressure() >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) { + } else if self.yellow_loop.get_pressure() + >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) + { self.is_yellow_pressurised = true; } } @@ -640,10 +652,16 @@ impl A320HydraulicBrakingLogic { * self.right_brake_command + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) * self.right_brake_yellow_command; + if !self.anti_skid_activated { + self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.37); + self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.37); + } } else { //Else we just use parking brake self.left_brake_yellow_command += dynamic_increment; + self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.7); self.right_brake_yellow_command += dynamic_increment; + self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.7); } self.left_brake_green_command -= dynamic_increment; self.right_brake_green_command -= dynamic_increment; diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 0c37c1ae813..b39afa9f7ed 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -4,11 +4,15 @@ use std::f64::consts::E; use std::time::Duration; use uom::{ si::{ - acceleration::foot_per_second_squared, f64::*, pressure::{psi,pascal}, time::second, volume::gallon, - volume_rate::gallon_per_second, + acceleration::foot_per_second_squared, + f64::*, + length::foot, + pressure::{pascal, psi}, thermodynamic_temperature::degree_celsius, + time::second, velocity::knot, - length::foot, + volume::gallon, + volume_rate::gallon_per_second, }, typenum::private::IsLessOrEqualPrivate, }; diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 65382c880d9..e3f68ffc99a 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -803,8 +803,8 @@ pub struct EngineDrivenPump { } impl EngineDrivenPump { const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; //according to the Type Certificate Data Sheet of LEAP 1A26 - //max N2 rpm is 116.5% @ 19391 RPM - //100% @ 16645 RPM + //max N2 rpm is 116.5% @ 19391 RPM + //100% @ 16645 RPM const PUMP_N2_GEAR_RATIO: f64 = 0.211; const DISPLACEMENT_BREAKPTS: [f64; 9] = [ @@ -833,7 +833,8 @@ impl EngineDrivenPump { line: &HydLoop, engine: &Engine, ) { - let n2_rpm = engine.corrected_n2().get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; + let n2_rpm = + engine.corrected_n2().get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; let mut pump_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; //TODO Activate pumps realistically, maybe with a displacement rate limited when activated/deactivated? From 53b34d202e54187b3c77aa179a712fa2f701c074 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 08:56:27 +0100 Subject: [PATCH 012/122] More cleaning! --- src/systems/a320_systems/src/hydraulic.rs | 64 +-- .../systems/src/hydraulic/brakecircuit.rs | 16 +- src/systems/systems/src/hydraulic/mod.rs | 369 +++++++----------- 3 files changed, 148 insertions(+), 301 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 5dc8ea17b5a..d1e969f9aa3 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,26 +1,18 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use uom::si::{ - acceleration::foot_per_second_squared, area::square_meter, f64::*, force::newton, length::foot, - length::meter, mass_density::kilogram_per_cubic_meter, pressure::atmosphere, pressure::pascal, - pressure::psi, ratio::percent, thermodynamic_temperature::degree_celsius, time::second, - velocity::knot, volume::cubic_inch, volume::gallon, volume::liter, - volume_rate::cubic_meter_per_second, volume_rate::gallon_per_second, + f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon, + volume_rate::gallon_per_second, }; //use crate::{ hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, PressureSource, Ptu, Pump, RatPump}, overhead::{OnOffFaultPushButton}, shared::DelayedTrueLogicGate}; use systems::engine::Engine; use systems::hydraulic::brakecircuit::BrakeCircuit; use systems::hydraulic::{ - ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, PressureSource, Ptu, - RatPropeller, RatPump, + ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, Ptu, RatPump, }; use systems::overhead::{AutoOffFaultPushButton, FirePushButton, OnOffFaultPushButton}; -use systems::{ - hydraulic::brakecircuit::AutoBrakeController, - simulation::{ - SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, - UpdateContext, - }, +use systems::simulation::{ + SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; pub struct A320Hydraulic { @@ -36,7 +28,6 @@ pub struct A320Hydraulic { rat: RatPump, ptu: Ptu, braking_circuit_norm: BrakeCircuit, - //autobrake_controller : AutoBrakeController, braking_circuit_altn: BrakeCircuit, total_sim_time_elapsed: Duration, lag_time_accumulator: Duration, @@ -102,7 +93,6 @@ impl A320Hydraulic { Volume::new::(0.08), ), - //autobrake_controller : AutoBrakeController::new(vec![Acceleration::new::(-5.),Acceleration::new::(-10.),Acceleration::new::(-20.)]), braking_circuit_altn: BrakeCircuit::new( Volume::new::(1.5), Volume::new::(0.0), @@ -198,17 +188,7 @@ impl A320Hydraulic { self.rat.get_stow_position(), self.rat.prop.get_rpm(), ); - // println!("---BRAKELOGIC : Ldmn={} Rdmn={} PbrakInd={} Askid={}",self.hyd_brake_logic.left_brake_command,self.hyd_brake_logic.right_brake_command,self.hyd_brake_logic.parking_brake_demand,self.hyd_brake_logic.anti_skid_activated); - // println!("---BRAKEDMNDS : LG={} RG={} LY={} RY={}",self.hyd_brake_logic.left_brake_green_command,self.hyd_brake_logic.right_brake_green_command,self.hyd_brake_logic.left_brake_yellow_command,self.hyd_brake_logic.right_brake_yellow_command); - //println!("---L={:.0} C={:.0} R={:.0}",ct.wheel_left_rpm,ct.wheel_center_rpm,ct.wheel_right_rpm); - - // let leftLock = ct.wheel_center_rpm / ct.wheel_left_rpm.max(1.); - // let rightLock = ct.wheel_center_rpm / ct.wheel_right_rpm.max(1.); - // println!("---LS={:.0} RS={:.0}",leftLock,rightLock); - // println!("---EDP1 n2={} EDP2 n2={}", engine1.n2.get::(), engine2.n2.get::()); - // println!("---EDP1 flowMax={:.1}gpm EDP2 flowMax={:.1}gpm", (self.engine_driven_pump_1.get_delta_vol_max().get::() / min_hyd_loop_timestep.as_secs_f64() )* 60.0, (self.engine_driven_pump_2.get_delta_vol_max().get::()/min_hyd_loop_timestep.as_secs_f64())*60.0); - //println!("---AutoBrakes={:.0} command={:0.2} accel={:0.2} accerror={:0.2}",self.hyd_brake_logic.autobrakes_setting,self.autobrake_controller.get_brake_command(),self.autobrake_controller.current_filtered_accel.get::(),self.autobrake_controller.current_accel_error.get::()); - // println!("---steps required: {:.2}", number_of_steps_f64); + self.debug_refresh_duration = Duration::from_secs_f64(0.0); } @@ -247,26 +227,22 @@ impl A320Hydraulic { self.ptu.update(&self.green_loop, &self.yellow_loop); self.engine_driven_pump_1.update( &min_hyd_loop_timestep, - &ct, &self.green_loop, &engine1, ); self.engine_driven_pump_2.update( &min_hyd_loop_timestep, - &ct, &self.yellow_loop, &engine2, ); self.yellow_electric_pump - .update(&min_hyd_loop_timestep, &ct, &self.yellow_loop); + .update(&min_hyd_loop_timestep, &self.yellow_loop); self.blue_electric_pump - .update(&min_hyd_loop_timestep, &ct, &self.blue_loop); + .update(&min_hyd_loop_timestep, &self.blue_loop); - self.rat - .update(&min_hyd_loop_timestep, &ct, &self.blue_loop); + self.rat.update(&min_hyd_loop_timestep, &self.blue_loop); self.green_loop.update( &min_hyd_loop_timestep, - &ct, Vec::new(), vec![&self.engine_driven_pump_1], Vec::new(), @@ -274,7 +250,6 @@ impl A320Hydraulic { ); self.yellow_loop.update( &min_hyd_loop_timestep, - &ct, vec![&self.yellow_electric_pump], vec![&self.engine_driven_pump_2], Vec::new(), @@ -282,7 +257,6 @@ impl A320Hydraulic { ); self.blue_loop.update( &min_hyd_loop_timestep, - &ct, vec![&self.blue_electric_pump], Vec::new(), vec![&self.rat], @@ -291,7 +265,6 @@ impl A320Hydraulic { self.update_hyd_avail_states(); - //self.autobrake_controller.update(&ct.delta, &ct); self.braking_circuit_norm .update(&min_hyd_loop_timestep, &self.green_loop); self.braking_circuit_altn @@ -566,19 +539,6 @@ impl SimulationElement for A320Hydraulic { "OVHD_HYD_EPUMPY_PB_HAS_FAULT", self.hyd_logic_inputs.yellow_epump_has_fault, ); - - //TODO Write here brake position based on applied pressure? - //TODO Decide here to set parking brake position to true if it's actually the case in hydraulic? - // let mut max_left_press= self.braking_circuit_norm.get_brake_pressure_left().get::().max(self.braking_circuit_altn.get_brake_pressure_left().get::()); - // max_left_press = max_left_press / 3000.0; - // max_left_press = max_left_press.min(1.0).max(0.0); - - // let mut max_right_press= self.braking_circuit_norm.get_brake_pressure_right().get::().max(self.braking_circuit_altn.get_brake_pressure_right().get::()); - // max_right_press = max_right_press / 3000.0; - // max_right_press = max_right_press.min(1.0).max(0.0); - - // writer.write_f64("BRAKE LEFT DMND", max_left_press); - // writer.write_f64("BRAKE RIGHT DMND", max_right_press); } } @@ -695,10 +655,6 @@ impl SimulationElement for A320HydraulicBrakingLogic { self.right_brake_command = state.read_f64("BRAKE RIGHT DMND") / 100.0; self.autobrakes_setting = state.read_f64("AUTOBRAKES SETTING").floor() as u8; } - - fn write(&self, writer: &mut SimulatorWriter) { - //TODO Decide here to set parking brake position to true if it's actually the case in hydraulic? - } } pub struct A320HydraulicLogic { diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index b39afa9f7ed..b31c8694cbf 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -2,19 +2,9 @@ use crate::{hydraulic::HydLoop, simulation::UpdateContext}; use std::f64::consts::E; use std::time::Duration; -use uom::{ - si::{ - acceleration::foot_per_second_squared, - f64::*, - length::foot, - pressure::{pascal, psi}, - thermodynamic_temperature::degree_celsius, - time::second, - velocity::knot, - volume::gallon, - volume_rate::gallon_per_second, - }, - typenum::private::IsLessOrEqualPrivate, +use uom::si::{ + acceleration::foot_per_second_squared, f64::*, pressure::psi, time::second, volume::gallon, + volume_rate::gallon_per_second, }; pub trait ActuatorHydInterface { diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index e3f68ffc99a..aa301ca5fc4 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1,34 +1,11 @@ -use std::f64::consts; use std::time::Duration; -use std::{borrow::Borrow, cmp::Ordering, fmt::Pointer}; - -use uom::{ - si::{ - acceleration::galileo, - area::square_meter, - f64::*, - force::newton, - length::foot, - length::meter, - mass_density::kilogram_per_cubic_meter, - pressure::atmosphere, - pressure::pascal, - pressure::psi, - ratio::percent, - thermodynamic_temperature::{self, degree_celsius}, - time::second, - velocity::knot, - volume::cubic_inch, - volume::gallon, - volume::liter, - volume_rate::cubic_meter_per_second, - volume_rate::gallon_per_second, - }, - typenum::private::IsLessOrEqualPrivate, -}; -use crate::{engine::Engine, simulation::UpdateContext}; +use uom::si::{ + f64::*, pressure::psi, ratio::percent, time::second, velocity::knot, volume::cubic_inch, + volume::gallon, volume_rate::gallon_per_second, +}; +use crate::engine::Engine; pub mod brakecircuit; // //Interpolate values_map_y at point value_at_point in breakpoints break_points_x @@ -57,16 +34,6 @@ pub fn interpolation(xs: &[f64], ys: &[f64], intermediate_x: f64) -> f64 { } } -// TODO: -// - Priority valve -// - Engine fire shutoff valve -// - Leak measurement valve -// - RAT pump implementation -// - Connecting electric pumps to electric sources -// - Connecting RAT pump/blue loop to emergency generator -// - Actuators -// - Bleed air sources for reservoir/line anti-cavitation - //////////////////////////////////////////////////////////////////////////////// // DATA & REFERENCES //////////////////////////////////////////////////////////////////////////////// @@ -170,29 +137,6 @@ pub fn interpolation(xs: &[f64], ys: &[f64], intermediate_x: f64) -> f64 { // ENUMERATIONS //////////////////////////////////////////////////////////////////////////////// -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ActuatorType { - Aileron, - BrakesNormal, - BrakesAlternate, - BrakesParking, - CargoDoor, - Elevator, - EmergencyGenerator, - EngReverser, - Flaps, - LandingGearNose, - LandingGearMain, - LandingGearDoorNose, - LandingGearDoorMain, - NoseWheelSteering, - Rudder, - Slat, - Spoiler, - Stabilizer, - YawDamper, -} - #[derive(Clone, Copy, Debug, PartialEq)] pub enum LoopColor { Blue, @@ -249,9 +193,9 @@ impl HydFluid { //Power Transfer Unit //TODO enhance simulation with RPM and variable displacement on one side? pub struct Ptu { - isEnabled: bool, - isActiveRight: bool, - isActiveLeft: bool, + is_enabled: bool, + is_active_right: bool, + is_active_left: bool, flow_to_right: VolumeRate, flow_to_left: VolumeRate, last_flow: VolumeRate, @@ -269,9 +213,9 @@ impl Ptu { pub fn new() -> Ptu { Ptu { - isEnabled: false, - isActiveRight: false, - isActiveLeft: false, + 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), @@ -283,36 +227,36 @@ impl Ptu { } pub fn get_is_active(&self) -> bool { - self.isActiveRight || self.isActiveLeft + self.is_active_right || self.is_active_left } pub fn is_enabled(&self) -> bool { - self.isEnabled + self.is_enabled } pub fn get_is_active_left_to_right(&self) -> bool { - self.isActiveLeft + self.is_active_left } pub fn get_is_active_right_to_left(&self) -> bool { - self.isActiveRight + self.is_active_right } - pub fn update(&mut self, loopLeft: &HydLoop, loopRight: &HydLoop) { - let deltaP = loopLeft.get_pressure() - loopRight.get_pressure(); + pub fn update(&mut self, loop_left: &HydLoop, loop_right: &HydLoop) { + let delta_p = loop_left.get_pressure() - loop_right.get_pressure(); //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 using PressureSource trait? - if self.isActiveLeft || (!self.isActiveRight && deltaP.get::() > 500.0) { + if self.is_active_left || (!self.is_active_right && delta_p.get::() > 500.0) { //Left sends flow to right - let mut vr = 16.0f64.min(loopLeft.loop_pressure.get::() * 0.0058) / 60.0; + 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( - loopLeft.current_max_flow.get::() * Ptu::AGRESSIVENESS_FACTOR, + loop_left.current_max_flow.get::() * Ptu::AGRESSIVENESS_FACTOR, ); //Low pass on flow @@ -324,15 +268,15 @@ impl Ptu { self.flow_to_right = VolumeRate::new::(vr * 0.81); self.last_flow = VolumeRate::new::(vr); - self.isActiveLeft = true; - } else if self.isActiveRight || (!self.isActiveLeft && deltaP.get::() < -500.0) { + self.is_active_left = true; + } else if self.is_active_right || (!self.is_active_left && delta_p.get::() < -500.0) { //Right sends flow to left - let mut vr = 34.0f64.min(loopRight.loop_pressure.get::() * 0.0125) / 60.0; + 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( - loopRight.current_max_flow.get::() * Ptu::AGRESSIVENESS_FACTOR, + loop_right.current_max_flow.get::() * Ptu::AGRESSIVENESS_FACTOR, ); //Low pass on flow @@ -344,26 +288,26 @@ impl Ptu { self.flow_to_right = VolumeRate::new::(-vr); self.last_flow = VolumeRate::new::(vr); - self.isActiveRight = true; + self.is_active_right = true; } //TODO REVIEW DEACTICATION LOGIC - if !self.isEnabled - || self.isActiveRight && loopLeft.loop_pressure.get::() > 2950.0 - || self.isActiveLeft && loopRight.loop_pressure.get::() > 2950.0 - || self.isActiveRight && loopRight.loop_pressure.get::() < 500.0 - || self.isActiveLeft && loopLeft.loop_pressure.get::() < 500.0 + if !self.is_enabled + || self.is_active_right && loop_left.loop_pressure.get::() > 2950.0 + || self.is_active_left && loop_right.loop_pressure.get::() > 2950.0 + || self.is_active_right && loop_right.loop_pressure.get::() < 500.0 + || self.is_active_left && loop_left.loop_pressure.get::() < 500.0 { self.flow_to_left = VolumeRate::new::(0.0); self.flow_to_right = VolumeRate::new::(0.0); - self.isActiveRight = false; - self.isActiveLeft = false; + self.is_active_right = false; + self.is_active_left = false; self.last_flow = VolumeRate::new::(0.0); } } pub fn enabling(&mut self, enable_flag: bool) { - self.isEnabled = enable_flag; + self.is_enabled = enable_flag; } } @@ -486,13 +430,11 @@ impl HydLoop { pub fn update( &mut self, delta_time: &Duration, - context: &UpdateContext, electric_pumps: Vec<&ElectricPump>, engine_driven_pumps: Vec<&EngineDrivenPump>, ram_air_pumps: Vec<&RatPump>, ptus: Vec<&Ptu>, ) { - let mut pressure = self.loop_pressure; let mut delta_vol_max = Volume::new::(0.); let mut delta_vol_min = Volume::new::(0.); let mut reservoir_return = Volume::new::(0.); @@ -530,45 +472,45 @@ impl HydLoop { //PTU flows handling let mut ptu_act = false; for ptu in ptus { - let mut actualFlow = VolumeRate::new::(0.0); + let mut actual_flow = VolumeRate::new::(0.0); if self.connected_to_ptu_left_side { - if ptu.isActiveLeft || ptu.isActiveLeft { + if ptu.is_active_left || ptu.is_active_right { ptu_act = true; } if ptu.flow_to_left > VolumeRate::new::(0.0) { //were are left side of PTU and positive flow so we receive flow using own reservoir - actualFlow = self.get_usable_reservoir_flow( + actual_flow = self.get_usable_reservoir_flow( ptu.flow_to_left, Time::new::(delta_time.as_secs_f64()), ); self.reservoir_volume -= - actualFlow * Time::new::(delta_time.as_secs_f64()); + actual_flow * Time::new::(delta_time.as_secs_f64()); } else { //we are using own flow to power right side so we send that back //to our own reservoir - actualFlow = ptu.flow_to_left; - reservoir_return -= actualFlow * Time::new::(delta_time.as_secs_f64()); + actual_flow = ptu.flow_to_left; + reservoir_return -= actual_flow * Time::new::(delta_time.as_secs_f64()); } - delta_vol += actualFlow * Time::new::(delta_time.as_secs_f64()); + delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); } else if self.connected_to_ptu_right_side { - if ptu.isActiveLeft || ptu.isActiveLeft { + if ptu.is_active_left || ptu.is_active_right { ptu_act = true; } if ptu.flow_to_right > VolumeRate::new::(0.0) { //were are right side of PTU and positive flow so we receive flow using own reservoir - actualFlow = self.get_usable_reservoir_flow( + actual_flow = self.get_usable_reservoir_flow( ptu.flow_to_right, Time::new::(delta_time.as_secs_f64()), ); self.reservoir_volume -= - actualFlow * Time::new::(delta_time.as_secs_f64()); + actual_flow * Time::new::(delta_time.as_secs_f64()); } else { //we are using own flow to power left side so we send that back //to our own reservoir - actualFlow = ptu.flow_to_right; - reservoir_return -= actualFlow * Time::new::(delta_time.as_secs_f64()); + actual_flow = ptu.flow_to_right; + reservoir_return -= actual_flow * Time::new::(delta_time.as_secs_f64()); } - delta_vol += actualFlow * Time::new::(delta_time.as_secs_f64()); + delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); } } self.ptu_active = ptu_act; @@ -592,30 +534,30 @@ impl HydLoop { //end priming //ACCUMULATOR - let accumulatorDeltaPress = self.accumulator_gas_pressure - self.loop_pressure; - let flowVariation = VolumeRate::new::(interpolation( + let accumulator_delta_press = self.accumulator_gas_pressure - self.loop_pressure; + let flow_variation = VolumeRate::new::(interpolation( &self.accumulator_press_breakpoints, &self.accumulator_flow_carac, - accumulatorDeltaPress.get::().abs(), + accumulator_delta_press.get::().abs(), )); //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.get::() > 0.0 { - let volumeFromAcc = self + if accumulator_delta_press.get::() > 0.0 { + let volume_from_acc = self .accumulator_fluid_volume - .min(flowVariation * Time::new::(delta_time.as_secs_f64())); - self.accumulator_fluid_volume -= volumeFromAcc; - self.accumulator_gas_volume += volumeFromAcc; - delta_vol += volumeFromAcc; + .min(flow_variation * Time::new::(delta_time.as_secs_f64())); + self.accumulator_fluid_volume -= volume_from_acc; + self.accumulator_gas_volume += volume_from_acc; + delta_vol += volume_from_acc; } else { - let volumeToAcc = delta_vol + let volume_to_acc = delta_vol .max(Volume::new::(0.0)) - .max(flowVariation * Time::new::(delta_time.as_secs_f64())); - self.accumulator_fluid_volume += volumeToAcc; - self.accumulator_gas_volume -= volumeToAcc; - delta_vol -= volumeToAcc; + .max(flow_variation * Time::new::(delta_time.as_secs_f64())); + self.accumulator_fluid_volume += volume_to_acc; + self.accumulator_gas_volume -= volume_to_acc; + delta_vol -= volume_to_acc; } self.accumulator_gas_pressure = (Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE) @@ -662,7 +604,6 @@ impl HydLoop { self.reservoir_volume += reservoir_return; //Update Volumes - //Low pass filter on final delta vol to help with stability and final flow noise delta_vol = HydLoop::DELTA_VOL_LOW_PASS_FILTER * delta_vol + (1. - HydLoop::DELTA_VOL_LOW_PASS_FILTER) * self.current_delta_vol; @@ -699,7 +640,7 @@ impl Pump { } } - fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop, rpm: f64) { + fn update(&mut self, delta_time: &Duration, line: &HydLoop, rpm: f64) { let displacement = self.calculate_displacement(line.get_pressure()); let flow = @@ -767,7 +708,7 @@ impl ElectricPump { self.active = false; } - pub fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop) { + pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { //TODO Simulate speed of pump depending on pump load (flow?/ current?) //Pump startup/shutdown process if self.active && self.rpm < ElectricPump::NOMINAL_SPEED { @@ -781,7 +722,7 @@ impl ElectricPump { //Limiting min and max speed self.rpm = self.rpm.min(ElectricPump::NOMINAL_SPEED).max(0.0); - self.pump.update(delta_time, context, line, self.rpm); + self.pump.update(delta_time, line, self.rpm); } pub fn is_active(&self) -> bool { @@ -811,7 +752,6 @@ impl EngineDrivenPump { 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, ]; const DISPLACEMENT_MAP: [f64; 9] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.0, 0.0]; - const MAX_RPM: f64 = 4000.; const DISPLACEMENT_DYNAMICS: f64 = 0.3; //0.1 == 90% filtering on max displacement transient @@ -826,23 +766,17 @@ impl EngineDrivenPump { } } - pub fn update( - &mut self, - delta_time: &Duration, - context: &UpdateContext, - line: &HydLoop, - engine: &Engine, - ) { + pub fn update(&mut self, delta_time: &Duration, line: &HydLoop, engine: &Engine) { let n2_rpm = engine.corrected_n2().get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; let mut pump_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; - //TODO Activate pumps realistically, maybe with a displacement rate limited when activated/deactivated? + //TODO Activate pumps realistically, adding a pressurised/unpressurised mode if !self.active { - //Hack for pump activation + //Hack for pump desactivation pump_rpm = 0.0; } - self.pump.update(delta_time, context, line, pump_rpm); + self.pump.update(delta_time, line, pump_rpm); } pub fn start(&mut self) { @@ -982,9 +916,8 @@ impl RatPump { } } - pub fn update(&mut self, delta_time: &Duration, context: &UpdateContext, line: &HydLoop) { - self.pump - .update(delta_time, context, line, self.prop.get_rpm()); + pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { + self.pump.update(delta_time, line, self.prop.get_rpm()); //Now forcing min to max to force a true real time regulation. self.pump.delta_vol_min = self.pump.delta_vol_max; //TODO: handle this properly by calculating who produced what volume at end of hyd loop update @@ -1031,37 +964,6 @@ impl PressureSource for RatPump { } } -//////////////////////////////////////////////////////////////////////////////// -// ACTUATOR DEFINITION -//////////////////////////////////////////////////////////////////////////////// - -pub struct Actuator { - a_type: ActuatorType, - active: bool, - affected_by_gravity: bool, - area: Area, - line: HydLoop, - neutral_is_zero: bool, - stall_load: Force, - volume_used_at_max_deflection: Volume, -} - -// TODO -impl Actuator { - pub fn new(a_type: ActuatorType, line: HydLoop) -> Actuator { - Actuator { - a_type, - active: false, - affected_by_gravity: false, - area: Area::new::(5.0), - line, - neutral_is_zero: true, - stall_load: Force::new::(47000.), - volume_used_at_max_deflection: Volume::new::(0.), - } - } -} - //////////////////////////////////////////////////////////////////////////////// // TESTS //////////////////////////////////////////////////////////////////////////////// @@ -1936,14 +1838,18 @@ mod tests { edpump.start(); let context = context(Duration::from_secs_f64(1.0)); //Small dt to freeze spool up effect - edpump.update(&context.delta, &context, &green_loop, &engine1); + edpump.update(&context.delta, &green_loop, &engine1); for pressure in (0..3500).step_by(500) { let mut rpmTab: Vec = Vec::new(); let mut flowTab: Vec = Vec::new(); for rpm in (0..10000).step_by(150) { green_loop.loop_pressure = Pressure::new::(pressure as f64); - engine1.corrected_n2 = - Ratio::new::((rpm as f64) / (4.0 * EngineDrivenPump::MAX_RPM)); + + engine1.corrected_n2 = Ratio::new::( + (rpm as f64) + / (EngineDrivenPump::PUMP_N2_GEAR_RATIO + * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), + ); edpump.update(&context.delta, &context, &green_loop, &engine1); rpmTab.push(rpm as f64); let flow = edpump.get_delta_vol_max() @@ -1965,7 +1871,7 @@ mod tests { mod utility_tests { use crate::hydraulic::interpolation; use rand::Rng; - use std::time::{Duration, Instant}; + use std::time::Instant; #[test] fn interp_test() { @@ -2014,8 +1920,6 @@ mod tests { "Time elapsed for 1000000 calls {} s", time_elapsed.as_secs_f64() ); - - //assert!(time_elapsed < Duration::from_millis(1500) ); } } #[cfg(test)] @@ -2025,65 +1929,62 @@ mod tests { mod epump_tests {} //TODO to update according to new caracteristics, spoolup times and displacement dynamic - // #[cfg(test)] - // mod edp_tests { - // use super::*; - // use uom::si::ratio::percent; - - // #[test] - // fn starts_inactive() { - // assert!(engine_driven_pump().active == false); - // } - - // #[test] - // fn max_flow_under_2500_psi_after_100ms() { - // let n2 = Ratio::new::(60.0); - // let pressure = Pressure::new::(2000.); - // let time = Duration::from_millis(100); - // let displacement = Volume::new::(EngineDrivenPump::DISPLACEMENT_MAP.iter().cloned().fold(-1./0. /* -inf */, f64::max)); - // assert!(delta_vol_equality_check(n2, displacement, pressure, time)) - // } - - // #[test] - // fn zero_flow_above_3000_psi_after_25ms() { - // let n2 = Ratio::new::(60.0); - // let pressure = Pressure::new::(3100.); - // let time = Duration::from_millis(25); - // let displacement = Volume::new::(0.); - // assert!(delta_vol_equality_check(n2, displacement, pressure, time)) - // } - - // fn delta_vol_equality_check( - // n2: Ratio, - // displacement: Volume, - // pressure: Pressure, - // time: Duration, - // ) -> bool { - // let actual = get_edp_actual_delta_vol_when(n2, pressure, time); - // let predicted = get_edp_predicted_delta_vol_when(n2, displacement, time); - // println!("Actual: {}", actual.get::()); - // println!("Predicted: {}", predicted.get::()); - // actual == predicted - // } - - // fn get_edp_actual_delta_vol_when(n2: Ratio, pressure: Pressure, time: Duration) -> Volume { - // let eng = engine(n2); - // let mut edp = engine_driven_pump(); - // let mut line = hydraulic_loop(LoopColor::Green); - // let mut context = context((time)); - // line.loop_pressure = pressure; - // edp.update(&time,&context, &line, &eng); - // edp.get_delta_vol_max() - // } - - // fn get_edp_predicted_delta_vol_when( - // n2: Ratio, - // displacement: Volume, - // time: Duration, - // ) -> Volume { - // let edp_rpm = (1.0f64.min(4.0 * n2.get::())) * EngineDrivenPump::MAX_RPM; - // let expected_flow = Pump::calculate_flow(edp_rpm, displacement); - // expected_flow * Time::new::(time.as_secs_f64()) - // } - // } + #[cfg(test)] + mod edp_tests { + use super::*; + use uom::si::ratio::percent; + + #[test] + fn starts_inactive() { + assert!(engine_driven_pump().active == false); + } + + #[test] + fn zero_flow_above_3000_psi_after_25ms() { + let n2 = Ratio::new::(60.0); + let pressure = Pressure::new::(3100.); + let time = Duration::from_millis(25); + let displacement = Volume::new::(0.); + assert!(delta_vol_equality_check(n2, displacement, pressure, time)) + } + + fn delta_vol_equality_check( + n2: Ratio, + displacement: Volume, + pressure: Pressure, + time: Duration, + ) -> bool { + let actual = get_edp_actual_delta_vol_when(n2, pressure, time); + let predicted = get_edp_predicted_delta_vol_when(n2, displacement, time); + println!("Actual: {}", actual.get::()); + println!("Predicted: {}", predicted.get::()); + actual == predicted + } + + fn get_edp_actual_delta_vol_when(n2: Ratio, pressure: Pressure, time: Duration) -> Volume { + let eng = engine(n2); + let mut edp = engine_driven_pump(); + let dummyUpdate = Duration::from_secs(1); + let mut context = context((time)); + let mut line = hydraulic_loop(LoopColor::Green); + + edp.start(); + line.loop_pressure = pressure; + edp.update(&dummyUpdate, &context, &line, &eng); //Update 10 times to stabilize displacement + + edp.update(&time, &context, &line, &eng); + edp.get_delta_vol_max() + } + + fn get_edp_predicted_delta_vol_when( + n2: Ratio, + displacement: Volume, + time: Duration, + ) -> Volume { + let n2_rpm = n2.get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; + let edp_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; + let expected_flow = Pump::calculate_flow(edp_rpm, displacement); + expected_flow * Time::new::(time.as_secs_f64()) + } + } } From f4333626db8680a1da793aba76f1946b28498763 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 08:59:37 +0100 Subject: [PATCH 013/122] remove debug stuff --- src/systems/a320_systems/src/hydraulic.rs | 23 ----------------------- src/systems/systems/src/hydraulic/mod.rs | 7 +------ 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index d1e969f9aa3..427cdf3d0a2 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -31,7 +31,6 @@ pub struct A320Hydraulic { braking_circuit_altn: BrakeCircuit, total_sim_time_elapsed: Duration, lag_time_accumulator: Duration, - debug_refresh_duration: Duration, is_green_pressurised: bool, is_blue_pressurised: bool, @@ -101,7 +100,6 @@ impl A320Hydraulic { total_sim_time_elapsed: Duration::new(0, 0), lag_time_accumulator: Duration::new(0, 0), - debug_refresh_duration: Duration::new(0, 0), is_green_pressurised: false, is_blue_pressurised: false, @@ -171,27 +169,6 @@ impl A320Hydraulic { //Number of time steps to do according to required time step let number_of_steps_f64 = time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); - self.debug_refresh_duration += ct.delta; - if self.debug_refresh_duration > Duration::from_secs_f64(0.3) { - println!( - "---HYDRAULIC UPDATE : t={}", - self.total_sim_time_elapsed.as_secs_f64() - ); - println!( - "---G: {:.0} B: {:.0} Y: {:.0}", - self.green_loop.get_pressure().get::(), - self.blue_loop.get_pressure().get::(), - self.yellow_loop.get_pressure().get::() - ); - println!( - "---RAT stow {:.0} Rat rpm: {:.0}", - self.rat.get_stow_position(), - self.rat.prop.get_rpm(), - ); - - self.debug_refresh_duration = Duration::from_secs_f64(0.0); - } - //updating rat stowed pos on all frames in case it's used for graphics self.rat.update_stow_pos(&ct.delta); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index aa301ca5fc4..292dd4cc724 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -519,17 +519,12 @@ impl HydLoop { //Priming the loop if not filled in //TODO bug, ptu can't prime the loop is it is not providing flow through delta_vol_max if self.loop_volume < self.max_loop_volume { - //} %TODO what to do if we are back under max volume and unprime the loop? let difference = self.max_loop_volume - self.loop_volume; - // println!("---Priming diff {}", difference.get::()); let availableFluidVol = self.reservoir_volume.min(delta_vol_max); let delta_loop_vol = availableFluidVol.min(difference); - delta_vol_max -= delta_loop_vol; //%TODO check if we cross the deltaVolMin? + delta_vol_max -= delta_loop_vol; //TODO check if we cross the deltaVolMin? self.loop_volume += delta_loop_vol; self.reservoir_volume -= delta_loop_vol; - // println!("---Priming vol {} / {}", self.loop_volume.get::(),self.max_loop_volume.get::()); - } else { - // println!("---Primed {}", self.loop_volume.get::()); } //end priming From d4a4045b5a61b21509815481d964ed1baf96b7ee Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 09:09:38 +0100 Subject: [PATCH 014/122] Hydraulic logic simvars handled by logic struc --- src/systems/a320_systems/src/hydraulic.rs | 75 +++++++++++++---------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index d1e969f9aa3..496681c5e73 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -428,10 +428,6 @@ impl SimulationElement for A320Hydraulic { "HYD_GREEN_EDPUMP_ACTIVE", self.engine_driven_pump_1.is_active(), ); - writer.write_bool( - "HYD_GREEN_EDPUMP_LOW_PRESS", - self.hyd_logic_inputs.green_edp_has_fault, - ); writer.write_bool( "HYD_GREEN_FIRE_VALVE_OPENED", self.green_loop.get_fire_shutoff_valve_state(), @@ -446,10 +442,6 @@ impl SimulationElement for A320Hydraulic { self.blue_loop.get_reservoir_volume().get::(), ); writer.write_bool("HYD_BLUE_EPUMP_ACTIVE", self.blue_electric_pump.is_active()); - writer.write_bool( - "HYD_BLUE_EPUMP_LOW_PRESS", - self.hyd_logic_inputs.blue_epump_has_fault, - ); writer.write_f64( "HYD_YELLOW_PRESSURE", @@ -463,10 +455,6 @@ impl SimulationElement for A320Hydraulic { "HYD_YELLOW_EDPUMP_ACTIVE", self.engine_driven_pump_2.is_active(), ); - writer.write_bool( - "HYD_YELLOW_EDPUMP_LOW_PRESS", - self.hyd_logic_inputs.yellow_edp_has_fault, - ); writer.write_bool( "HYD_YELLOW_FIRE_VALVE_OPENED", self.yellow_loop.get_fire_shutoff_valve_state(), @@ -475,10 +463,6 @@ impl SimulationElement for A320Hydraulic { "HYD_YELLOW_EPUMP_ACTIVE", self.yellow_electric_pump.is_active(), ); - writer.write_bool( - "HYD_YELLOW_EPUMP_LOW_PRESS", - self.hyd_logic_inputs.yellow_epump_has_fault, - ); writer.write_bool("HYD_PTU_VALVE_OPENED", self.ptu.is_enabled()); writer.write_bool("HYD_PTU_ACTIVE_Y2G", self.ptu.get_is_active_right_to_left()); @@ -521,24 +505,6 @@ impl SimulationElement for A320Hydraulic { "HYD_BRAKE_ALTN_ACC_PRESS", self.braking_circuit_altn.get_acc_pressure().get::(), ); - - //Send overhead fault info - writer.write_bool( - "OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT", - self.hyd_logic_inputs.green_edp_has_fault, - ); - writer.write_bool( - "OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT", - self.hyd_logic_inputs.yellow_edp_has_fault, - ); - writer.write_bool( - "OVHD_HYD_EPUMPB_PB_HAS_FAULT", - self.hyd_logic_inputs.blue_epump_has_fault, - ); - writer.write_bool( - "OVHD_HYD_EPUMPY_PB_HAS_FAULT", - self.hyd_logic_inputs.yellow_epump_has_fault, - ); } } @@ -785,6 +751,47 @@ impl SimulationElement for A320HydraulicLogic { self.pushback_angle = state.read_f64("PUSHBACK ANGLE"); self.pushback_state = state.read_f64("PUSHBACK STATE"); } + + fn write(&self, writer: &mut SimulatorWriter) { + + writer.write_bool( + "HYD_GREEN_EDPUMP_LOW_PRESS", + self.green_edp_has_fault, + ); + + writer.write_bool( + "HYD_BLUE_EPUMP_LOW_PRESS", + self.blue_epump_has_fault, + ); + + writer.write_bool( + "HYD_YELLOW_EDPUMP_LOW_PRESS", + self.yellow_edp_has_fault, + ); + + writer.write_bool( + "HYD_YELLOW_EPUMP_LOW_PRESS", + self.yellow_epump_has_fault, + ); + + //Send overhead fault info + writer.write_bool( + "OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT", + self.green_edp_has_fault, + ); + writer.write_bool( + "OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT", + self.yellow_edp_has_fault, + ); + writer.write_bool( + "OVHD_HYD_EPUMPB_PB_HAS_FAULT", + self.blue_epump_has_fault, + ); + writer.write_bool( + "OVHD_HYD_EPUMPY_PB_HAS_FAULT", + self.yellow_epump_has_fault, + ); + } } pub struct A320HydraulicOverheadPanel { From f77660721fae068913aa02065ca5d1b45891310e Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 10:09:21 +0100 Subject: [PATCH 015/122] repaired broken tests --- src/systems/systems/Eng_Driv_pump_carac.png | Bin 0 -> 39713 bytes src/systems/systems/Epump_carac.png | Bin 0 -> 33830 bytes .../systems/src/hydraulic/brakecircuit.rs | 16 ++----- src/systems/systems/src/hydraulic/mod.rs | 43 ++++++++---------- ..._green_ptu_loop_simulation()_Green_acc.png | Bin 0 -> 48578 bytes ...green_ptu_loop_simulation()_Loop_press.png | Bin 0 -> 58763 bytes ...yellow_green_ptu_loop_simulation()_PTU.png | Bin 0 -> 51333 bytes ...green_ptu_loop_simulation()_Yellow_acc.png | Bin 0 -> 34334 bytes 8 files changed, 24 insertions(+), 35 deletions(-) create mode 100644 src/systems/systems/Eng_Driv_pump_carac.png create mode 100644 src/systems/systems/Epump_carac.png create mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_Green_acc.png create mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_Loop_press.png create mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_PTU.png create mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_Yellow_acc.png diff --git a/src/systems/systems/Eng_Driv_pump_carac.png b/src/systems/systems/Eng_Driv_pump_carac.png new file mode 100644 index 0000000000000000000000000000000000000000..2b6e70d2d60f00b6d30537647ade15dfac75fc0e GIT binary patch literal 39713 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 literal 0 HcmV?d00001 diff --git a/src/systems/systems/Epump_carac.png b/src/systems/systems/Epump_carac.png new file mode 100644 index 0000000000000000000000000000000000000000..cb2ff1db3d4949b422c720ff19c1fb293ce6fe8c GIT binary patch literal 33830 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 literal 0 HcmV?d00001 diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index b31c8694cbf..397b1f72fbb 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -304,8 +304,10 @@ impl AutoBrakeController { #[cfg(test)] mod tests { - //use uom::si::volume_rate::VolumeRate; - //use BrakeCircuit; + use uom::si::{ + acceleration::foot_per_second_squared, f64::*, pressure::{pascal,psi}, time::second, volume::gallon, + volume_rate::gallon_per_second,velocity::knot,length::foot,thermodynamic_temperature::degree_celsius, + }; use super::*; use crate::{ hydraulic::{HydFluid, HydLoop, LoopColor}, @@ -421,16 +423,6 @@ mod tests { #[test] fn auto_brake_controller() { - // let init_max_vol = Volume::new::(0.0); - // let mut hyd_loop = hydraulic_loop(LoopColor::Yellow); - // hyd_loop.loop_pressure= Pressure::new::(3000.0); - - // let mut brake_circuit_primed = BrakeCircuit::new( - // init_max_vol, - // init_max_vol, - // Volume::new::(0.1) - // ); - let mut controller = AutoBrakeController::new(vec![ Acceleration::new::(-1.5), Acceleration::new::(-3.), diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 292dd4cc724..52088e2ce3e 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1085,9 +1085,12 @@ impl History { #[cfg(test)] mod tests { - //use uom::si::volume_rate::VolumeRate; - use uom::si::acceleration::foot_per_second_squared; + use crate::simulation::UpdateContext; + use uom::si::{ + acceleration::foot_per_second_squared, f64::*, pressure::{pascal,psi}, time::second, volume::{liter,gallon}, + volume_rate::gallon_per_second,length::foot,thermodynamic_temperature::degree_celsius, + }; use super::*; #[test] @@ -1159,10 +1162,9 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&ct.delta, &ct, &green_loop, &engine1); + edp1.update(&ct.delta, &green_loop, &engine1); green_loop.update( &ct.delta, - &ct, Vec::new(), vec![&edp1], Vec::new(), @@ -1245,10 +1247,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } - epump.update(&ct.delta, &ct, &yellow_loop); + epump.update(&ct.delta, &yellow_loop); yellow_loop.update( &ct.delta, - &ct, vec![&epump], Vec::new(), Vec::new(), @@ -1295,10 +1296,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } - epump.update(&ct.delta, &ct, &blue_loop); + epump.update(&ct.delta, &blue_loop); blue_loop.update( &ct.delta, - &ct, vec![&epump], Vec::new(), Vec::new(), @@ -1376,10 +1376,9 @@ mod tests { } rat.update_physics(&ct.delta, &indicated_airpseed); - rat.update(&ct.delta, &ct, &blue_loop); + rat.update(&ct.delta, &blue_loop); blue_loop.update( &ct.delta, - &ct, Vec::new(), Vec::new(), vec![&rat], @@ -1479,8 +1478,8 @@ mod tests { ptu.flow_to_left.get::(), ptu.flow_to_right.get::(), green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), - ptu.isActiveLeft as i8 as f64, - ptu.isActiveRight as i8 as f64, + ptu.is_active_left as i8 as f64, + ptu.is_active_right as i8 as f64, ], ); accuGreenHistory.init( @@ -1551,7 +1550,7 @@ mod tests { println!("------------IS PTU ACTIVE??------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(!ptu.isActiveLeft && !ptu.isActiveRight); + assert!(!ptu.is_active_left && !ptu.is_active_right); } if x == 600 { @@ -1580,12 +1579,11 @@ mod tests { } ptu.update(&green_loop, &yellow_loop); - edp1.update(&ct.delta, &ct, &green_loop, &engine1); - epump.update(&ct.delta, &ct, &yellow_loop); + edp1.update(&ct.delta, &green_loop, &engine1); + epump.update(&ct.delta, &yellow_loop); yellow_loop.update( &ct.delta, - &ct, vec![&epump], Vec::new(), Vec::new(), @@ -1593,7 +1591,6 @@ mod tests { ); green_loop.update( &ct.delta, - &ct, Vec::new(), vec![&edp1], Vec::new(), @@ -1617,8 +1614,8 @@ mod tests { ptu.flow_to_left.get::(), ptu.flow_to_right.get::(), green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), - ptu.isActiveLeft as i8 as f64, - ptu.isActiveRight as i8 as f64, + ptu.is_active_left as i8 as f64, + ptu.is_active_right as i8 as f64, ], ); @@ -1804,7 +1801,7 @@ mod tests { for rpm in (0..10000).step_by(150) { green_loop.loop_pressure = Pressure::new::(pressure as f64); epump.rpm = rpm as f64; - epump.update(&context.delta, &context, &green_loop); + epump.update(&context.delta, &green_loop); rpmTab.push(rpm as f64); let flow = epump.get_delta_vol_max() / Time::new::(context.delta.as_secs_f64()); @@ -1845,7 +1842,7 @@ mod tests { / (EngineDrivenPump::PUMP_N2_GEAR_RATIO * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), ); - edpump.update(&context.delta, &context, &green_loop, &engine1); + edpump.update(&context.delta, &green_loop, &engine1); rpmTab.push(rpm as f64); let flow = edpump.get_delta_vol_max() / Time::new::(context.delta.as_secs_f64()); @@ -1965,9 +1962,9 @@ mod tests { edp.start(); line.loop_pressure = pressure; - edp.update(&dummyUpdate, &context, &line, &eng); //Update 10 times to stabilize displacement + edp.update(&dummyUpdate, &line, &eng); //Update 10 times to stabilize displacement - edp.update(&time, &context, &line, &eng); + edp.update(&time, &line, &eng); edp.get_delta_vol_max() } diff --git a/src/systems/systems/yellow_green_ptu_loop_simulation()_Green_acc.png b/src/systems/systems/yellow_green_ptu_loop_simulation()_Green_acc.png new file mode 100644 index 0000000000000000000000000000000000000000..32db25aa7d7eb8694f5318299d3647e0124751ac GIT binary patch literal 48578 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 JOZL|N{{byB{ki}E literal 0 HcmV?d00001 diff --git a/src/systems/systems/yellow_green_ptu_loop_simulation()_Loop_press.png b/src/systems/systems/yellow_green_ptu_loop_simulation()_Loop_press.png new file mode 100644 index 0000000000000000000000000000000000000000..097f7407ab27ba79cc6bc22ed300255a44b25471 GIT binary patch literal 58763 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

Wn0ks?9jf#k!$SaGGY&)bA&U4 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

(hKDZhNl-0 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

Wn0ks?9jf#k!$SaGGY&)bA&U4 zPe7EWGxwY_hOs(TEC&*T8!gsACaZJsMAQgd(+`1Wu&E|0>yeUH1s|J!6bblF(SON_j+Ry z$h+(f2lTRpBIb&-L=Du`Pw9c}c6mFI#YqmT&842Bt z!9qKsrYn7Qkv|oSO0JKyq3PNpIe~>17ekfCkmS(L!s5 z%hJuT6l-dAsV@yr$ZfAkxbyh(Lp?Ekny`!_KgAisXZ2lO!O-Q43gD!rsGKo`s`*yl z4=tm%bh~Rasx$_e%*txLmqje2VDwUVL&L>Du_v~+1sz*+b93&+?_BKp$t%nZ**eW| zO9t^m5)&solrrDI-rctuCGs7uxf3WByYAqoTk8F&NR?SfM~6(v^-o2+Ik(-;k{nbK z2gy=@}XQfzp>8a}=G4C|iBoXyIkUs#!IZs#RN^KYHoLS4d7P zBgOudo(FHCy$S|Utjl%>Dr*C?mEL5HhohneZUphb`Qaj^@2Jf4|7z7)*8nnvg9#&i zoEzg}k>z*{oN{if2`Pw7NQ`=a|77@ZV<6qPsKO=3c+>@Z^gy8Hcm@0j;?24DB0z%& zW0F2zM%>xCY~0~)_Oi5l@UP}7jzy|9-8_(i*%>QR&Ph}$Jg5&B?zPCRS zE4%x_MlXvWav4kS%uMr%s%Yr?LPA37tM@0`IJyL%XlrYm5Uu8h$<>uYBNGGN5t8x% z>c5be|F@6+4&p58-9YHSssB1=Eu>gHCO9@e?p;*$hqRL-kMKuEmBFBJ8r|2yEN9nD zUV&H}dj%WoA1iIv4pfDTQp(& zLRQzU*K0q6I*QMg2sgGlX6L>q(J_GAgD-;{ZCV^Lo6)|!k)WIy{Vg``0gMt`jWFq!8k}xFM19|0$Yd|itD1yyA;AeJ9&%yC=p-CT&uCA_fne{!_>a)&kuRloOOj#h! zcRarXB0r&`q*84?MCy0Z8d01rXl~7Axv^*T`0<^#_wv>6GPiuz%uDK-qb7oGcYnQq zmz<30+_X58E2G<(iAW=7!1iIUDf4Ll2_@mPruxVA4Hwzj<6uYBrJ3?7U3E&B0DOsy zHRh9Xtu~0(v{Fg!*A%+#f}W?I_qzaWHkknClwxa$@{;}E^K%W4 z#O=@tGd&0EvLlz@Zs)fy1BJD>J{T73mNm(ra?Hmngzu#Coi3uwd4lh=ldy89ch+Ee zl~-PM=dyjo_B|j}tZFq9$G&KeMWB28?e_lew(hB>^r*X$*OJJJefZfWLbU2S`W@@! zOyJ~THW=8r$c2vBi)@DNcfFf8BrtvA_#L2|QycpFHF)x<rcan!}Pu zqoJ>_{cnFHC~(Hrs73O@cQdv9=ZkZ6WPC{t+Ap8WmNuI3u|)X0I+mpTddP_W`>>^D z`aC#%mA!DdK+|%qe-Dm?hSKwVwCGOqR<}nB?R0+vP~yx0={C-0n9}Q6UKsYdef}l39$u2=2s> zAEQerUM$pKDYS8*o;=nVa;hs{+}T|$o!9ayG$McgqW5#CZg~Xh^$4_-Y37IQ?5x3+ zD)i?TeU)b8qA!Embi4^>UFb3OwxbAJI>;N*T#P7}3IC&j)0dWpf8+Cc+mE|`KhFo>XEg0K zEzRmB$;4NV1|$r=4|UpPkImzLGN(bc9m501rEQDlrAzViaK-Lim*S~?uXTZU*Gp+@CFAL4Vayz30k3ALiY-8h7XbpOn-e>B5e7lY!&lVM>GqN)nhVdcv<<-ivs6^7Z;&RMJ6- zd7ANDLZW_jbacT6y0{m4kEp+cZqXb}xd<$NlI!l@xrK!-XtOwg^0>{S{wXXh%%K`+ z$%oo?DD{NXz~8eM%NQ#Lb&Gxr3qq=zyBp zFkEPqp;nsfP@bHu_w958`cFLQVntGA6N7-3L~4HB8w^BKK07^5?l&C<((3H&Oz?0n z3QnI1HfA@ogHn7jim5$8NgdhdRW>&1Kx;iKM9b*AF zx-?nWmGWX^SAVxrdYy#8-p^)RjiCSK=XVp=cs?#{_@$20x1&Jz1lm41%#R&{qZvK105OEs}(uis&-x z5YNF*pDY$OHp2$1urS4&%hzsq0|ph{C!nC{1jZQKLl|sqK>1jopYFfPQRM<^Ll_}# zIuk{#G>m|o|C6cMwxGENhXkVHAkDMoeW@ly&=E7tV|JLT_<_WL`Oll3NI$grhy_ZvRrs0$)3*dCj>8_{n8EeKu#e`u&=KUIR4rbghoeb z%|Lr024KnXy_qQh?e#DAXxV#6ZyTo!_b*jR^hCix6Io0ng zEQ^5C8WjgeSTu*x)=FWQ>NyBi7;S>?K$_}w#_?6{^hzSOPEb?WT(=m5kAmz^GQEKv4< zbmcZ+4G0S(fLsot#s+sP`s4|c+_f|A#Q_tFP*k3$M*}gVAdj%=|Goj_v;?Bo2{CDjeM(pkDo3k2WgReylN&A|dqKr;=v!5Qu1ZPMW&Q&ip&5Vw{(V0@Pw;3@O}yrO z!dEdXcWQ;{lmf&}t@E?f;e5R-K#{SJS2``O*PfS17_Z$1XB3f^`(wydTMqYZ9UT!8 zA9S$LFJIo5mh$ANmC`_4_)zZ`Dq?Q_a50gVhbI!wmujh{^y}BRA$M1-h9m*Gi?Fbq z79(HsYL5H>QNXr8fBN*2RqZJ^icu+7AF_LUqDUfOfP83wfYt1TEGDb09Oa;f2nUc0 z;2itIYl%7yAM!!kcXxNUEEfclj@w(9+hU{(Zq}%uPF)p6a7mw|5pBqV0un1cqIoVFtA@q5O)uYK>f@J_BlQyBw zhg$~VwMYm0@@>~(whDowBA?S*92W-S*nAi*1(J6V9DK+zY|&B!G%J&y$2i{g;D@rX z%#xCmlY{qfvuZ-mc@1T^(5?fTFx>RZ40-AZR;^B8v5-*EE~gi@x93C_Z=%X&zygO- zcx*&*d3R9DXn$=Sl-Llc%Hu_3<>lq(Kqeq!=DjB$lp{a-_&kKeh@J&;E*6f+s6rBx z@{c#rO2MUwK|_c8vtOSWAIYl$==CP$PzcyZNM~xXZP(mbEvYy;i=G=~YF4|3?CY!p zAiekzcX@f}p_*k3^V7>A;VITfJL8q^X2q(x#6%d60!dQum)<=%JbVc8jwC%GOC#0~ zfmu@5(8vk)w_~YLBcC76RAhuJ^SK|eA$i)z2fRR%Z4u0fR@l-*-@nai-xq>@zH;3IwA`HYLDOP{tOX`+-1;2y?>zWM zei?JlOik5&y7?g6ss4gw?V%aDh-ci=($WQjQVdihod-XAU!Zgb`D>=GB0?L$$ottBL|!I+q0Gwa9K z9Cl-G2{fYd3Ppw0I5L^JF zAReqLfK&{K4YEKOh+wcB7;TrP;02`Uz>AZPt6D1EX|j}>;BbQlfLpPx@r)RxN!2zf z06p*;JaxFs$1g^uDsh zLlWofYNTx-$zkpHw);TTfpN~)&rcWR3hq->VL`zGoVn%hZ{7|gY(D}pCu+S!GBPp* z0TK<&!$H%Bm?aZ-A4>g|oepWSC4yP)cYAvgjoKAF3J@d^O%qrptgBb2;FuPf_O~8Y zXnCD-P1ZGndu%-BdyEtfQcWOncs86?R#vL5t2;3M9n9;3{mcMg)3^N|MM574^1dn; zJCI+g5sjqg?8pqgnFaa>sGvsBGzf0}et;Yn$o4a6bQ^rNMPUvk_VJ><{nxEjequqo zm-k@^K~SjU{fE{+tbm6)JO5hr{~BkA55h0!cnZ=trgyzzbZKcR7xC}T&IPc0b9Ysi zm#dhq=YnNu{SdYTDSdET&0imTfBN+4D`>fZ6Y+_OeOvT(p}=$J0gr(Lo7bezK1`*J zEDrbp0KSg(UY+{cItpsWZ6@V9f7Tjsn9r8GAF}tQ$);)5dMae*-~A*&2$&I zY9yJuY#V}C;}_Tk%u7lEDMLldP${H=1u%tE|W=r-_LP0?pNH z+@rw@T;RHE8hf)21sh=iEf~`5!dcCSMZF)0s=0-Mfq_H{NQDobH?#qY1ID#q>c$8Z z^RBEExPnWj4T$4=S{h=`fik@WiZI+MA|S$seum;%TPuRNU_cs0eT9M(K*|#Y;sWr= zci6O1P}1XgZDxWX=49mNJ?Wg7?%n&=J|PapvGx5&qpem(lco~sXL47sU!R+4@^^F) zAO}951qY6lv$OM;C1J*`Y8|WZOwHt^q(~w?T!^)@R8@PeV8FK~pg430t{n4F4>u|k zyUP(#>Bwlw(JteHFEb)Tj6T59zU;V@o}=$o?8cIruHp?+>`xFrqpZdRgAxm=B8M;h}*WaM($|9WRseI%mt~nC9No zdUbX6=aQ0PtQ`0J>dd(zAx5kVH_tE-UsiuiE|i%JVYlaE-|)utMoI? z6n2a8``Yt?7Ss)dk|%tQUx&RkC&3JC*MA;{?lWY;ZY?hrOAMv7p#Qw~TNe3LBNlmm zJvEnWgSJW9P%qe8S`gFcpNt1w z|3-G{qTmYclU{0=Z=U1)8`;H1_GEidOSMQf{0AF2x)3&{9+prW)HJ!|4-%Fbw8*N8 zBIin33?;dphui@+E&NzgW`yt?_R9hPNdBj4BbVtc1RPhUz$Z$@#}^HbkObGi^nfwt zMw%*^iCB0)$7F_({F@ja`>ZjD5)&6MbX-*hnlKtrHb`mh@dA-wzI^Fr8L>D-vq}oH zzN$lpiD$%C_%X)*!&1+H5tWLUH|pjCF>n+OSGzfZhEQ^HT8iuB!~x@IDH?>x+J4b- z9KB@D9*s0p)o%MGEPckDFgNiA+-SE=)ZF&@LqAWwY^P?b$We>W&=`H6^u9%YjuPVo|5t;1Wx|nY?Lw&i_o#8j$LmIv(K9*pzB6&;aZfQ@@VQ zUuUyAc5)Lj2yA4w8NK*V1KYc8^%xYeDsli`?qC8LXa;Tr0i)G!_2w*1?@mR;#V?DC zv({xj-6OhjZ|IjLxAN10@0WIB}?qtav(g?Cnl`;K_m ziQe}_y>C%Cy2yI{L+CKrY5)v{L`8kd%%m>tV}JA?p$WN}oY(%PrLa)R3h;F4{ie`V z%tHAcb zlB-b(o>a;MNcqa;w$zy3Z^7_8){71ldjJ9u4I?9Z?RHTBm@Ra-=34Mep;m4>|?WSoeRU=?wQPbN_4JTs#ytf${%N+EV!R_%5`e`q19ObTjG(8JQI5 zv(hn~^wy`~bh&Zs)-x~+0qg}Y)W+`z!QHR|p-d%6_&?1^zad=*P?(V<@B<9TDJU8M zOmW|AA_6ck51gbsniBFnG|HI@8Sh?a%_CF_a0pV->^@L-BHdB&CGT%gr;Ad(xlAnV z&Wo%G*g&dMcERk2_&o0LT2C?O)ao_kvS?QQ-k5G!{~m?>3>|f5W&spqDE809#4v%G zWJN3_faX#(iYjwOIWe@_fXjl$21*IsNX7C|W~MFZlq~9HbpTsEC67M^1sTG<1W<}l zy!rmUz=m}y6y`3e&B7{$LjI$-LJWjBu};BJ+5)J@;ksiCj*emQ>VXa1VEU)e((X|& z%YufY4t`x!)SdhH?}PjahKYxu%D?mWzNR4pojo}4aKl>OizQD3n~(~?fM@Juk8z_h z=N4kusPS+EH_Ww1rIwn2)*OVM<$H|p2Ir;G{dEYoIytI5J$T)k*7Ig~C2W1y?*dGT zBU8X{FjNkXV}YBseR_EQ$j;7g)pWr9f8^&=csj(P5yNAJTNm|E5Am)7==%;ft`wEV z%G{rv7_m|`;6iC>Z=VDEs3EYfFsqZ*O$Fp10VSm@)Ow&FrlEy1m}&HdCO5C!0}(Q1 zg%2B#csV)09BwZbx^Pg@s77dbajve~ShA1_U)S=&G32P)xF8je{j97q0m&-&X=D@H zA%zrMhnm^)}!ViUq^>0CQz`iUcJl0lH08nKu?d&_`0mXJerZx z{VP6k^v?!7(idglo6xUkaz9a7`i@cn*?6l|Cas~X3lAkLD;v8gC?q5yBg5h&AD)dJ zhSKaZ=O&`3uMFRH;3o%45(QhSa-Nr&sS(fXVo6x~@nb7YAYLa9t%t$At(`t$01oU% zGT3Wt^T*09#}-=uNf}q%{EFFb>E2($A?3C{Ik1_TokdJa!j|B9LxDlY08q-nZgNr* z%vFk%+b`oI90aHb)Zi>jb~q*7+}L=O+%wda=RNbXydYXJD^$6N&ey8OCdqC8#mT9Y z&*?Z)=i-2^Z`mJe!#!eF{A4u)-dlk9VxS?ju&^M#-OZaiJYSZPMyA-2GCXb

(hKDZhNl-0 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 R#T5Lbp>}wGrmBVi{{SEKrNsaM diff --git a/src/systems/systems/yellow_green_ptu_loop_simulation()_PTU.png b/src/systems/systems/yellow_green_ptu_loop_simulation()_PTU.png deleted file mode 100644 index ebc6af6fc4ea2bbce84b608a578e144c0e73d1c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51333 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 Date: Tue, 9 Mar 2021 12:07:07 +0100 Subject: [PATCH 018/122] brakes .js formatting --- .../Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js b/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js index 0c5a3935d29..cf334faec49 100644 --- a/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js +++ b/A32NX/html_ui/Pages/VCockpit/Instruments/Airliners/A320_Neo/BRK/A320_Neo_BRK.js @@ -75,10 +75,10 @@ var A320_Neo_BRK; return; } - const powerAvailable = SimVar.GetSimVarValue("L:DCPowerAvailable","Bool"); + const powerAvailable = SimVar.GetSimVarValue("L:DCPowerAvailable", "Bool"); if (this.topGauge != null) { if (powerAvailable) { - this.topGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS","PSI") / 1000); + this.topGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS", "PSI") / 1000); } else { this.topGauge.setValue(0); } @@ -86,14 +86,14 @@ var A320_Neo_BRK; if (this.leftGauge != null) { if (powerAvailable) { - this.leftGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS","PSI") / 1000); + this.leftGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS", "PSI") / 1000); } else { this.leftGauge.setValue(0); } } if (this.rightGauge != null) { if (powerAvailable) { - this.rightGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS","PSI") / 1000); + this.rightGauge.setValue(SimVar.GetSimVarValue("L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS", "PSI") / 1000); } else { this.rightGauge.setValue(0); } From 6d07bb7c2aa298f0dc3a0ab0721d2d2e4d7caf65 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 12:13:53 +0100 Subject: [PATCH 019/122] Update units in simvar documentation --- docs/a320-simvars.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index 33cb1648b7a..5785beb2364 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -752,7 +752,7 @@ - 2 - A32NX_HYD_{loop_name}_PRESSURE - - Pressure + - Psi - Current pressure in the {loop_name} hydraulic circuit - {loop_name} - GREEN @@ -760,7 +760,7 @@ - YELLOW - A32NX_HYD_{loop_name}_RESERVOIR - - Volume + - Gallon - Current fluid level in the {loop_name} hydraulic circuit reservoir - {loop_name} - GREEN @@ -818,7 +818,7 @@ - Power Transfer Unit instantaneous flow in motor side - A32NX_HYD_RAT_STOW_POSITION - - Position [0.0 : 1.0] + - Percent over 100 - RAT position, from fully stowed (0) to fully deployed (1) - A32NX_HYD_RAT_RPM @@ -826,21 +826,21 @@ - RAT propeller current RPM - A32NX_HYD_BRAKE_NORM_{brake_side}_PRESS - - psi + - Psi - Current pressure in brake slave circuit on green brake circuit - {brake_side} - LEFT - RIGHT - A32NX_HYD_BRAKE_ALTN_{brake_side}_PRESS - - psi + - Psi - Current pressure in brake slave circuit on yellow alternate brake circuit - {brake_side} - LEFT - RIGHT - A32NX_HYD_BRAKE_ALTN_ACC_PRESS - - psi + - Psi - Current pressure in brake accumulator on yellow alternate brake circuit - A32NX_FMGC_FLIGHT_PHASE From 42312046f3ed65a436cd60de09a97fd3cef95c3d Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 13:02:01 +0100 Subject: [PATCH 020/122] Updated simvar names --- src/systems/a320_systems/src/hydraulic.rs | 29 +++++++-------- src/systems/a320_systems_wasm/src/lib.rs | 35 ++++++------------- .../systems/src/hydraulic/brakecircuit.rs | 7 ++-- src/systems/systems/src/hydraulic/mod.rs | 7 ++-- .../systems/src/simulation/update_context.rs | 35 ++++--------------- 5 files changed, 35 insertions(+), 78 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 99b96682197..b808ef8d04d 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -375,7 +375,7 @@ impl A320Hydraulic { && (!self.hyd_logic_inputs.weight_on_wheels || self.hyd_logic_inputs.eng_1_master_on && self.hyd_logic_inputs.eng_2_master_on || !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on - || (!self.hyd_logic_inputs.parking_brake_applied && !nsw_pin_inserted)) + || (!self.hyd_logic_inputs.parking_brake_lever_pos && !nsw_pin_inserted)) && !ptu_inhibit { self.ptu.enabling(true); @@ -591,17 +591,17 @@ impl SimulationElement for A320HydraulicBrakingLogic { } fn read(&mut self, state: &mut SimulatorReader) { - self.parking_brake_demand = state.read_bool("PARK_BRAKE_DMND"); //TODO see if A32nx var exists + self.parking_brake_demand = state.read_bool("BRAKE PARKING INDICATOR"); self.weight_on_wheels = state.read_bool("SIM ON GROUND"); - self.anti_skid_activated = state.read_bool("ANTISKID ACTIVE"); - self.left_brake_command = state.read_f64("BRAKE LEFT DMND") / 100.0; - self.right_brake_command = state.read_f64("BRAKE RIGHT DMND") / 100.0; + self.anti_skid_activated = state.read_bool("ANTISKID BRAKES ACTIVE"); + self.left_brake_command = state.read_f64("BRAKE LEFT POSITION") / 100.0; + self.right_brake_command = state.read_f64("BRAKE RIGHT POSITION") / 100.0; self.autobrakes_setting = state.read_f64("AUTOBRAKES SETTING").floor() as u8; } } pub struct A320HydraulicLogic { - parking_brake_applied: bool, + parking_brake_lever_pos: bool, weight_on_wheels: bool, eng_1_master_on: bool, eng_2_master_on: bool, @@ -629,7 +629,7 @@ impl A320HydraulicLogic { pub fn new() -> A320HydraulicLogic { A320HydraulicLogic { - parking_brake_applied: true, + parking_brake_lever_pos: true, weight_on_wheels: true, eng_1_master_on: false, eng_2_master_on: false, @@ -712,16 +712,16 @@ impl SimulationElement for A320HydraulicLogic { } fn read(&mut self, state: &mut SimulatorReader) { - self.parking_brake_applied = state.read_bool("PARK_BRAKE_ON"); - self.eng_1_master_on = state.read_bool("ENG MASTER 1"); - self.eng_2_master_on = state.read_bool("ENG MASTER 2"); + self.parking_brake_lever_pos = state.read_bool("BRAKE PARKING INDICATOR"); + self.eng_1_master_on = state.read_bool("GENERAL ENG1 STARTER ACTIVE"); + self.eng_2_master_on = state.read_bool("GENERAL ENG1 STARTER ACTIVE"); self.weight_on_wheels = state.read_bool("SIM ON GROUND"); //Handling here of the previous values of cargo doors self.cargo_door_front_pos_prev = self.cargo_door_front_pos; - self.cargo_door_front_pos = state.read_f64("CARGO FRONT POS"); + self.cargo_door_front_pos = state.read_f64("EXIT OPEN 5"); self.cargo_door_back_pos_prev = self.cargo_door_back_pos; - self.cargo_door_back_pos = state.read_f64("CARGO BACK POS"); + self.cargo_door_back_pos = state.read_f64("EXIT OPEN 3"); //Handling here of the previous values of pushback angle. Angle keeps moving while towed. Feel free to find better hack self.pushback_angle_prev = self.pushback_angle; @@ -857,10 +857,7 @@ pub mod tests { Length::new::(2000.), ThermodynamicTemperature::new::(25.0), true, - Acceleration::new::(0.), - 0.0, - 0.0, - 0.0, + Acceleration::new::(0.) ) } diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index e42ab14201e..120b9562c9a 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -36,7 +36,6 @@ struct A320SimulatorReaderWriter { fuel_tank_left_main_quantity: AircraftVariable, sim_on_ground: AircraftVariable, unlimited_fuel: AircraftVariable, - parking_brake: AircraftVariable, parking_brake_demand: AircraftVariable, master_eng_1: AircraftVariable, master_eng_2: AircraftVariable, @@ -47,10 +46,7 @@ struct A320SimulatorReaderWriter { anti_skid_activated: AircraftVariable, left_brake_command: AircraftVariable, right_brake_command: AircraftVariable, - long_accel: AircraftVariable, - wheel_rpm_center: AircraftVariable, - wheel_rpm_left: AircraftVariable, - wheel_rpm_right: AircraftVariable, + longitudinal_accel: AircraftVariable, } impl A320SimulatorReaderWriter { fn new() -> Result> { @@ -85,8 +81,6 @@ impl A320SimulatorReaderWriter { )?, sim_on_ground: AircraftVariable::from("SIM ON GROUND", "Bool", 0)?, unlimited_fuel: AircraftVariable::from("UNLIMITED FUEL", "Bool", 0)?, - - parking_brake: AircraftVariable::from("BRAKE PARKING POSITION", "Bool", 1)?, 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)?, @@ -97,14 +91,11 @@ impl A320SimulatorReaderWriter { 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)?, - long_accel: AircraftVariable::from( + longitudinal_accel: AircraftVariable::from( "ACCELERATION BODY Z", "feet per second squared", 0, )?, - wheel_rpm_center: AircraftVariable::from("CENTER WHEEL RPM", "Rpm", 0)?, - wheel_rpm_left: AircraftVariable::from("LEFT WHEEL RPM", "Rpm", 0)?, - wheel_rpm_right: AircraftVariable::from("RIGHT WHEEL RPM", "Rpm", 0)?, }) } } @@ -125,21 +116,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(), - "ENG MASTER 1" => self.master_eng_1.get(), - "ENG MASTER 2" => self.master_eng_2.get(), - "PARK_BRAKE_ON" => self.parking_brake.get(), - "PARK_BRAKE_DMND" => self.parking_brake_demand.get(), - "CARGO FRONT POS" => self.cargo_door_front_pos.get(), - "CARGO BACK POS" => self.cargo_door_back_pos.get(), + "GENERAL ENG1 STARTER ACTIVE" => self.master_eng_1.get(), + "GENERAL ENG2 STARTER ACTIVE" => 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 ACTIVE" => self.anti_skid_activated.get(), - "BRAKE LEFT DMND" => self.left_brake_command.get(), - "BRAKE RIGHT DMND" => self.right_brake_command.get(), - "LONG ACC" => self.long_accel.get(), - "WHEEL RPM CENTER" => self.wheel_rpm_center.get(), - "WHEEL RPM LEFT" => self.wheel_rpm_left.get(), - "WHEEL RPM RIGHT" => self.wheel_rpm_right.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/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 397b1f72fbb..de0b8ef94ee 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -263,7 +263,7 @@ impl AutoBrakeController { pub fn update(&mut self, delta_time: &Duration, ct: &UpdateContext) { self.current_filtered_accel = self.current_filtered_accel - + (ct.acc_long - self.current_filtered_accel) + + (ct._longitudinal_acceleration - self.current_filtered_accel) * (1. - E.powf( -delta_time.as_secs_f64() / AutoBrakeController::LONG_ACC_FILTER_TIMECONST, @@ -429,7 +429,7 @@ mod tests { Acceleration::new::(-15.), ]); let mut context = context(Duration::from_secs_f64(0.)); - context.acc_long = Acceleration::new::(0.0); + context._longitudinal_acceleration = Acceleration::new::(0.0); assert!(controller.get_brake_command() <= 0.0); @@ -484,9 +484,6 @@ mod tests { ThermodynamicTemperature::new::(25.0), true, Acceleration::new::(0.), - 0.0, - 0.0, - 0.0, ) } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 52088e2ce3e..124a8468f7e 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -520,8 +520,8 @@ impl HydLoop { //TODO bug, ptu can't prime the loop is 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 availableFluidVol = self.reservoir_volume.min(delta_vol_max); - let delta_loop_vol = availableFluidVol.min(difference); + let available_fluid_vol = self.reservoir_volume.min(delta_vol_max); + let delta_loop_vol = available_fluid_vol.min(difference); delta_vol_max -= delta_loop_vol; //TODO check if we cross the deltaVolMin? self.loop_volume += delta_loop_vol; self.reservoir_volume -= delta_loop_vol; @@ -1725,9 +1725,6 @@ mod tests { ThermodynamicTemperature::new::(25.0), true, Acceleration::new::(0.), - 0.0, - 0.0, - 0.0, ) } diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index f2581cfc098..a25e4f63b68 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -15,10 +15,7 @@ pub struct UpdateContext { pub indicated_altitude: Length, pub ambient_temperature: ThermodynamicTemperature, pub is_on_ground: bool, - pub acc_long: Acceleration, - pub wheel_center_rpm: f64, - pub wheel_left_rpm: f64, - pub wheel_right_rpm: f64, + pub _longitudinal_acceleration: Acceleration, } impl UpdateContext { pub fn new( @@ -27,10 +24,7 @@ impl UpdateContext { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, is_on_ground: bool, - acc_long: Acceleration, - wheel_center_rpm: f64, - wheel_left_rpm: f64, - wheel_right_rpm: f64, + _longitudinal_acceleration: Acceleration, ) -> UpdateContext { UpdateContext { delta, @@ -38,10 +32,7 @@ impl UpdateContext { indicated_altitude, ambient_temperature, is_on_ground, - acc_long: Acceleration::new::(0.), - wheel_center_rpm: 0.0, - wheel_left_rpm: 0.0, - wheel_right_rpm: 0.0, + _longitudinal_acceleration: Acceleration::new::(0.), } } @@ -55,10 +46,7 @@ impl UpdateContext { indicated_altitude: Length::new::(reader.read_f64("INDICATED ALTITUDE")), is_on_ground: reader.read_bool("SIM ON GROUND"), delta: delta_time, - acc_long: Acceleration::new::(reader.read_f64("LONG ACC")), - wheel_center_rpm: reader.read_f64("WHEEL RPM CENTER"), - wheel_left_rpm: reader.read_f64("WHEEL RPM LEFT"), - wheel_right_rpm: reader.read_f64("WHEEL RPM RIGHT"), + _longitudinal_acceleration: Acceleration::new::(reader.read_f64("ACCELERATION BODY Z")), } } @@ -81,10 +69,7 @@ pub struct UpdateContextBuilder { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, on_ground: bool, - acc_long: Acceleration, - wheel_center_rpm: f64, - wheel_left_rpm: f64, - wheel_right_rpm: f64, + _longitudinal_acceleration: Acceleration, } impl UpdateContextBuilder { fn new() -> UpdateContextBuilder { @@ -94,10 +79,7 @@ impl UpdateContextBuilder { indicated_altitude: Length::new::(5000.), ambient_temperature: ThermodynamicTemperature::new::(0.), on_ground: false, - acc_long: Acceleration::new::(0.), - wheel_center_rpm: 0.0, - wheel_left_rpm: 0.0, - wheel_right_rpm: 0.0, + _longitudinal_acceleration: Acceleration::new::(0.), } } @@ -108,10 +90,7 @@ impl UpdateContextBuilder { self.indicated_altitude, self.ambient_temperature, self.on_ground, - self.acc_long, - self.wheel_center_rpm, - self.wheel_left_rpm, - self.wheel_right_rpm, + self._longitudinal_acceleration, ) } From 1982c987486aa4717ce71957c185caafb1fabe97 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 13:04:02 +0100 Subject: [PATCH 021/122] Moved graph drawing utils to test crate --- src/systems/systems/src/hydraulic/mod.rs | 215 ++++++++++++----------- 1 file changed, 108 insertions(+), 107 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 124a8468f7e..c76e45eda94 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -963,134 +963,135 @@ impl PressureSource for RatPump { // TESTS //////////////////////////////////////////////////////////////////////////////// -use plotlib::page::Page; -use plotlib::repr::Plot; -use plotlib::style::{LineStyle, PointMarker, PointStyle}; -use plotlib::view::ContinuousView; - -extern crate rustplotlib; -use rustplotlib::Figure; - -fn make_figure<'a>(h: &'a History) -> Figure<'a> { - use rustplotlib::{Axes2D, Line2D}; - - let mut allAxis: Vec> = Vec::new(); - - let mut idx = 0; - for curData in &h.dataVector { - let mut currAxis = Axes2D::new() - .add( - Line2D::new(h.nameVector[idx].as_str()) - .data(&h.timeVector, &curData) - .color("blue") - //.marker("x") - //.linestyle("--") - .linewidth(1.0), - ) - .xlabel("Time [sec]") - .ylabel(h.nameVector[idx].as_str()) - .legend("best") - .xlim(0.0, *h.timeVector.last().unwrap()); - //.ylim(-2.0, 2.0); - - currAxis = currAxis.grid(true); - idx = idx + 1; - allAxis.push(Some(currAxis)); - } - - Figure::new().subplots(allAxis.len() as u32, 1, allAxis) -} -//History class to record a simulation -pub struct History { - timeVector: Vec, //Simulation time starting from 0 - nameVector: Vec, //Name of each var saved - dataVector: Vec>, //Vector data for each var saved - dataSize: usize, -} +#[cfg(test)] +mod tests { -impl History { - pub fn new(names: Vec) -> History { - History { - timeVector: Vec::new(), - nameVector: names.clone(), - dataVector: Vec::new(), - dataSize: names.len(), - } - } + use crate::simulation::UpdateContext; + use uom::si::{ + acceleration::foot_per_second_squared, f64::*, pressure::{pascal,psi}, time::second, volume::{liter,gallon}, + volume_rate::gallon_per_second,length::foot,thermodynamic_temperature::degree_celsius, + }; - //Sets initialisation values of each data before first step - pub fn init(&mut self, start_time: f64, values: Vec) { - self.timeVector.push(start_time); - for idx in 0..(values.len()) { - self.dataVector.push(vec![values[idx]]); + use plotlib::page::Page; + use plotlib::repr::Plot; + use plotlib::style::{LineStyle, PointMarker, PointStyle}; + use plotlib::view::ContinuousView; + + extern crate rustplotlib; + use rustplotlib::Figure; + + fn make_figure<'a>(h: &'a History) -> Figure<'a> { + use rustplotlib::{Axes2D, Line2D}; + + let mut allAxis: Vec> = Vec::new(); + + let mut idx = 0; + for curData in &h.dataVector { + let mut currAxis = Axes2D::new() + .add( + Line2D::new(h.nameVector[idx].as_str()) + .data(&h.timeVector, &curData) + .color("blue") + //.marker("x") + //.linestyle("--") + .linewidth(1.0), + ) + .xlabel("Time [sec]") + .ylabel(h.nameVector[idx].as_str()) + .legend("best") + .xlim(0.0, *h.timeVector.last().unwrap()); + //.ylim(-2.0, 2.0); + + currAxis = currAxis.grid(true); + idx = idx + 1; + allAxis.push(Some(currAxis)); } - } - //Updates all values and time vector - pub fn update(&mut self, delta_time: f64, values: Vec) { - self.timeVector - .push(self.timeVector.last().unwrap() + delta_time); - self.pushData(values); + Figure::new().subplots(allAxis.len() as u32, 1, allAxis) } - pub fn pushData(&mut self, values: Vec) { - for idx in 0..values.len() { - self.dataVector[idx].push(values[idx]); - } + //History class to record a simulation + pub struct History { + timeVector: Vec, //Simulation time starting from 0 + nameVector: Vec, //Name of each var saved + dataVector: Vec>, //Vector data for each var saved + dataSize: usize, } - //Builds a graph using rust crate plotlib - pub fn show(self) { - let mut v = ContinuousView::new() - .x_range(0.0, *self.timeVector.last().unwrap()) - .y_range(0.0, 3500.0) - .x_label("Time (s)") - .y_label("Value"); + impl History { + pub fn new(names: Vec) -> History { + History { + timeVector: Vec::new(), + nameVector: names.clone(), + dataVector: Vec::new(), + dataSize: names.len(), + } + } - for cur_data in self.dataVector { - //Here build the 2 by Xsamples vector - let mut new_vector: Vec<(f64, f64)> = Vec::new(); - for sample_idx in 0..self.timeVector.len() { - new_vector.push((self.timeVector[sample_idx], cur_data[sample_idx])); + //Sets initialisation values of each data before first step + pub fn init(&mut self, start_time: f64, values: Vec) { + self.timeVector.push(start_time); + for idx in 0..(values.len()) { + self.dataVector.push(vec![values[idx]]); } + } - // We create our scatter plot from the data - let s1: Plot = Plot::new(new_vector).line_style(LineStyle::new().colour("#DD3355")); + //Updates all values and time vector + pub fn update(&mut self, delta_time: f64, values: Vec) { + self.timeVector + .push(self.timeVector.last().unwrap() + delta_time); + self.pushData(values); + } - v = v.add(s1); + pub fn pushData(&mut self, values: Vec) { + for idx in 0..values.len() { + self.dataVector[idx].push(values[idx]); + } } - // A page with a single view is then saved to an SVG file - Page::single(&v).save("scatter.svg").unwrap(); - } + //Builds a graph using rust crate plotlib + pub fn show(self) { + let mut v = ContinuousView::new() + .x_range(0.0, *self.timeVector.last().unwrap()) + .y_range(0.0, 3500.0) + .x_label("Time (s)") + .y_label("Value"); + + for cur_data in self.dataVector { + //Here build the 2 by Xsamples vector + let mut new_vector: Vec<(f64, f64)> = Vec::new(); + for sample_idx in 0..self.timeVector.len() { + new_vector.push((self.timeVector[sample_idx], cur_data[sample_idx])); + } - //builds a graph using matplotlib python backend. PYTHON REQUIRED AS WELL AS MATPLOTLIB PACKAGE - pub fn showMatplotlib(&self, figure_title: &str) { - let fig = make_figure(&self); + // We create our scatter plot from the data + let s1: Plot = Plot::new(new_vector).line_style(LineStyle::new().colour("#DD3355")); - use rustplotlib::backend::Matplotlib; - use rustplotlib::Backend; - let mut mpl = Matplotlib::new().unwrap(); - mpl.set_style("ggplot").unwrap(); + v = v.add(s1); + } - fig.apply(&mut mpl).unwrap(); + // A page with a single view is then saved to an SVG file + Page::single(&v).save("scatter.svg").unwrap(); + } - //mpl.savefig("simple.png").unwrap(); - mpl.savefig(figure_title); - //mpl.dump_pickle("simple.fig.pickle").unwrap(); - mpl.wait().unwrap(); - } -} + //builds a graph using matplotlib python backend. PYTHON REQUIRED AS WELL AS MATPLOTLIB PACKAGE + pub fn showMatplotlib(&self, figure_title: &str) { + let fig = make_figure(&self); -#[cfg(test)] -mod tests { + use rustplotlib::backend::Matplotlib; + use rustplotlib::Backend; + let mut mpl = Matplotlib::new().unwrap(); + mpl.set_style("ggplot").unwrap(); - use crate::simulation::UpdateContext; - use uom::si::{ - acceleration::foot_per_second_squared, f64::*, pressure::{pascal,psi}, time::second, volume::{liter,gallon}, - volume_rate::gallon_per_second,length::foot,thermodynamic_temperature::degree_celsius, - }; + fig.apply(&mut mpl).unwrap(); + + //mpl.savefig("simple.png").unwrap(); + mpl.savefig(figure_title); + //mpl.dump_pickle("simple.fig.pickle").unwrap(); + mpl.wait().unwrap(); + } + } use super::*; #[test] From d8dbdf98b024022e260a7181cd5081e2501ad9d5 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 13:37:28 +0100 Subject: [PATCH 022/122] Small optimisation for rat physics --- src/systems/systems/src/hydraulic/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index c76e45eda94..3ad4ca8b0d9 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -868,9 +868,11 @@ impl RatPropeller { stow_pos: f64, displacement_ratio: f64, ) { - self.update_generated_torque(indicated_speed, stow_pos); - self.update_friction_torque(displacement_ratio); - self.update_physics(delta_time); + 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); + } } } pub struct RatPump { From 5af8a74033bb760469095e2114cdc494a670d48e Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 13:42:45 +0100 Subject: [PATCH 023/122] Master switch bug correction --- src/systems/a320_systems/src/hydraulic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index b808ef8d04d..0a9acf4ae9d 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -714,7 +714,7 @@ impl SimulationElement for A320HydraulicLogic { 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 ENG1 STARTER ACTIVE"); - self.eng_2_master_on = state.read_bool("GENERAL ENG1 STARTER ACTIVE"); + self.eng_2_master_on = state.read_bool("GENERAL ENG2 STARTER ACTIVE"); self.weight_on_wheels = state.read_bool("SIM ON GROUND"); //Handling here of the previous values of cargo doors From 83b9366a5651170ea6c0d16767ce50db439478f7 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 14:10:51 +0100 Subject: [PATCH 024/122] Code formatting --- src/systems/a320_systems/src/hydraulic.rs | 40 +--- .../systems/src/hydraulic/brakecircuit.rs | 15 +- src/systems/systems/src/hydraulic/mod.rs | 179 ++---------------- .../systems/src/simulation/update_context.rs | 4 +- 4 files changed, 43 insertions(+), 195 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 0a9acf4ae9d..27322fef991 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -4,7 +4,6 @@ use uom::si::{ volume_rate::gallon_per_second, }; -//use crate::{ hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, PressureSource, Ptu, Pump, RatPump}, overhead::{OnOffFaultPushButton}, shared::DelayedTrueLogicGate}; use systems::engine::Engine; use systems::hydraulic::brakecircuit::BrakeCircuit; use systems::hydraulic::{ @@ -730,44 +729,21 @@ impl SimulationElement for A320HydraulicLogic { } fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.green_edp_has_fault); - writer.write_bool( - "HYD_GREEN_EDPUMP_LOW_PRESS", - self.green_edp_has_fault, - ); - - writer.write_bool( - "HYD_BLUE_EPUMP_LOW_PRESS", - self.blue_epump_has_fault, - ); + writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.blue_epump_has_fault); - writer.write_bool( - "HYD_YELLOW_EDPUMP_LOW_PRESS", - self.yellow_edp_has_fault, - ); + writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.yellow_edp_has_fault); - writer.write_bool( - "HYD_YELLOW_EPUMP_LOW_PRESS", - self.yellow_epump_has_fault, - ); + writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.yellow_epump_has_fault); - //Send overhead fault info - writer.write_bool( - "OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT", - self.green_edp_has_fault, - ); + writer.write_bool("OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT", self.green_edp_has_fault); writer.write_bool( "OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT", self.yellow_edp_has_fault, ); - writer.write_bool( - "OVHD_HYD_EPUMPB_PB_HAS_FAULT", - self.blue_epump_has_fault, - ); - writer.write_bool( - "OVHD_HYD_EPUMPY_PB_HAS_FAULT", - self.yellow_epump_has_fault, - ); + writer.write_bool("OVHD_HYD_EPUMPB_PB_HAS_FAULT", self.blue_epump_has_fault); + writer.write_bool("OVHD_HYD_EPUMPY_PB_HAS_FAULT", self.yellow_epump_has_fault); } } @@ -857,7 +833,7 @@ pub mod tests { Length::new::(2000.), ThermodynamicTemperature::new::(25.0), true, - Acceleration::new::(0.) + Acceleration::new::(0.), ) } diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index de0b8ef94ee..0fe64509337 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -304,15 +304,22 @@ impl AutoBrakeController { #[cfg(test)] mod tests { - use uom::si::{ - acceleration::foot_per_second_squared, f64::*, pressure::{pascal,psi}, time::second, volume::gallon, - volume_rate::gallon_per_second,velocity::knot,length::foot,thermodynamic_temperature::degree_celsius, - }; use super::*; use crate::{ hydraulic::{HydFluid, HydLoop, LoopColor}, simulation::{UpdateContext, UpdateContextBuilder}, }; + use uom::si::{ + acceleration::foot_per_second_squared, + f64::*, + length::foot, + pressure::{pascal, psi}, + thermodynamic_temperature::degree_celsius, + time::second, + velocity::knot, + volume::gallon, + volume_rate::gallon_per_second, + }; #[test] //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 3ad4ca8b0d9..356f276044e 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -34,109 +34,6 @@ pub fn interpolation(xs: &[f64], ys: &[f64], intermediate_x: f64) -> f64 { } } -//////////////////////////////////////////////////////////////////////////////// -// DATA & REFERENCES -//////////////////////////////////////////////////////////////////////////////// -/// -/// On A320, the reservoir level variation can, depending on the system, -/// decrease in flight by about 3.5 l (G RSVR), 4 l (Y RSVR) and 0.5 l (B RSVR) -/// -/// Each MLG door open (2 total) uses 0.25 liters each of green hyd fluid -/// Each cargo door open (3 total) uses 0.2 liters each of yellow hyd fluid -/// -/// Reservoirs -/// ------------------------------------------ -/// Normal Qty: -/// ----------- -/// Blue: 6.5L (1.7 US Gal) -/// Yellow: 12.5L (3.3 US Gal) -/// Green: 14.5L (3.8 US Gal) -/// -/// Loops -/// ------------------------------------------ -/// Max loop volume - green: 100L 26.41gal including reservoir -/// Max loop volume - yellow: 75L 19.81gal including reservoir -/// Max loop volume - blue: 50L 15.85gal including reservoir -/// -/// EDP (Eaton PV3-240-10C/D/F (F is neo)): -/// ------------------------------------------ -/// 37.5 GPM max (100% N2) -/// 3750 RPM -/// 3000 PSI -/// Displacement: 2.40 in3/rev -/// -/// -/// Electric Pump (Eaton MPEV3-032-EA2 (neo) MPEV-032-15 (ceo)): -/// ------------------------------------------ -/// Uses 115/200 VAC, 400HZ electric motor -/// 8.45 GPM max -/// 7600 RPM at full displacement, 8000 RPM at no displacement -/// 3000 PSI -/// Displacement: 0.263 in3/rev -/// -/// -/// PTU (Eaton Vickers MPHV3-115-1C): -/// ------------------------------------------ -/// 2987 PSI -/// -/// 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 -/// -/// -/// RAT PUMP (Eaton PV3-115): -/// ------------------------------------------ -/// Max displacement: 1.15 in3/rev, 18.85 mL/rev -/// Normal speed: 6,600 RPM -/// Max. Ov. Speed: 8,250 RPM -/// Theoretical Flow at normal speed: 32.86 gpm, 124.4 l/m -/// -/// -/// Equations: -/// ------------------------------------------ -/// Flow (Q), gpm: Q = (in3/rev * rpm) / 231 -/// Velocity (V), ft/s: V = (0.3208 * flow rate, gpm) / internal area, sq in -/// Force (F), lbs: F = density * area * velocity^2 -/// Pressure (P), PSI: P = force / area -/// PressureDelta = VolumeDelta / Total_uncompressed_volume * Fluid_Bulk_Modulus -/// -/// -/// Hydraulic Fluid: EXXON HyJet IV -/// ------------------------------------------ -/// Kinematic viscosity at 40C: 10.55 mm^2 s^-1, +/- 20% -/// Density at 25C: 996 kg m^-3 -/// -/// Hydraulic Line (HP) inner diameter -/// ------------------------------------------ -/// Currently unknown. Estimating to be 7.5mm for now? -/// -/// -/// Actuator Force Simvars -/// ------------------------------------------- -/// ACCELERATION BODY X (relative to aircraft, "east/west", Feet per second squared) -/// ACCELERATION BODY Y (relative to aircraft, vertical, Feet per second squared) -/// ACCELERATION BODY Z (relative to aircraft, "north/south", Feet per second squared) -/// ROTATION VELOCITY BODY X (feet per second) -/// ROTATION VELOCITY BODY Y (feet per second) -/// ROTATION VELOCITY BODY Z (feet per second) -/// VELOCITY BODY X (feet per second) -/// VELOCITY BODY Y (feet per second) -/// VELOCITY BODY Z (feet per second) -/// WING FLEX PCT (:1 for left, :2 for right; settable) (percent over 100) -/// - -//////////////////////////////////////////////////////////////////////////////// -// ENUMERATIONS -//////////////////////////////////////////////////////////////////////////////// - #[derive(Clone, Copy, Debug, PartialEq)] pub enum LoopColor { Blue, @@ -151,10 +48,6 @@ pub enum PtuState { YellowToGreen, } -//////////////////////////////////////////////////////////////////////////////// -// TRAITS -//////////////////////////////////////////////////////////////////////////////// - // Trait common to all hydraulic pumps // Max gives maximum available volume at that time as if it is a variable displacement // pump it can be adjusted by pump regulation @@ -562,7 +455,7 @@ impl HydLoop { //END ACCUMULATOR //Actuators - let used_fluidQty = Volume::new::(0.); // %%total fluid used + let used_fluid_qty = Volume::new::(0.); // %%total fluid used //foreach actuator //used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated*264.172; %264.172 is m^3 to gallons //reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated*264.172; @@ -571,7 +464,7 @@ impl HydLoop { //end foreach //end actuator - delta_vol -= used_fluidQty; + delta_vol -= used_fluid_qty; //How much we need to reach target of 3000? let mut volume_needed_to_reach_pressure_target = @@ -868,7 +761,8 @@ impl RatPropeller { stow_pos: f64, displacement_ratio: f64, ) { - if stow_pos > 0.1 { //Do not update anything on the propeller if still stowed + 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); @@ -965,14 +859,19 @@ impl PressureSource for RatPump { // TESTS //////////////////////////////////////////////////////////////////////////////// - #[cfg(test)] mod tests { use crate::simulation::UpdateContext; use uom::si::{ - acceleration::foot_per_second_squared, f64::*, pressure::{pascal,psi}, time::second, volume::{liter,gallon}, - volume_rate::gallon_per_second,length::foot,thermodynamic_temperature::degree_celsius, + acceleration::foot_per_second_squared, + f64::*, + length::foot, + pressure::{pascal, psi}, + thermodynamic_temperature::degree_celsius, + time::second, + volume::{gallon, liter}, + volume_rate::gallon_per_second, }; use plotlib::page::Page; @@ -1166,13 +1065,7 @@ mod tests { } edp1.update(&ct.delta, &green_loop, &engine1); - green_loop.update( - &ct.delta, - Vec::new(), - vec![&edp1], - Vec::new(), - Vec::new(), - ); + green_loop.update(&ct.delta, Vec::new(), vec![&edp1], Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1251,13 +1144,7 @@ mod tests { assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } epump.update(&ct.delta, &yellow_loop); - yellow_loop.update( - &ct.delta, - vec![&epump], - Vec::new(), - Vec::new(), - Vec::new(), - ); + yellow_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1300,13 +1187,7 @@ mod tests { assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } epump.update(&ct.delta, &blue_loop); - blue_loop.update( - &ct.delta, - vec![&epump], - Vec::new(), - Vec::new(), - Vec::new(), - ); + blue_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1380,13 +1261,7 @@ mod tests { rat.update_physics(&ct.delta, &indicated_airpseed); rat.update(&ct.delta, &blue_loop); - blue_loop.update( - &ct.delta, - Vec::new(), - Vec::new(), - vec![&rat], - Vec::new(), - ); + blue_loop.update(&ct.delta, Vec::new(), Vec::new(), vec![&rat], Vec::new()); if x % 20 == 0 { println!("Iteration {} Time {}", x, time); println!("-------------------------------------------"); @@ -1582,23 +1457,11 @@ mod tests { } ptu.update(&green_loop, &yellow_loop); - edp1.update(&ct.delta, &green_loop, &engine1); - epump.update(&ct.delta, &yellow_loop); - - yellow_loop.update( - &ct.delta, - vec![&epump], - Vec::new(), - Vec::new(), - vec![&ptu], - ); - green_loop.update( - &ct.delta, - Vec::new(), - vec![&edp1], - Vec::new(), - vec![&ptu], - ); + edp1.update(&ct.delta, &green_loop, &engine1); + epump.update(&ct.delta, &yellow_loop); + + yellow_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), vec![&ptu]); + green_loop.update(&ct.delta, Vec::new(), vec![&edp1], Vec::new(), vec![&ptu]); LoopHistory.update( ct.delta.as_secs_f64(), diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index a25e4f63b68..4c63cca6a10 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -46,7 +46,9 @@ impl UpdateContext { indicated_altitude: Length::new::(reader.read_f64("INDICATED ALTITUDE")), is_on_ground: reader.read_bool("SIM ON GROUND"), delta: delta_time, - _longitudinal_acceleration: Acceleration::new::(reader.read_f64("ACCELERATION BODY Z")), + _longitudinal_acceleration: Acceleration::new::( + reader.read_f64("ACCELERATION BODY Z"), + ), } } From c92ec83acbabcd2648c2ac9fc4e6b964c01c5933 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 14:26:06 +0100 Subject: [PATCH 025/122] Comment in pseudocode updated --- src/systems/systems/src/hydraulic/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 356f276044e..44b1d41623f 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -455,14 +455,14 @@ impl HydLoop { //END ACCUMULATOR //Actuators - let used_fluid_qty = Volume::new::(0.); // %%total fluid used - //foreach actuator - //used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated*264.172; %264.172 is m^3 to gallons - //reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated*264.172; - //actuator.resetVolumes() - //actuator.set_available_pressure(self.loop_pressure) - //end foreach - //end actuator + let used_fluid_qty = Volume::new::(0.); + //foreach actuator + //used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated + //reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated + //actuator.resetVolumes() + //actuator.set_available_pressure(self.loop_pressure) + //end foreach + //end actuator delta_vol -= used_fluid_qty; From 61a3bb2c1dd6bf4e7bf52ae91537af4d47438c16 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 14:43:10 +0100 Subject: [PATCH 026/122] cargo clippy errors --- src/systems/systems/src/hydraulic/brakecircuit.rs | 6 +++--- src/systems/systems/src/hydraulic/mod.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 0fe64509337..21082d9c6ed 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -82,7 +82,7 @@ impl BrakeCircuit { } BrakeCircuit { - total_displacement: total_displacement, + total_displacement, current_brake_position_left: 0.0, demanded_brake_position_left: 0.0, pressure_applied_left: Pressure::new::(0.0), @@ -249,7 +249,7 @@ impl AutoBrakeController { let num = accel_targets.len(); assert!(num > 0); AutoBrakeController { - accel_targets: accel_targets, + accel_targets, num_of_modes: num, current_selected_mode: 0, current_filtered_accel: Acceleration::new::(0.0), @@ -307,7 +307,7 @@ mod tests { use super::*; use crate::{ hydraulic::{HydFluid, HydLoop, LoopColor}, - simulation::{UpdateContext, UpdateContextBuilder}, + simulation::UpdateContext, }; use uom::si::{ acceleration::foot_per_second_squared, diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 44b1d41623f..1d1d75fcb2d 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1014,7 +1014,7 @@ mod tests { edp1.active = true; let init_n2 = Ratio::new::(55.0); - let mut engine1 = engine(init_n2); + let engine1 = engine(init_n2); let ct = context(Duration::from_millis(100)); let green_acc_var_names = vec![ @@ -1820,7 +1820,7 @@ mod tests { let eng = engine(n2); let mut edp = engine_driven_pump(); let dummyUpdate = Duration::from_secs(1); - let mut context = context((time)); + let mut context = context(time); let mut line = hydraulic_loop(LoopColor::Green); edp.start(); From ddf39026f972ec43ad2782fec73487a2b1db2142 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 15:07:08 +0100 Subject: [PATCH 027/122] cargo clippy corrections again --- src/systems/a320_systems/src/hydraulic.rs | 1 - src/systems/systems/src/hydraulic/mod.rs | 48 +++++++++++------------ 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 27322fef991..9f63d9998b5 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -839,7 +839,6 @@ pub mod tests { #[test] fn is_nws_pin_engaged_test() { - let mut overhead = overhead(); let mut logic = hyd_logic(); let update_delta = Duration::from_secs_f64(0.08); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 1d1d75fcb2d..dc8493caf53 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -365,7 +365,7 @@ impl HydLoop { //PTU flows handling let mut ptu_act = false; for ptu in ptus { - let mut actual_flow = VolumeRate::new::(0.0); + let actual_flow ; if self.connected_to_ptu_left_side { if ptu.is_active_left || ptu.is_active_right { ptu_act = true; @@ -1598,8 +1598,8 @@ mod tests { struct PressureCaracteristic { pressure: Pressure, - rpmTab: Vec, - flowTab: Vec, + rpm_tab: Vec, + flow_tab: Vec, } mod characteristics_tests { @@ -1620,7 +1620,7 @@ mod tests { currAxis = currAxis .add( Line2D::new(press_str.as_str()) - .data(&curPressure.rpmTab, &curPressure.flowTab) + .data(&curPressure.rpm_tab, &curPressure.flow_tab) .color(colors[colorIdx]) //.marker("x") .linestyle(linestyles[styleIdx]) @@ -1629,7 +1629,7 @@ mod tests { .xlabel("RPM") .ylabel("Max Flow") .legend("best") - .xlim(0.0, *curPressure.rpmTab.last().unwrap()); + .xlim(0.0, *curPressure.rpm_tab.last().unwrap()); //.ylim(-2.0, 2.0); colorIdx = (colorIdx + 1) % colors.len(); styleIdx = (styleIdx + 1) % linestyles.len(); @@ -1659,22 +1659,22 @@ mod tests { epump.start(); for pressure in (0..3500).step_by(500) { - let mut rpmTab: Vec = Vec::new(); - let mut flowTab: Vec = Vec::new(); + let mut rpm_tab: Vec = Vec::new(); + let mut flow_tab: Vec = Vec::new(); for rpm in (0..10000).step_by(150) { green_loop.loop_pressure = Pressure::new::(pressure as f64); epump.rpm = rpm as f64; epump.update(&context.delta, &green_loop); - rpmTab.push(rpm as f64); + rpm_tab.push(rpm as f64); let flow = epump.get_delta_vol_max() / Time::new::(context.delta.as_secs_f64()); let flowGal = flow.get::() as f64; - flowTab.push(flowGal); + flow_tab.push(flowGal); } outputCaracteristics.push(PressureCaracteristic { pressure: green_loop.loop_pressure, - rpmTab, - flowTab, + rpm_tab, + flow_tab, }); } show_carac("Epump_carac", &outputCaracteristics); @@ -1695,8 +1695,8 @@ mod tests { edpump.update(&context.delta, &green_loop, &engine1); for pressure in (0..3500).step_by(500) { - let mut rpmTab: Vec = Vec::new(); - let mut flowTab: Vec = Vec::new(); + let mut rpm_tab: Vec = Vec::new(); + let mut flow_tab: Vec = Vec::new(); for rpm in (0..10000).step_by(150) { green_loop.loop_pressure = Pressure::new::(pressure as f64); @@ -1706,16 +1706,16 @@ mod tests { * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), ); edpump.update(&context.delta, &green_loop, &engine1); - rpmTab.push(rpm as f64); + rpm_tab.push(rpm as f64); let flow = edpump.get_delta_vol_max() / Time::new::(context.delta.as_secs_f64()); - let flowGal = flow.get::() as f64; - flowTab.push(flowGal); + let flow_gal = flow.get::() as f64; + flow_tab.push(flow_gal); } outputCaracteristics.push(PressureCaracteristic { pressure: green_loop.loop_pressure, - rpmTab, - flowTab, + rpm_tab, + flow_tab, }); } show_carac("Eng_Driv_pump_carac", &outputCaracteristics); @@ -1763,13 +1763,13 @@ mod tests { //Speed check let mut rng = rand::thread_rng(); - let timeStart = Instant::now(); + let time_start = Instant::now(); for idx in 0..1000000 { - let testVal = rng.gen_range(xs1[0]..*xs1.last().unwrap()); - let mut res = interpolation(&xs1, &ys1, testVal); + let test_val = rng.gen_range(xs1[0]..*xs1.last().unwrap()); + let mut res = interpolation(&xs1, &ys1, test_val); res = res + 2.78; } - let time_elapsed = timeStart.elapsed(); + let time_elapsed = time_start.elapsed(); println!( "Time elapsed for 1000000 calls {} s", @@ -1819,13 +1819,13 @@ mod tests { fn get_edp_actual_delta_vol_when(n2: Ratio, pressure: Pressure, time: Duration) -> Volume { let eng = engine(n2); let mut edp = engine_driven_pump(); - let dummyUpdate = Duration::from_secs(1); + let dummy_update = Duration::from_secs(1); let mut context = context(time); let mut line = hydraulic_loop(LoopColor::Green); edp.start(); line.loop_pressure = pressure; - edp.update(&dummyUpdate, &line, &eng); //Update 10 times to stabilize displacement + edp.update(&dummy_update, &line, &eng); //Update 10 times to stabilize displacement edp.update(&time, &line, &eng); edp.get_delta_vol_max() From 5943bdbfc24266a2e9d54e4ca459774fdd927cd7 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 15:25:13 +0100 Subject: [PATCH 028/122] cargo fmt --- .gitignore | 1 + src/systems/systems/src/hydraulic/mod.rs | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 92571846bc3..93aa11db0a1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ node_modules /src/instruments/src/EFB/web/ /src/systems/target/ /src/systems/systems/*.png +.vscode/settings.json diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index dc8493caf53..7cd76309978 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -365,7 +365,7 @@ impl HydLoop { //PTU flows handling let mut ptu_act = false; for ptu in ptus { - let actual_flow ; + let actual_flow; if self.connected_to_ptu_left_side { if ptu.is_active_left || ptu.is_active_right { ptu_act = true; @@ -855,10 +855,6 @@ impl PressureSource for RatPump { } } -//////////////////////////////////////////////////////////////////////////////// -// TESTS -//////////////////////////////////////////////////////////////////////////////// - #[cfg(test)] mod tests { @@ -876,7 +872,7 @@ mod tests { use plotlib::page::Page; use plotlib::repr::Plot; - use plotlib::style::{LineStyle, PointMarker, PointStyle}; + use plotlib::style::LineStyle; use plotlib::view::ContinuousView; extern crate rustplotlib; From f2ed1b8bde0c896d85c3ab95d4a9dda70e605442 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 17:12:10 +0100 Subject: [PATCH 029/122] Cargo clippy again --- src/systems/a320_systems/src/hydraulic.rs | 65 ++---- src/systems/systems/Cargo.toml | 2 + .../systems/src/hydraulic/brakecircuit.rs | 7 +- src/systems/systems/src/hydraulic/mod.rs | 185 +++++++++--------- 4 files changed, 114 insertions(+), 145 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 9f63d9998b5..371687e56a6 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -651,54 +651,51 @@ impl A320HydraulicLogic { //TODO, code duplication to handle timeouts: generic function to do pub fn is_cargo_operation_flag(&mut self, delta_time_update: &Duration) -> bool { - let cargo_door_moved = self.cargo_door_back_pos != self.cargo_door_back_pos_prev - || self.cargo_door_front_pos != self.cargo_door_front_pos_prev; + let cargo_door_moved = (self.cargo_door_back_pos - self.cargo_door_back_pos_prev).abs() + > f64::EPSILON + || (self.cargo_door_front_pos - self.cargo_door_front_pos_prev).abs() > f64::EPSILON; if cargo_door_moved { self.cargo_door_timer = Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP); + } else if self.cargo_door_timer > *delta_time_update { + self.cargo_door_timer -= *delta_time_update; } else { - if self.cargo_door_timer > *delta_time_update { - self.cargo_door_timer -= *delta_time_update; - } else { - self.cargo_door_timer = Duration::from_secs(0); - } + self.cargo_door_timer = Duration::from_secs(0); } self.cargo_door_timer > Duration::from_secs_f64(0.0) } pub fn is_cargo_operation_ptu_flag(&mut self, delta_time_update: &Duration) -> bool { - let cargo_door_moved = self.cargo_door_back_pos != self.cargo_door_back_pos_prev - || self.cargo_door_front_pos != self.cargo_door_front_pos_prev; + let cargo_door_moved = (self.cargo_door_back_pos - self.cargo_door_back_pos_prev).abs() + > f64::EPSILON + || (self.cargo_door_front_pos - self.cargo_door_front_pos_prev).abs() > f64::EPSILON; if cargo_door_moved { self.cargo_door_timer_ptu = Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU); + } else if self.cargo_door_timer_ptu > *delta_time_update { + self.cargo_door_timer_ptu -= *delta_time_update; } else { - if self.cargo_door_timer_ptu > *delta_time_update { - self.cargo_door_timer_ptu -= *delta_time_update; - } else { - self.cargo_door_timer_ptu = Duration::from_secs(0); - } + self.cargo_door_timer_ptu = Duration::from_secs(0); } self.cargo_door_timer_ptu > Duration::from_secs_f64(0.0) } pub fn is_nsw_pin_inserted_flag(&mut self, delta_time_update: &Duration) -> bool { - let pushback_in_progress = - (self.pushback_angle != self.pushback_angle_prev) && self.pushback_state != 3.0; + let pushback_in_progress = (self.pushback_angle - self.pushback_angle_prev).abs() + > f64::EPSILON + && (self.pushback_state - 3.0).abs() > f64::EPSILON; if pushback_in_progress { self.nws_tow_engaged_timer = Duration::from_secs_f64(A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT); + } else if self.nws_tow_engaged_timer > *delta_time_update { + self.nws_tow_engaged_timer -= *delta_time_update; //TODO CHECK if rollover issue to expect if not limiting to 0 } else { - if self.nws_tow_engaged_timer > *delta_time_update { - self.nws_tow_engaged_timer -= *delta_time_update; //TODO CHECK if rollover issue to expect if not limiting to 0 - } else { - self.nws_tow_engaged_timer = Duration::from_secs(0); - } + self.nws_tow_engaged_timer = Duration::from_secs(0); } self.nws_tow_engaged_timer > Duration::from_secs(0) @@ -805,38 +802,12 @@ impl SimulationElement for A320HydraulicOverheadPanel { #[cfg(test)] pub mod tests { use std::time::Duration; - - use uom::si::{ - acceleration::{foot_per_second_squared, Acceleration}, - f64::*, - length::foot, - thermodynamic_temperature::degree_celsius, - velocity::knot, - }; - use super::A320HydraulicLogic; - use super::A320HydraulicOverheadPanel; - use crate::UpdateContext; - - fn overhead() -> A320HydraulicOverheadPanel { - A320HydraulicOverheadPanel::new() - } fn hyd_logic() -> A320HydraulicLogic { A320HydraulicLogic::new() } - fn context(delta_time: Duration) -> UpdateContext { - UpdateContext::new( - delta_time, - Velocity::new::(0.), - Length::new::(2000.), - ThermodynamicTemperature::new::(25.0), - true, - Acceleration::new::(0.), - ) - } - #[test] fn is_nws_pin_engaged_test() { let mut logic = hyd_logic(); diff --git a/src/systems/systems/Cargo.toml b/src/systems/systems/Cargo.toml index 2cb0ee3b3a7..42d92325b48 100644 --- a/src/systems/systems/Cargo.toml +++ b/src/systems/systems/Cargo.toml @@ -8,5 +8,7 @@ edition = "2018" uom = "0.30.0" rand = "0.8.0" ntest = "0.7.2" + +[dev-dependencies] plotlib = "0.5.1" rustplotlib = "0.0.4" diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 21082d9c6ed..009e1811c8c 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -311,21 +311,18 @@ mod tests { }; use uom::si::{ acceleration::foot_per_second_squared, - f64::*, length::foot, pressure::{pascal, psi}, thermodynamic_temperature::degree_celsius, - time::second, velocity::knot, volume::gallon, - volume_rate::gallon_per_second, }; #[test] //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn brake_state_at_init() { let init_max_vol = Volume::new::(1.5); - let mut brake_circuit_unprimed = BrakeCircuit::new( + let brake_circuit_unprimed = BrakeCircuit::new( init_max_vol, Volume::new::(0.0), Volume::new::(0.1), @@ -340,7 +337,7 @@ mod tests { assert!(brake_circuit_unprimed.accumulator_fluid_volume == Volume::new::(0.0)); assert!(brake_circuit_unprimed.accumulator_gas_volume == init_max_vol); - let mut brake_circuit_primed = + let brake_circuit_primed = BrakeCircuit::new(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); assert!( diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 7cd76309978..7dfc53b304e 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -211,7 +211,7 @@ pub struct HydLoop { accumulator_fluid_volume: Volume, accumulator_press_breakpoints: [f64; 9], accumulator_flow_carac: [f64; 9], - color: LoopColor, + _color: LoopColor, connected_to_ptu_left_side: bool, connected_to_ptu_right_side: bool, loop_pressure: Pressure, @@ -241,7 +241,7 @@ impl HydLoop { const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.005, 0.008, 0.01, 0.02, 0.08, 0.15, 0.35, 0.5]; pub fn new( - color: LoopColor, + _color: LoopColor, 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, @@ -254,7 +254,7 @@ impl HydLoop { accumulator_gas_pressure: Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE), accumulator_gas_volume: Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME), accumulator_fluid_volume: Volume::new::(0.), - color, + _color, connected_to_ptu_left_side, connected_to_ptu_right_side, loop_pressure: Pressure::new::(14.7), @@ -365,7 +365,7 @@ impl HydLoop { //PTU flows handling let mut ptu_act = false; for ptu in ptus { - let actual_flow; + let actual_flow ; if self.connected_to_ptu_left_side { if ptu.is_active_left || ptu.is_active_right { ptu_act = true; @@ -855,6 +855,7 @@ impl PressureSource for RatPump { } } + #[cfg(test)] mod tests { @@ -881,85 +882,85 @@ mod tests { fn make_figure<'a>(h: &'a History) -> Figure<'a> { use rustplotlib::{Axes2D, Line2D}; - let mut allAxis: Vec> = Vec::new(); + let mut all_axis: Vec> = Vec::new(); let mut idx = 0; - for curData in &h.dataVector { - let mut currAxis = Axes2D::new() + for cur_data in &h.data_vector { + let mut curr_axis = Axes2D::new() .add( - Line2D::new(h.nameVector[idx].as_str()) - .data(&h.timeVector, &curData) + 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.nameVector[idx].as_str()) + .ylabel(h.name_vector[idx].as_str()) .legend("best") - .xlim(0.0, *h.timeVector.last().unwrap()); + .xlim(0.0, *h.time_vector.last().unwrap()); //.ylim(-2.0, 2.0); - currAxis = currAxis.grid(true); + curr_axis = curr_axis.grid(true); idx = idx + 1; - allAxis.push(Some(currAxis)); + all_axis.push(Some(curr_axis)); } - Figure::new().subplots(allAxis.len() as u32, 1, allAxis) + Figure::new().subplots(all_axis.len() as u32, 1, all_axis) } //History class to record a simulation pub struct History { - timeVector: Vec, //Simulation time starting from 0 - nameVector: Vec, //Name of each var saved - dataVector: Vec>, //Vector data for each var saved - dataSize: usize, + time_vector: Vec, //Simulation time starting from 0 + name_vector: Vec, //Name of each var saved + data_vector: Vec>, //Vector data for each var saved + _data_size: usize, } impl History { pub fn new(names: Vec) -> History { History { - timeVector: Vec::new(), - nameVector: names.clone(), - dataVector: Vec::new(), - dataSize: names.len(), + 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 pub fn init(&mut self, start_time: f64, values: Vec) { - self.timeVector.push(start_time); + self.time_vector.push(start_time); for idx in 0..(values.len()) { - self.dataVector.push(vec![values[idx]]); + self.data_vector.push(vec![values[idx]]); } } //Updates all values and time vector pub fn update(&mut self, delta_time: f64, values: Vec) { - self.timeVector - .push(self.timeVector.last().unwrap() + delta_time); - self.pushData(values); + self.time_vector + .push(self.time_vector.last().unwrap() + delta_time); + self.push_data(values); } - pub fn pushData(&mut self, values: Vec) { + pub fn push_data(&mut self, values: Vec) { for idx in 0..values.len() { - self.dataVector[idx].push(values[idx]); + self.data_vector[idx].push(values[idx]); } } //Builds a graph using rust crate plotlib - pub fn show(self) { + pub fn _show(self) { let mut v = ContinuousView::new() - .x_range(0.0, *self.timeVector.last().unwrap()) + .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.dataVector { + for cur_data in self.data_vector { //Here build the 2 by Xsamples vector let mut new_vector: Vec<(f64, f64)> = Vec::new(); - for sample_idx in 0..self.timeVector.len() { - new_vector.push((self.timeVector[sample_idx], cur_data[sample_idx])); + for sample_idx in 0..self.time_vector.len() { + new_vector.push((self.time_vector[sample_idx], cur_data[sample_idx])); } // We create our scatter plot from the data @@ -973,7 +974,7 @@ mod tests { } //builds a graph using matplotlib python backend. PYTHON REQUIRED AS WELL AS MATPLOTLIB PACKAGE - pub fn showMatplotlib(&self, figure_title: &str) { + pub fn show_matplotlib(&self, figure_title: &str) { let fig = make_figure(&self); use rustplotlib::backend::Matplotlib; @@ -983,9 +984,8 @@ mod tests { fig.apply(&mut mpl).unwrap(); - //mpl.savefig("simple.png").unwrap(); - mpl.savefig(figure_title); - //mpl.dump_pickle("simple.fig.pickle").unwrap(); + let _result=mpl.savefig(figure_title); + mpl.wait().unwrap(); } } @@ -1000,10 +1000,10 @@ mod tests { "Loop Reservoir".to_string(), "Loop Flow".to_string(), ]; - let mut greenLoopHistory = History::new(green_loop_var_names); + let mut green_loop_history = History::new(green_loop_var_names); let edp1_var_names = vec!["Delta Vol Max".to_string(), "n2 ratio".to_string()]; - let mut edp1_History = History::new(edp1_var_names); + let mut edp1_history = History::new(edp1_var_names); let mut edp1 = engine_driven_pump(); let mut green_loop = hydraulic_loop(LoopColor::Green); @@ -1019,9 +1019,9 @@ mod tests { "Acc fluid vol".to_string(), "Acc gas vol".to_string(), ]; - let mut accuGreenHistory = History::new(green_acc_var_names); + let mut accu_green_history = History::new(green_acc_var_names); - greenLoopHistory.init( + green_loop_history.init( 0.0, vec![ green_loop.loop_pressure.get::(), @@ -1030,14 +1030,14 @@ mod tests { green_loop.current_flow.get::(), ], ); - edp1_History.init( + edp1_history.init( 0.0, vec![ edp1.get_delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); - accuGreenHistory.init( + accu_green_history.init( 0.0, vec![ green_loop.loop_pressure.get::(), @@ -1088,7 +1088,7 @@ mod tests { ); } - greenLoopHistory.update( + green_loop_history.update( ct.delta.as_secs_f64(), vec![ green_loop.loop_pressure.get::(), @@ -1097,14 +1097,14 @@ mod tests { green_loop.current_flow.get::(), ], ); - edp1_History.update( + edp1_history.update( ct.delta.as_secs_f64(), vec![ edp1.get_delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); - accuGreenHistory.update( + accu_green_history.update( ct.delta.as_secs_f64(), vec![ green_loop.loop_pressure.get::(), @@ -1116,9 +1116,9 @@ mod tests { } assert!(true); - greenLoopHistory.showMatplotlib("green_loop_edp_simulation_press"); - edp1_History.showMatplotlib("green_loop_edp_simulation_EDP1 data"); - accuGreenHistory.showMatplotlib("green_loop_edp_simulation_Green Accum data"); + green_loop_history.show_matplotlib("green_loop_edp_simulation_press"); + edp1_history.show_matplotlib("green_loop_edp_simulation_EDP1 data"); + accu_green_history.show_matplotlib("green_loop_edp_simulation_Green Accum data"); } #[test] @@ -1293,7 +1293,7 @@ mod tests { "GREEN Loop delta vol".to_string(), "YELLOW Loop delta vol".to_string(), ]; - let mut LoopHistory = History::new(loop_var_names); + let mut loop_history = History::new(loop_var_names); let ptu_var_names = vec![ "GREEN side flow".to_string(), @@ -1310,7 +1310,7 @@ mod tests { "Acc fluid vol".to_string(), "Acc gas vol".to_string(), ]; - let mut accuGreenHistory = History::new(green_acc_var_names); + let mut accu_green_history = History::new(green_acc_var_names); let yellow_acc_var_names = vec![ "Loop Pressure".to_string(), @@ -1318,7 +1318,7 @@ mod tests { "Acc fluid vol".to_string(), "Acc gas vol".to_string(), ]; - let mut accuYellowHistory = History::new(yellow_acc_var_names); + let mut accu_yellow_history = History::new(yellow_acc_var_names); let mut epump = electric_pump(); epump.stop(); @@ -1335,7 +1335,7 @@ mod tests { let ct = context(Duration::from_millis(100)); - LoopHistory.init( + loop_history.init( 0.0, vec![ green_loop.loop_pressure.get::(), @@ -1356,7 +1356,7 @@ mod tests { ptu.is_active_right as i8 as f64, ], ); - accuGreenHistory.init( + accu_green_history.init( 0.0, vec![ green_loop.loop_pressure.get::(), @@ -1365,7 +1365,7 @@ mod tests { green_loop.accumulator_gas_volume.get::(), ], ); - accuYellowHistory.init( + accu_yellow_history.init( 0.0, vec![ yellow_loop.loop_pressure.get::(), @@ -1459,7 +1459,7 @@ mod tests { yellow_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), vec![&ptu]); green_loop.update(&ct.delta, Vec::new(), vec![&edp1], Vec::new(), vec![&ptu]); - LoopHistory.update( + loop_history.update( ct.delta.as_secs_f64(), vec![ green_loop.loop_pressure.get::(), @@ -1481,7 +1481,7 @@ mod tests { ], ); - accuGreenHistory.update( + accu_green_history.update( ct.delta.as_secs_f64(), vec![ green_loop.loop_pressure.get::(), @@ -1490,7 +1490,7 @@ mod tests { green_loop.accumulator_gas_volume.get::(), ], ); - accuYellowHistory.update( + accu_yellow_history.update( ct.delta.as_secs_f64(), vec![ yellow_loop.loop_pressure.get::(), @@ -1520,11 +1520,11 @@ mod tests { } } - LoopHistory.showMatplotlib("yellow_green_ptu_loop_simulation()_Loop_press"); - ptu_history.showMatplotlib("yellow_green_ptu_loop_simulation()_PTU"); + loop_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Loop_press"); + ptu_history.show_matplotlib("yellow_green_ptu_loop_simulation()_PTU"); - accuGreenHistory.showMatplotlib("yellow_green_ptu_loop_simulation()_Green_acc"); - accuYellowHistory.showMatplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); + accu_green_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Green_acc"); + accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); assert!(true) } @@ -1601,37 +1601,37 @@ mod tests { mod characteristics_tests { use super::*; - fn show_carac(figure_title: &str, outputCaracteristics: &Vec) { + fn show_carac(figure_title: &str, output_caracteristics: &Vec) { use rustplotlib::{Axes2D, Line2D}; - let mut allAxis: Vec> = Vec::new(); + let mut all_axis: Vec> = Vec::new(); let colors = ["blue", "yellow", "red", "black", "cyan", "magenta", "green"]; let linestyles = ["--", "-.", "-"]; - let mut currAxis = Axes2D::new(); - currAxis = currAxis.grid(true); - let mut colorIdx = 0; - let mut styleIdx = 0; - for curPressure in outputCaracteristics { - let press_str = format!("P={:.0}", curPressure.pressure.get::()); - currAxis = currAxis + let mut curr_axis = Axes2D::new(); + curr_axis = curr_axis.grid(true); + let mut color_idx = 0; + let mut style_idx = 0; + for cur_pressure in output_caracteristics { + let press_str = format!("P={:.0}", cur_pressure.pressure.get::()); + curr_axis = curr_axis .add( Line2D::new(press_str.as_str()) - .data(&curPressure.rpm_tab, &curPressure.flow_tab) - .color(colors[colorIdx]) + .data(&cur_pressure.rpm_tab, &cur_pressure.flow_tab) + .color(colors[color_idx]) //.marker("x") - .linestyle(linestyles[styleIdx]) + .linestyle(linestyles[style_idx]) .linewidth(1.0), ) .xlabel("RPM") .ylabel("Max Flow") .legend("best") - .xlim(0.0, *curPressure.rpm_tab.last().unwrap()); + .xlim(0.0, *cur_pressure.rpm_tab.last().unwrap()); //.ylim(-2.0, 2.0); - colorIdx = (colorIdx + 1) % colors.len(); - styleIdx = (styleIdx + 1) % linestyles.len(); + color_idx = (color_idx + 1) % colors.len(); + style_idx = (style_idx + 1) % linestyles.len(); } - allAxis.push(Some(currAxis)); - let fig = Figure::new().subplots(allAxis.len() as u32, 1, allAxis); + all_axis.push(Some(curr_axis)); + let fig = Figure::new().subplots(all_axis.len() as u32, 1, all_axis); use rustplotlib::backend::Matplotlib; use rustplotlib::Backend; @@ -1640,14 +1640,14 @@ mod tests { fig.apply(&mut mpl).unwrap(); - mpl.savefig(figure_title); + let _result =mpl.savefig(figure_title); mpl.wait().unwrap(); } #[test] fn epump_charac() { - let mut outputCaracteristics: Vec = Vec::new(); + let mut output_caracteristics: Vec = Vec::new(); let mut epump = ElectricPump::new(); let context = context(Duration::from_secs_f64(0.0001)); //Small dt to freeze spool up effect @@ -1664,22 +1664,22 @@ mod tests { rpm_tab.push(rpm as f64); let flow = epump.get_delta_vol_max() / Time::new::(context.delta.as_secs_f64()); - let flowGal = flow.get::() as f64; - flow_tab.push(flowGal); + let flow_gal = flow.get::() as f64; + flow_tab.push(flow_gal); } - outputCaracteristics.push(PressureCaracteristic { + output_caracteristics.push(PressureCaracteristic { pressure: green_loop.loop_pressure, rpm_tab, flow_tab, }); } - show_carac("Epump_carac", &outputCaracteristics); + show_carac("Epump_carac", &output_caracteristics); } #[test] //TODO broken until rpm relation repaired fn engine_d_pump_charac() { - let mut outputCaracteristics: Vec = Vec::new(); + let mut output_caracteristics: Vec = Vec::new(); let mut edpump = EngineDrivenPump::new(); //let context = context(Duration::from_secs_f64(0.0001) ); //Small dt to freeze spool up effect @@ -1708,13 +1708,13 @@ mod tests { let flow_gal = flow.get::() as f64; flow_tab.push(flow_gal); } - outputCaracteristics.push(PressureCaracteristic { + output_caracteristics.push(PressureCaracteristic { pressure: green_loop.loop_pressure, rpm_tab, flow_tab, }); } - show_carac("Eng_Driv_pump_carac", &outputCaracteristics); + show_carac("Eng_Driv_pump_carac", &output_caracteristics); } } @@ -1760,10 +1760,10 @@ mod tests { //Speed check let mut rng = rand::thread_rng(); let time_start = Instant::now(); - for idx in 0..1000000 { + for _idx in 0..1000000 { let test_val = rng.gen_range(xs1[0]..*xs1.last().unwrap()); - let mut res = interpolation(&xs1, &ys1, test_val); - res = res + 2.78; + let mut _res = interpolation(&xs1, &ys1, test_val); + _res = _res + 2.78; } let time_elapsed = time_start.elapsed(); @@ -1816,7 +1816,6 @@ mod tests { let eng = engine(n2); let mut edp = engine_driven_pump(); let dummy_update = Duration::from_secs(1); - let mut context = context(time); let mut line = hydraulic_loop(LoopColor::Green); edp.start(); From 725f2f63de2486db2e8a107078abda47f6234312 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 17:44:31 +0100 Subject: [PATCH 030/122] cargo fmt --- src/systems/a320_systems/src/hydraulic.rs | 2 +- src/systems/systems/src/hydraulic/mod.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 371687e56a6..706c94b42b1 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -801,8 +801,8 @@ impl SimulationElement for A320HydraulicOverheadPanel { #[cfg(test)] pub mod tests { - use std::time::Duration; use super::A320HydraulicLogic; + use std::time::Duration; fn hyd_logic() -> A320HydraulicLogic { A320HydraulicLogic::new() diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 7dfc53b304e..b00715e9ceb 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -365,7 +365,7 @@ impl HydLoop { //PTU flows handling let mut ptu_act = false; for ptu in ptus { - let actual_flow ; + let actual_flow; if self.connected_to_ptu_left_side { if ptu.is_active_left || ptu.is_active_right { ptu_act = true; @@ -855,7 +855,6 @@ impl PressureSource for RatPump { } } - #[cfg(test)] mod tests { @@ -984,7 +983,7 @@ mod tests { fig.apply(&mut mpl).unwrap(); - let _result=mpl.savefig(figure_title); + let _result = mpl.savefig(figure_title); mpl.wait().unwrap(); } @@ -1640,7 +1639,7 @@ mod tests { fig.apply(&mut mpl).unwrap(); - let _result =mpl.savefig(figure_title); + let _result = mpl.savefig(figure_title); mpl.wait().unwrap(); } From 470c8c264e5c739fe19454f361ec3c5807187349 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:23:12 +0100 Subject: [PATCH 031/122] Hyd loop: added sub functions for accu and ptu --- src/systems/systems/src/hydraulic/mod.rs | 225 ++++++++++++++--------- 1 file changed, 136 insertions(+), 89 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index b00715e9ceb..fb695e04f7b 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1,8 +1,13 @@ use std::time::Duration; use uom::si::{ - f64::*, pressure::psi, ratio::percent, time::second, velocity::knot, volume::cubic_inch, - volume::gallon, volume_rate::gallon_per_second, + f64::*, + pressure::psi, + ratio::percent, + time::second, + velocity::knot, + volume::{cubic_inch, gallon}, + volume_rate::gallon_per_second, }; use crate::engine::Engine; @@ -79,7 +84,7 @@ impl HydFluid { } pub fn get_bulk_mod(&self) -> Pressure { - return self.current_bulk; + self.current_bulk } } @@ -94,6 +99,12 @@ pub struct Ptu { last_flow: VolumeRate, } +impl Default for Ptu { + fn default() -> Self { + Ptu::new() + } +} + impl Ptu { //Low pass filter to handle flow dynamic: avoids instantaneous flow transient, // simulating RPM dynamic of PTU @@ -320,49 +331,46 @@ impl HydLoop { self.fire_shutoff_valve_opened } - pub fn update( - &mut self, - delta_time: &Duration, - electric_pumps: Vec<&ElectricPump>, - engine_driven_pumps: Vec<&EngineDrivenPump>, - ram_air_pumps: Vec<&RatPump>, - ptus: Vec<&Ptu>, - ) { - 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.); + fn update_accumulator(&mut self, delta_time: &Duration, delta_vol: &mut Volume) { + let accumulator_delta_press = self.accumulator_gas_pressure - self.loop_pressure; + let flow_variation = VolumeRate::new::(interpolation( + &self.accumulator_press_breakpoints, + &self.accumulator_flow_carac, + accumulator_delta_press.get::().abs(), + )); - if self.fire_shutoff_valve_opened { - for p in engine_driven_pumps { - delta_vol_max += p.get_delta_vol_max(); - delta_vol_min += p.get_delta_vol_min(); - } - } - for p in electric_pumps { - delta_vol_max += p.get_delta_vol_max(); - delta_vol_min += p.get_delta_vol_min(); - } - for p in ram_air_pumps { - delta_vol_max += p.get_delta_vol_max(); - delta_vol_min += p.get_delta_vol_min(); + //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 { + let volume_from_acc = self + .accumulator_fluid_volume + .min(flow_variation * Time::new::(delta_time.as_secs_f64())); + self.accumulator_fluid_volume -= volume_from_acc; + self.accumulator_gas_volume += volume_from_acc; + *delta_vol += volume_from_acc; + } else { + let volume_to_acc = delta_vol + .max(Volume::new::(0.0)) + .max(flow_variation * Time::new::(delta_time.as_secs_f64())); + self.accumulator_fluid_volume += volume_to_acc; + self.accumulator_gas_volume -= volume_to_acc; + *delta_vol -= volume_to_acc; } - //Storing max pump capacity available. for now used in PTU model to limit it's input flow - self.current_max_flow = delta_vol_max / Time::new::(delta_time.as_secs_f64()); - - //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::( - 0.04 * delta_time.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; + self.accumulator_gas_pressure = (Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE) + * Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME)) + / (Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME) + - self.accumulator_fluid_volume); + } - //PTU flows handling + fn update_ptu_flows( + &mut self, + delta_time: &Duration, + ptus: Vec<&Ptu>, + delta_vol: &mut Volume, + reservoir_return: &mut Volume, + ) { let mut ptu_act = false; for ptu in ptus { let actual_flow; @@ -382,9 +390,10 @@ impl HydLoop { //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 * Time::new::(delta_time.as_secs_f64()); + *reservoir_return -= + actual_flow * Time::new::(delta_time.as_secs_f64()); } - delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); + *delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); } else if self.connected_to_ptu_right_side { if ptu.is_active_left || ptu.is_active_right { ptu_act = true; @@ -401,16 +410,65 @@ impl HydLoop { //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 * Time::new::(delta_time.as_secs_f64()); + *reservoir_return -= + actual_flow * Time::new::(delta_time.as_secs_f64()); } - delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); + *delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); } } self.ptu_active = ptu_act; - //END PTU + } - //Priming the loop if not filled in - //TODO bug, ptu can't prime the loop is it is not providing flow through delta_vol_max + pub fn update( + &mut self, + delta_time: &Duration, + electric_pumps: Vec<&ElectricPump>, + engine_driven_pumps: Vec<&EngineDrivenPump>, + ram_air_pumps: Vec<&RatPump>, + ptus: Vec<&Ptu>, + ) { + 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.get_delta_vol_max(); + delta_vol_min += p.get_delta_vol_min(); + } + } + for p in electric_pumps { + delta_vol_max += p.get_delta_vol_max(); + delta_vol_min += p.get_delta_vol_min(); + } + for p in ram_air_pumps { + delta_vol_max += p.get_delta_vol_max(); + delta_vol_min += p.get_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 / Time::new::(delta_time.as_secs_f64()); + + //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::( + 0.04 * delta_time.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(delta_time, ptus, &mut delta_vol, &mut reservoir_return); + + //Updates current accumulator state and updates loop delta_vol + self.update_accumulator(delta_time, &mut delta_vol); + + //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); @@ -419,44 +477,10 @@ impl HydLoop { self.loop_volume += delta_loop_vol; self.reservoir_volume -= delta_loop_vol; } - //end priming - - //ACCUMULATOR - let accumulator_delta_press = self.accumulator_gas_pressure - self.loop_pressure; - let flow_variation = VolumeRate::new::(interpolation( - &self.accumulator_press_breakpoints, - &self.accumulator_flow_carac, - accumulator_delta_press.get::().abs(), - )); - //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 { - let volume_from_acc = self - .accumulator_fluid_volume - .min(flow_variation * Time::new::(delta_time.as_secs_f64())); - self.accumulator_fluid_volume -= volume_from_acc; - self.accumulator_gas_volume += volume_from_acc; - delta_vol += volume_from_acc; - } else { - let volume_to_acc = delta_vol - .max(Volume::new::(0.0)) - .max(flow_variation * Time::new::(delta_time.as_secs_f64())); - self.accumulator_fluid_volume += volume_to_acc; - self.accumulator_gas_volume -= volume_to_acc; - delta_vol -= volume_to_acc; - } - - self.accumulator_gas_pressure = (Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE) - * Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME)) - / (Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME) - - self.accumulator_fluid_volume); - //END ACCUMULATOR - - //Actuators + //Actuators to update here, we get their accumulated consumptions and returns, then reset them for next iteration let used_fluid_qty = Volume::new::(0.); - //foreach actuator + //foreach actuator pseudocode //used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated //reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated //actuator.resetVolumes() @@ -502,10 +526,6 @@ impl HydLoop { } } -//////////////////////////////////////////////////////////////////////////////// -// PUMP DEFINITION -//////////////////////////////////////////////////////////////////////////////// - pub struct Pump { delta_vol_max: Volume, delta_vol_min: Volume, @@ -566,6 +586,13 @@ pub struct ElectricPump { rpm: f64, pump: Pump, } + +impl Default for ElectricPump { + fn default() -> Self { + ElectricPump::new() + } +} + impl ElectricPump { const SPOOLUP_TIME: f64 = 4.0; const SPOOLDOWN_TIME: f64 = 4.0; @@ -630,6 +657,13 @@ pub struct EngineDrivenPump { active: bool, pump: Pump, } + +impl Default for EngineDrivenPump { + fn default() -> Self { + EngineDrivenPump::new() + } +} + impl EngineDrivenPump { const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; //according to the Type Certificate Data Sheet of LEAP 1A26 //max N2 rpm is 116.5% @ 19391 RPM @@ -696,6 +730,12 @@ pub struct RatPropeller { torque_sum: f64, } +impl Default for RatPropeller { + fn default() -> Self { + RatPropeller::new() + } +} + impl RatPropeller { const LOW_SPEED_PHYSICS_ACTIVATION: f64 = 50.; //Low speed special calculation threshold. Under that value we compute resistant torque depending on pump angle and displacement const STOWED_ANGLE: f64 = std::f64::consts::PI / 2.; @@ -776,6 +816,13 @@ pub struct RatPump { stowed_position: f64, max_displacement: f64, } + +impl Default for RatPump { + fn default() -> Self { + RatPump::new() + } +} + impl RatPump { const DISPLACEMENT_BREAKPTS: [f64; 9] = [ 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, From f9ebd1ea701d28f73ea3f302a919b6b2102708fb Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:24:02 +0100 Subject: [PATCH 032/122] Higher brake pressure limit for Aksid off case This will be updated later with a real pressure limiter --- src/systems/a320_systems/src/hydraulic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 706c94b42b1..31fe2b26926 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -555,8 +555,8 @@ impl A320HydraulicBrakingLogic { + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) * self.right_brake_yellow_command; if !self.anti_skid_activated { - self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.37); - self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.37); + self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.5); + self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.5); } } else { //Else we just use parking brake From bbb4f40f17966415ebcb62d477e2b61195bcdc94 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 10 Mar 2021 14:33:51 +0100 Subject: [PATCH 033/122] Simvar writes refactoring --- docs/a320-simvars.md | 6 +- src/systems/a320_systems/src/hydraulic.rs | 135 +++-------- .../systems/src/hydraulic/brakecircuit.rs | 81 +++++-- src/systems/systems/src/hydraulic/mod.rs | 228 ++++++++++++------ 4 files changed, 254 insertions(+), 196 deletions(-) diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index 5785beb2364..1c5eaee50e9 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -808,10 +808,10 @@ - A32NX_HYD_PTU_ACTIVE_{motor_side} - Bool - - Power Transfer Unit is trying to transfer hydraulic power from either yellow to green or green to yellow circuits + - Power Transfer Unit is trying to transfer hydraulic power from either yellow to green (R2L) or green to yellow (L2R) circuits - {motor_side} - - Y2G - - G2Y + - L2R + - R2L - A32NX_HYD_PTU_MOTOR_FLOW - Gallon per second diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 31fe2b26926..6cddc930c71 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,14 +1,9 @@ use std::time::Duration; -use uom::si::{ - f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon, - volume_rate::gallon_per_second, -}; +use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; use systems::engine::Engine; use systems::hydraulic::brakecircuit::BrakeCircuit; -use systems::hydraulic::{ - ElectricPump, EngineDrivenPump, HydFluid, HydLoop, LoopColor, Ptu, RatPump, -}; +use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RatPump}; use systems::overhead::{AutoOffFaultPushButton, FirePushButton, OnOffFaultPushButton}; use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, @@ -49,7 +44,7 @@ impl A320Hydraulic { hyd_brake_logic: A320HydraulicBrakingLogic::new(), blue_loop: HydLoop::new( - LoopColor::Blue, + "BLUE", false, false, Volume::new::(15.8), @@ -57,9 +52,10 @@ impl A320Hydraulic { Volume::new::(8.0), Volume::new::(1.56), HydFluid::new(Pressure::new::(1450000000.0)), + false, ), green_loop: HydLoop::new( - LoopColor::Green, + "GREEN", true, false, Volume::new::(26.38), @@ -67,9 +63,10 @@ impl A320Hydraulic { Volume::new::(15.), Volume::new::(3.6), HydFluid::new(Pressure::new::(1450000000.0)), + true, ), yellow_loop: HydLoop::new( - LoopColor::Yellow, + "YELLOW", false, true, Volume::new::(19.75), @@ -77,21 +74,24 @@ impl A320Hydraulic { Volume::new::(10.0), Volume::new::(3.15), HydFluid::new(Pressure::new::(1450000000.0)), + true, ), - engine_driven_pump_1: EngineDrivenPump::new(), - engine_driven_pump_2: EngineDrivenPump::new(), - blue_electric_pump: ElectricPump::new(), - yellow_electric_pump: ElectricPump::new(), - rat: RatPump::new(), - ptu: Ptu::new(), + engine_driven_pump_1: EngineDrivenPump::new("GREEN"), + engine_driven_pump_2: EngineDrivenPump::new("YELLOW"), + blue_electric_pump: ElectricPump::new("BLUE"), + yellow_electric_pump: ElectricPump::new("YELLOW"), + rat: RatPump::new(""), + ptu: Ptu::new(""), braking_circuit_norm: BrakeCircuit::new( + "NORM", Volume::new::(0.), Volume::new::(0.), Volume::new::(0.08), ), braking_circuit_altn: BrakeCircuit::new( + "ALTN", Volume::new::(1.5), Volume::new::(0.0), Volume::new::(0.08), @@ -386,101 +386,24 @@ impl A320Hydraulic { impl SimulationElement for A320Hydraulic { fn accept(&mut self, visitor: &mut T) { - visitor.visit(&mut self.hyd_logic_inputs); - visitor.visit(&mut self.hyd_brake_logic); - visitor.visit(self); - } + self.yellow_electric_pump.accept(visitor); + self.blue_electric_pump.accept(visitor); + self.engine_driven_pump_1.accept(visitor); + self.engine_driven_pump_2.accept(visitor); - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64( - "HYD_GREEN_PRESSURE", - self.green_loop.get_pressure().get::(), - ); - writer.write_f64( - "HYD_GREEN_RESERVOIR", - self.green_loop.get_reservoir_volume().get::(), - ); - writer.write_bool( - "HYD_GREEN_EDPUMP_ACTIVE", - self.engine_driven_pump_1.is_active(), - ); - writer.write_bool( - "HYD_GREEN_FIRE_VALVE_OPENED", - self.green_loop.get_fire_shutoff_valve_state(), - ); + self.ptu.accept(visitor); - writer.write_f64( - "HYD_BLUE_PRESSURE", - self.blue_loop.get_pressure().get::(), - ); - writer.write_f64( - "HYD_BLUE_RESERVOIR", - self.blue_loop.get_reservoir_volume().get::(), - ); - writer.write_bool("HYD_BLUE_EPUMP_ACTIVE", self.blue_electric_pump.is_active()); + self.blue_loop.accept(visitor); + self.green_loop.accept(visitor); + self.yellow_loop.accept(visitor); - writer.write_f64( - "HYD_YELLOW_PRESSURE", - self.yellow_loop.get_pressure().get::(), - ); - writer.write_f64( - "HYD_YELLOW_RESERVOIR", - self.yellow_loop.get_reservoir_volume().get::(), - ); - writer.write_bool( - "HYD_YELLOW_EDPUMP_ACTIVE", - self.engine_driven_pump_2.is_active(), - ); - writer.write_bool( - "HYD_YELLOW_FIRE_VALVE_OPENED", - self.yellow_loop.get_fire_shutoff_valve_state(), - ); - writer.write_bool( - "HYD_YELLOW_EPUMP_ACTIVE", - self.yellow_electric_pump.is_active(), - ); - - writer.write_bool("HYD_PTU_VALVE_OPENED", self.ptu.is_enabled()); - writer.write_bool("HYD_PTU_ACTIVE_Y2G", self.ptu.get_is_active_right_to_left()); - writer.write_bool("HYD_PTU_ACTIVE_G2Y", self.ptu.get_is_active_left_to_right()); - writer.write_f64( - "HYD_PTU_MOTOR_FLOW", - self.ptu.get_flow().get::(), - ); - - writer.write_f64("HYD_RAT_STOW_POSITION", self.rat.get_stow_position()); + self.hyd_logic_inputs.accept(visitor); + self.hyd_brake_logic.accept(visitor); - writer.write_f64("HYD_RAT_RPM", self.rat.prop.get_rpm()); + self.braking_circuit_norm.accept(visitor); + self.braking_circuit_altn.accept(visitor); - //BRAKES - writer.write_f64( - "HYD_BRAKE_NORM_LEFT_PRESS", - self.braking_circuit_norm - .get_brake_pressure_left() - .get::(), - ); - writer.write_f64( - "HYD_BRAKE_NORM_RIGHT_PRESS", - self.braking_circuit_norm - .get_brake_pressure_right() - .get::(), - ); - writer.write_f64( - "HYD_BRAKE_ALTN_LEFT_PRESS", - self.braking_circuit_altn - .get_brake_pressure_left() - .get::(), - ); - writer.write_f64( - "HYD_BRAKE_ALTN_RIGHT_PRESS", - self.braking_circuit_altn - .get_brake_pressure_right() - .get::(), - ); - writer.write_f64( - "HYD_BRAKE_ALTN_ACC_PRESS", - self.braking_circuit_altn.get_acc_pressure().get::(), - ); + visitor.visit(self); } } diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 009e1811c8c..057b535b8c0 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -1,7 +1,12 @@ -use crate::{hydraulic::HydLoop, simulation::UpdateContext}; +use crate::{ + hydraulic::HydLoop, + simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext}, +}; use std::f64::consts::E; +use std::string::String; use std::time::Duration; + use uom::si::{ acceleration::foot_per_second_squared, f64::*, pressure::psi, time::second, volume::gallon, volume_rate::gallon_per_second, @@ -16,6 +21,10 @@ pub trait ActuatorHydInterface { //Brake model is simplified as we just move brake position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). // So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position pub struct BrakeCircuit { + _id: String, + id_left_press: String, + id_right_press: String, + id_acc_press: String, //Total volume used when at max braking position. //Simple model for now we assume at max braking we have max brake displacement total_displacement: Volume, @@ -45,6 +54,26 @@ pub struct BrakeCircuit { accumulator_fluid_pressure_sensor_filtered: Pressure, //Fluid pressure in brake circuit filtered for cockpit gauges } +impl SimulationElement for BrakeCircuit { + fn accept(&mut self, visitor: &mut T) { + visitor.visit(self); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64( + &self.id_left_press, + self.get_brake_pressure_left().get::(), + ); + writer.write_f64( + &self.id_right_press, + self.get_brake_pressure_right().get::(), + ); + if self.has_accumulator { + writer.write_f64(&self.id_acc_press, self.get_acc_pressure().get::()); + } + } +} + impl BrakeCircuit { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1000.0; // Nitrogen PSI const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = @@ -55,6 +84,7 @@ impl BrakeCircuit { const ACC_PRESSURE_SENSOR_FILTER_TIMECONST: f64 = 0.1; //Time constant of the filter used to measure brake circuit pressure pub fn new( + id: &str, accumulator_volume: Volume, accumulator_fluid_volume_at_init: Volume, total_displacement: Volume, @@ -82,6 +112,11 @@ impl BrakeCircuit { } 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), + total_displacement, current_brake_position_left: 0.0, demanded_brake_position_left: 0.0, @@ -138,8 +173,8 @@ impl BrakeCircuit { self.demanded_brake_position_right = self.current_brake_position_right; } } else { - self.volume_to_res_accumulator = self.volume_to_res_accumulator - + delta_vol.abs().min(self.accumulator_fluid_volume); + self.volume_to_res_accumulator += + delta_vol.abs().min(self.accumulator_fluid_volume); } self.accumulator_gas_pressure = @@ -306,7 +341,7 @@ impl AutoBrakeController { mod tests { use super::*; use crate::{ - hydraulic::{HydFluid, HydLoop, LoopColor}, + hydraulic::{HydFluid, HydLoop}, simulation::UpdateContext, }; use uom::si::{ @@ -323,6 +358,7 @@ mod tests { 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), @@ -337,8 +373,12 @@ mod tests { 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(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); + let brake_circuit_primed = BrakeCircuit::new( + "altn", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); assert!( brake_circuit_unprimed.get_brake_pressure_left() @@ -353,11 +393,15 @@ mod tests { #[test] fn brake_pressure_rise() { let init_max_vol = Volume::new::(1.5); - let mut hyd_loop = hydraulic_loop(LoopColor::Yellow); + let mut hyd_loop = hydraulic_loop("YELLOW"); hyd_loop.loop_pressure = Pressure::new::(2500.0); - let mut brake_circuit_primed = - BrakeCircuit::new(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); + let mut brake_circuit_primed = BrakeCircuit::new( + "Altn", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -391,11 +435,15 @@ mod tests { #[test] fn brake_pressure_rise_no_accumulator() { let init_max_vol = Volume::new::(0.0); - let mut hyd_loop = hydraulic_loop(LoopColor::Yellow); + let mut hyd_loop = hydraulic_loop("GREEN"); hyd_loop.loop_pressure = Pressure::new::(2500.0); - let mut brake_circuit_primed = - BrakeCircuit::new(init_max_vol, init_max_vol / 2.0, Volume::new::(0.1)); + let mut brake_circuit_primed = BrakeCircuit::new( + "norm", + init_max_vol, + init_max_vol / 2.0, + Volume::new::(0.1), + ); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -445,9 +493,9 @@ mod tests { assert!(controller.get_brake_command() >= 0.0); } - fn hydraulic_loop(loop_color: LoopColor) -> HydLoop { + fn hydraulic_loop(loop_color: &str) -> HydLoop { match loop_color { - LoopColor::Yellow => HydLoop::new( + "GREEN" => HydLoop::new( loop_color, false, true, @@ -456,8 +504,9 @@ mod tests { Volume::new::(10.0), Volume::new::(3.83), HydFluid::new(Pressure::new::(1450000000.0)), + true, ), - LoopColor::Green => HydLoop::new( + "YELLOW" => HydLoop::new( loop_color, true, false, @@ -466,6 +515,7 @@ mod tests { Volume::new::(8.0), Volume::new::(3.3), HydFluid::new(Pressure::new::(1450000000.0)), + true, ), _ => HydLoop::new( loop_color, @@ -476,6 +526,7 @@ mod tests { Volume::new::(8.0), Volume::new::(1.5), HydFluid::new(Pressure::new::(1450000000.0)), + false, ), } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index fb695e04f7b..cbbf64d2d79 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1,3 +1,4 @@ +use std::string::String; use std::time::Duration; use uom::si::{ @@ -11,6 +12,7 @@ use uom::si::{ }; use crate::engine::Engine; +use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; pub mod brakecircuit; // //Interpolate values_map_y at point value_at_point in breakpoints break_points_x @@ -39,20 +41,6 @@ pub fn interpolation(xs: &[f64], ys: &[f64], intermediate_x: f64) -> f64 { } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LoopColor { - Blue, - Green, - Yellow, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum PtuState { - Off, - GreenToYellow, - YellowToGreen, -} - // Trait common to all hydraulic pumps // Max gives maximum available volume at that time as if it is a variable displacement // pump it can be adjusted by pump regulation @@ -91,6 +79,12 @@ impl HydFluid { //Power Transfer Unit //TODO enhance simulation with RPM and variable displacement on one side? pub struct Ptu { + _id: String, + active_left_id: String, + active_right_id: String, + flow_id: String, + enabled_id: String, + is_enabled: bool, is_active_right: bool, is_active_left: bool, @@ -101,7 +95,16 @@ pub struct Ptu { impl Default for Ptu { fn default() -> Self { - Ptu::new() + Ptu::new("") + } +} + +impl SimulationElement for Ptu { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_left_id, self.is_active_left); + writer.write_bool(&self.active_right_id, self.is_active_left); + writer.write_f64(&self.flow_id, self.get_flow().get::()); + writer.write_bool(&self.enabled_id, self.is_enabled()); } } @@ -115,8 +118,13 @@ impl Ptu { // set to 0.5 PTU will only use half of the flow that all pumps are able to generate const AGRESSIVENESS_FACTOR: f64 = 0.7; - pub fn new() -> Ptu { + pub fn new(id: &str) -> Ptu { Ptu { + _id: id.to_uppercase(), + active_left_id: format!("HYD_PTU{}_ACTIVE_L2R", id), + active_right_id: format!("HYD_PTU{}_ACTIVE_R2L", id), + flow_id: format!("HYD_PTU{}_MOTOR_FLOW", id), + enabled_id: format!("HYD_PTU{}_VALVE_OPENED", id), is_enabled: false, is_active_right: false, is_active_left: false, @@ -216,13 +224,16 @@ impl Ptu { } pub struct HydLoop { + _color_id: String, + pressure_id: String, + reservoir_id: String, + fire_valve_id: String, fluid: HydFluid, accumulator_gas_pressure: Pressure, accumulator_gas_volume: Volume, accumulator_fluid_volume: Volume, accumulator_press_breakpoints: [f64; 9], accumulator_flow_carac: [f64; 9], - _color: LoopColor, connected_to_ptu_left_side: bool, connected_to_ptu_right_side: bool, loop_pressure: Pressure, @@ -235,6 +246,20 @@ pub struct HydLoop { current_flow: VolumeRate, current_max_flow: VolumeRate, //Current total max flow available from pressure sources fire_shutoff_valve_opened: bool, + has_fire_valve: bool, +} + +impl SimulationElement for HydLoop { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.pressure_id, self.get_pressure().get::()); + writer.write_f64( + &self.reservoir_id, + self.get_reservoir_volume().get::(), + ); + if self.has_fire_valve { + writer.write_bool(&self.fire_valve_id, self.get_fire_shutoff_valve_state()); + } + } } impl HydLoop { @@ -251,8 +276,9 @@ impl HydLoop { [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.005, 0.008, 0.01, 0.02, 0.08, 0.15, 0.35, 0.5]; + #[allow(clippy::too_many_arguments)] pub fn new( - _color: LoopColor, + 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, @@ -260,12 +286,17 @@ impl HydLoop { high_pressure_volume: Volume, reservoir_volume: Volume, fluid: HydFluid, + has_fire_valve: bool, ) -> HydLoop { HydLoop { + _color_id: id.to_uppercase(), + pressure_id: format!("HYD_{}_PRESSURE", id), + reservoir_id: format!("HYD_{}_RESERVOIR", id), + fire_valve_id: format!("HYD_{}_FIRE_VALVE_OPENED", id), + accumulator_gas_pressure: Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE), accumulator_gas_volume: Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME), accumulator_fluid_volume: Volume::new::(0.), - _color, connected_to_ptu_left_side, connected_to_ptu_right_side, loop_pressure: Pressure::new::(14.7), @@ -281,6 +312,7 @@ impl HydLoop { accumulator_flow_carac: HydLoop::ACCUMULATOR_FLOW_CARAC, current_max_flow: VolumeRate::new::(0.), fire_shutoff_valve_opened: true, + has_fire_valve, } } @@ -432,7 +464,7 @@ impl HydLoop { let mut reservoir_return = Volume::new::(0.); let mut delta_vol = Volume::new::(0.); - if self.fire_shutoff_valve_opened { + if self.fire_shutoff_valve_opened && self.has_fire_valve { for p in engine_driven_pumps { delta_vol_max += p.get_delta_vol_max(); delta_vol_min += p.get_delta_vol_min(); @@ -582,6 +614,9 @@ impl PressureSource for Pump { } pub struct ElectricPump { + _id: String, + active_id: String, + active: bool, rpm: f64, pump: Pump, @@ -589,7 +624,13 @@ pub struct ElectricPump { impl Default for ElectricPump { fn default() -> Self { - ElectricPump::new() + ElectricPump::new("DEFAULT") + } +} + +impl SimulationElement for ElectricPump { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_id, self.is_active()); } } @@ -603,8 +644,10 @@ impl ElectricPump { const DISPLACEMENT_MAP: [f64; 9] = [0.263, 0.263, 0.263, 0.263, 0.263, 0.263, 0.163, 0.0, 0.0]; const DISPLACEMENT_DYNAMICS: f64 = 1.0; //1 == No filtering - pub fn new() -> ElectricPump { + pub fn new(id: &str) -> ElectricPump { ElectricPump { + _id: String::from(id).to_uppercase(), + active_id: format!("HYD_{}_EPUMP_ACTIVE", id), active: false, rpm: 0., pump: Pump::new( @@ -654,13 +697,22 @@ impl PressureSource for ElectricPump { } pub struct EngineDrivenPump { + _id: String, + active_id: String, + active: bool, pump: Pump, } impl Default for EngineDrivenPump { fn default() -> Self { - EngineDrivenPump::new() + EngineDrivenPump::new("DEFAULT") + } +} + +impl SimulationElement for EngineDrivenPump { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_id, self.is_active()); } } @@ -677,8 +729,10 @@ impl EngineDrivenPump { const DISPLACEMENT_DYNAMICS: f64 = 0.3; //0.1 == 90% filtering on max displacement transient - pub fn new() -> EngineDrivenPump { + pub fn new(id: &str) -> EngineDrivenPump { EngineDrivenPump { + _id: String::from(id).to_uppercase(), + active_id: format!("HYD_{}_EDPUMP_ACTIVE", id), active: false, pump: Pump::new( EngineDrivenPump::DISPLACEMENT_BREAKPTS, @@ -723,6 +777,9 @@ impl PressureSource for EngineDrivenPump { } pub struct RatPropeller { + _id: String, + rpm_id: String, + pos: f64, speed: f64, acc: f64, @@ -732,7 +789,13 @@ pub struct RatPropeller { impl Default for RatPropeller { fn default() -> Self { - RatPropeller::new() + RatPropeller::new("") + } +} + +impl SimulationElement for RatPropeller { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.rpm_id, self.get_rpm()); } } @@ -745,8 +808,10 @@ impl RatPropeller { ]; const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 5., 1., 1.]; - pub fn new() -> RatPropeller { + pub fn new(id: &str) -> RatPropeller { RatPropeller { + _id: String::from(id).to_uppercase(), + rpm_id: format!("HYD_{}RAT_RPM", id), pos: RatPropeller::STOWED_ANGLE, speed: 0., acc: 0., @@ -810,6 +875,9 @@ impl RatPropeller { } } pub struct RatPump { + _id: String, + stow_pos_id: String, + active: bool, pump: Pump, pub prop: RatPropeller, @@ -819,7 +887,18 @@ pub struct RatPump { impl Default for RatPump { fn default() -> Self { - RatPump::new() + RatPump::new("") + } +} + +impl SimulationElement for RatPump { + fn accept(&mut self, visitor: &mut T) { + self.prop.accept(visitor); + visitor.visit(self); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.stow_pos_id, self.get_stow_position()); } } @@ -833,7 +912,7 @@ impl RatPump { const STOWING_SPEED: f64 = 1.; //Speed to go from 0 to 1 stow position per sec. 1 means full deploying in 1s - pub fn new() -> RatPump { + pub fn new(id: &str) -> RatPump { let mut max_disp = 0.; for v in RatPump::DISPLACEMENT_MAP.iter() { if v > &max_disp { @@ -842,13 +921,15 @@ impl RatPump { } RatPump { + _id: String::from(id).to_uppercase(), + stow_pos_id: format!("HYD_{}RAT_STOW_POSITION", id), active: false, pump: Pump::new( RatPump::DISPLACEMENT_BREAKPTS, RatPump::DISPLACEMENT_MAP, RatPump::DISPLACEMENT_DYNAMICS, ), - prop: RatPropeller::new(), + prop: RatPropeller::new(id), stowed_position: 0., max_displacement: max_disp, } @@ -948,7 +1029,7 @@ mod tests { //.ylim(-2.0, 2.0); curr_axis = curr_axis.grid(true); - idx = idx + 1; + idx += 1; all_axis.push(Some(curr_axis)); } @@ -976,8 +1057,8 @@ mod tests { //Sets initialisation values of each data before first step pub fn init(&mut self, start_time: f64, values: Vec) { self.time_vector.push(start_time); - for idx in 0..(values.len()) { - self.data_vector.push(vec![values[idx]]); + for v in values { + self.data_vector.push(vec![v]); } } @@ -1052,7 +1133,7 @@ mod tests { let mut edp1_history = History::new(edp1_var_names); let mut edp1 = engine_driven_pump(); - let mut green_loop = hydraulic_loop(LoopColor::Green); + let mut green_loop = hydraulic_loop("GREEN"); edp1.active = true; let init_n2 = Ratio::new::(55.0); @@ -1160,7 +1241,6 @@ mod tests { ], ); } - assert!(true); green_loop_history.show_matplotlib("green_loop_edp_simulation_press"); edp1_history.show_matplotlib("green_loop_edp_simulation_EDP1 data"); @@ -1171,7 +1251,7 @@ mod tests { //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s fn yellow_loop_epump_simulation() { let mut epump = electric_pump(); - let mut yellow_loop = hydraulic_loop(LoopColor::Yellow); + let mut yellow_loop = hydraulic_loop("YELLOW"); epump.active = true; let ct = context(Duration::from_millis(100)); @@ -1206,15 +1286,13 @@ mod tests { ); } } - - assert!(true) } #[test] //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s fn blue_loop_epump_simulation() { let mut epump = electric_pump(); - let mut blue_loop = hydraulic_loop(LoopColor::Blue); + let mut blue_loop = hydraulic_loop("BLUE"); epump.active = true; let ct = context(Duration::from_millis(100)); @@ -1249,15 +1327,13 @@ mod tests { ); } } - - assert!(true) } #[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 = RatPump::new(); - let mut blue_loop = hydraulic_loop(LoopColor::Blue); + let mut rat = RatPump::new(""); + let mut blue_loop = hydraulic_loop("BLUE"); let timestep = 0.05; let ct = context(Duration::from_secs_f64(timestep)); @@ -1322,8 +1398,6 @@ mod tests { } time += timestep; } - - assert!(true) } #[test] @@ -1368,16 +1442,16 @@ mod tests { let mut epump = electric_pump(); epump.stop(); - let mut yellow_loop = hydraulic_loop(LoopColor::Yellow); + let mut yellow_loop = hydraulic_loop("YELLOW"); let mut edp1 = engine_driven_pump(); assert!(!edp1.active); //Is off when created? let mut engine1 = engine(Ratio::new::(0.0)); - let mut green_loop = hydraulic_loop(LoopColor::Green); + let mut green_loop = hydraulic_loop("GREEN"); - let mut ptu = Ptu::new(); + let mut ptu = Ptu::new(""); let ct = context(Duration::from_millis(100)); @@ -1465,7 +1539,7 @@ mod tests { edp1.start(); } - if x >= 500 && x <= 600 { + if (500..=600).contains(&x) { //10s later and during 10s, ptu should stay inactive println!("------------IS PTU ACTIVE??------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); @@ -1479,14 +1553,14 @@ mod tests { assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); edp1.stop(); - // epump.active = false; + epump.stop(); } if x == 800 { //@80s diabling edp and epump println!("-----------IS PRESSURE OFF?-----------"); assert!(yellow_loop.loop_pressure < Pressure::new::(50.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(50.0)); + assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); assert!( green_loop.reservoir_volume > Volume::new::(0.0) @@ -1571,31 +1645,31 @@ mod tests { accu_green_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Green_acc"); accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); - - assert!(true) } - fn hydraulic_loop(loop_color: LoopColor) -> HydLoop { + fn hydraulic_loop(loop_color: &str) -> HydLoop { match loop_color { - LoopColor::Yellow => HydLoop::new( + "GREEN" => HydLoop::new( loop_color, - false, true, - Volume::new::(26.00), + false, + Volume::new::(26.41), Volume::new::(26.41), Volume::new::(10.0), Volume::new::(3.83), HydFluid::new(Pressure::new::(1450000000.0)), + true, ), - LoopColor::Green => HydLoop::new( + "YELLOW" => HydLoop::new( loop_color, - true, false, + true, Volume::new::(10.2), Volume::new::(10.2), Volume::new::(8.0), Volume::new::(3.3), HydFluid::new(Pressure::new::(1450000000.0)), + true, ), _ => HydLoop::new( loop_color, @@ -1606,16 +1680,17 @@ mod tests { Volume::new::(8.0), Volume::new::(1.5), HydFluid::new(Pressure::new::(1450000000.0)), + false, ), } } fn electric_pump() -> ElectricPump { - ElectricPump::new() + ElectricPump::new("DEFAULT") } fn engine_driven_pump() -> EngineDrivenPump { - EngineDrivenPump::new() + EngineDrivenPump::new("DEFAULT") } fn engine(n2: Ratio) -> Engine { @@ -1647,7 +1722,7 @@ mod tests { mod characteristics_tests { use super::*; - fn show_carac(figure_title: &str, output_caracteristics: &Vec) { + fn show_carac(figure_title: &str, output_caracteristics: &[PressureCaracteristic]) { use rustplotlib::{Axes2D, Line2D}; let mut all_axis: Vec> = Vec::new(); @@ -1694,10 +1769,10 @@ mod tests { #[test] fn epump_charac() { let mut output_caracteristics: Vec = Vec::new(); - let mut epump = ElectricPump::new(); + let mut epump = ElectricPump::new("YELLOW"); let context = context(Duration::from_secs_f64(0.0001)); //Small dt to freeze spool up effect - let mut green_loop = hydraulic_loop(LoopColor::Green); + let mut green_loop = hydraulic_loop("GREEN"); epump.start(); for pressure in (0..3500).step_by(500) { @@ -1726,10 +1801,10 @@ mod tests { //TODO broken until rpm relation repaired fn engine_d_pump_charac() { let mut output_caracteristics: Vec = Vec::new(); - let mut edpump = EngineDrivenPump::new(); + let mut edpump = EngineDrivenPump::new("GREEN"); //let context = context(Duration::from_secs_f64(0.0001) ); //Small dt to freeze spool up effect - let mut green_loop = hydraulic_loop(LoopColor::Green); + let mut green_loop = hydraulic_loop("GREEN"); let mut engine1 = engine(Ratio::new::(0.0)); edpump.start(); @@ -1780,16 +1855,25 @@ mod tests { ]; //Check before first element - assert!(interpolation(&xs1, &ys1, -500.0) == ys1[0]); + assert!((interpolation(&xs1, &ys1, -500.0) - ys1[0]).abs() < f64::EPSILON); //Check after last - assert!(interpolation(&xs1, &ys1, 100000000.0) == *ys1.last().unwrap()); + assert!( + (interpolation(&xs1, &ys1, 100000000.0) - *ys1.last().unwrap()).abs() + < f64::EPSILON + ); //Check equal first - assert!(interpolation(&xs1, &ys1, *xs1.first().unwrap()) == *ys1.first().unwrap()); + assert!( + (interpolation(&xs1, &ys1, *xs1.first().unwrap()) - *ys1.first().unwrap()).abs() + < f64::EPSILON + ); //Check equal last - assert!(interpolation(&xs1, &ys1, *xs1.last().unwrap()) == *ys1.last().unwrap()); + assert!( + (interpolation(&xs1, &ys1, *xs1.last().unwrap()) - *ys1.last().unwrap()).abs() + < f64::EPSILON + ); //Check interp middle let res = interpolation(&xs1, &ys1, 358.0); @@ -1809,7 +1893,7 @@ mod tests { for _idx in 0..1000000 { let test_val = rng.gen_range(xs1[0]..*xs1.last().unwrap()); let mut _res = interpolation(&xs1, &ys1, test_val); - _res = _res + 2.78; + _res += 2.78; } let time_elapsed = time_start.elapsed(); @@ -1833,7 +1917,7 @@ mod tests { #[test] fn starts_inactive() { - assert!(engine_driven_pump().active == false); + assert!(!engine_driven_pump().active); } #[test] @@ -1862,7 +1946,7 @@ mod tests { let eng = engine(n2); let mut edp = engine_driven_pump(); let dummy_update = Duration::from_secs(1); - let mut line = hydraulic_loop(LoopColor::Green); + let mut line = hydraulic_loop("GREEN"); edp.start(); line.loop_pressure = pressure; From 468b3aeb08bc300d2debea9c7ee56061ff060999 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 10 Mar 2021 15:14:37 +0100 Subject: [PATCH 034/122] Die clippy! --- src/systems/systems/src/hydraulic/mod.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index cbbf64d2d79..cc3db890479 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1006,13 +1006,12 @@ mod tests { extern crate rustplotlib; use rustplotlib::Figure; - fn make_figure<'a>(h: &'a History) -> Figure<'a> { + fn make_figure(h: &History) -> Figure { use rustplotlib::{Axes2D, Line2D}; let mut all_axis: Vec> = Vec::new(); - let mut idx = 0; - for cur_data in &h.data_vector { + 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()) @@ -1029,7 +1028,6 @@ mod tests { //.ylim(-2.0, 2.0); curr_axis = curr_axis.grid(true); - idx += 1; all_axis.push(Some(curr_axis)); } @@ -1070,8 +1068,8 @@ mod tests { } pub fn push_data(&mut self, values: Vec) { - for idx in 0..values.len() { - self.data_vector[idx].push(values[idx]); + for (idx, v) in values.iter().enumerate() { + self.data_vector[idx].push(*v); } } @@ -1086,8 +1084,8 @@ mod tests { for cur_data in self.data_vector { //Here build the 2 by Xsamples vector let mut new_vector: Vec<(f64, f64)> = Vec::new(); - for sample_idx in 0..self.time_vector.len() { - new_vector.push((self.time_vector[sample_idx], cur_data[sample_idx])); + for (idx, sample) in self.time_vector.iter().enumerate() { + new_vector.push((*sample, cur_data[idx])); } // We create our scatter plot from the data From 359668046684ce18abf26f74b7f8bd36a59f0f85 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 10 Mar 2021 20:34:50 +0100 Subject: [PATCH 035/122] updated hydraulic logic --- src/systems/a320_systems/src/hydraulic.rs | 99 +++++++++++++++-------- src/systems/systems/src/hydraulic/mod.rs | 8 +- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 6cddc930c71..255deeb414b 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -189,7 +189,7 @@ impl A320Hydraulic { //UPDATING HYDRAULICS AT FIXED STEP for _cur_loop in 0..num_of_update_loops { //Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly - self.update_hyd_logic_inputs(&min_hyd_loop_timestep, &overhead_panel, &ct); + self.update_logic(&min_hyd_loop_timestep, overhead_panel, ct); //Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic @@ -239,8 +239,6 @@ impl A320Hydraulic { Vec::new(), ); - self.update_hyd_avail_states(); - self.braking_circuit_norm .update(&min_hyd_loop_timestep, &self.green_loop); self.braking_circuit_altn @@ -262,29 +260,71 @@ impl A320Hydraulic { } } - pub fn update_hyd_logic_inputs( + fn update_logic( &mut self, delta_time_update: &Duration, overhead_panel: &A320HydraulicOverheadPanel, ct: &UpdateContext, ) { - let mut cargo_operated_ptu = false; - let mut cargo_operated_ypump = false; - let mut nsw_pin_inserted = false; + self.update_external_cond(&delta_time_update); + + self.update_hyd_avail_states(); + + self.update_pump_faults(); + + self.update_rat_deploy(ct); + + self.update_ed_pump_states(overhead_panel); + + self.update_e_pump_states(overhead_panel); + + self.update_ptu_logic(overhead_panel); + } + + fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + let ptu_inhibit = self.hyd_logic_inputs.cargo_operated_ptu_cond + && overhead_panel.yellow_epump_push_button.is_auto(); //TODO is auto will change once auto/on button is created in overhead library + if overhead_panel.ptu_push_button.is_auto() + && (!self.hyd_logic_inputs.weight_on_wheels + || self.hyd_logic_inputs.eng_1_master_on && self.hyd_logic_inputs.eng_2_master_on + || !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on + || (!self.hyd_logic_inputs.parking_brake_lever_pos + && !self.hyd_logic_inputs.nsw_pin_inserted_cond)) + && !ptu_inhibit + { + self.ptu.enabling(true); + } else { + self.ptu.enabling(false); + } + } + + fn update_rat_deploy(&mut self, ct: &UpdateContext) { + //RAT Deployment //Todo check all other needed conditions + if !self.hyd_logic_inputs.eng_1_master_on + && !self.hyd_logic_inputs.eng_2_master_on + && ct.indicated_airspeed > Velocity::new::(100.) + //Todo get speed from ADIRS + { + self.rat.set_active(); + } + } + fn update_external_cond(&mut self, delta_time_update: &Duration) { //Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update if self.hyd_logic_inputs.weight_on_wheels { - cargo_operated_ptu = self + self.hyd_logic_inputs.cargo_operated_ptu_cond = self .hyd_logic_inputs .is_cargo_operation_ptu_flag(&delta_time_update); - cargo_operated_ypump = self + self.hyd_logic_inputs.cargo_operated_ypump_cond = self .hyd_logic_inputs .is_cargo_operation_flag(&delta_time_update); - nsw_pin_inserted = self + self.hyd_logic_inputs.nsw_pin_inserted_cond = self .hyd_logic_inputs .is_nsw_pin_inserted_flag(&delta_time_update); } + } + fn update_pump_faults(&mut self) { //Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop //At current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong if self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() { @@ -307,16 +347,9 @@ impl A320Hydraulic { } else { self.hyd_logic_inputs.blue_epump_has_fault = false; } + } - //RAT Deployment //Todo check all other needed conditions - if !self.hyd_logic_inputs.eng_1_master_on - && !self.hyd_logic_inputs.eng_2_master_on - && ct.indicated_airspeed > Velocity::new::(100.) - //Todo get speed from ADIRS - { - self.rat.set_active(); - } - + fn update_ed_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { if overhead_panel.edp1_push_button.is_auto() && self.hyd_logic_inputs.eng_1_master_on && !overhead_panel.eng1_fire_pb.is_released() @@ -350,8 +383,12 @@ impl A320Hydraulic { } else { self.yellow_loop.set_fire_shutoff_valve_state(true); } + } - if overhead_panel.yellow_epump_push_button.is_off() || cargo_operated_ypump { + fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + if overhead_panel.yellow_epump_push_button.is_off() + || self.hyd_logic_inputs.cargo_operated_ypump_cond + { self.yellow_electric_pump.start(); } else if overhead_panel.yellow_epump_push_button.is_auto() { self.yellow_electric_pump.stop(); @@ -368,19 +405,6 @@ impl A320Hydraulic { } else if overhead_panel.blue_epump_push_button.is_off() { self.blue_electric_pump.stop(); } - - let ptu_inhibit = cargo_operated_ptu && overhead_panel.yellow_epump_push_button.is_auto(); //TODO is auto will change once auto/on button is created in overhead library - if overhead_panel.ptu_push_button.is_auto() - && (!self.hyd_logic_inputs.weight_on_wheels - || self.hyd_logic_inputs.eng_1_master_on && self.hyd_logic_inputs.eng_2_master_on - || !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on - || (!self.hyd_logic_inputs.parking_brake_lever_pos && !nsw_pin_inserted)) - && !ptu_inhibit - { - self.ptu.enabling(true); - } else { - self.ptu.enabling(false); - } } } @@ -390,6 +414,7 @@ impl SimulationElement for A320Hydraulic { self.blue_electric_pump.accept(visitor); self.engine_driven_pump_1.accept(visitor); self.engine_driven_pump_2.accept(visitor); + self.rat.accept(visitor); self.ptu.accept(visitor); @@ -541,6 +566,10 @@ pub struct A320HydraulicLogic { blue_epump_has_fault: bool, green_edp_has_fault: bool, yellow_edp_has_fault: bool, + + cargo_operated_ptu_cond: bool, + cargo_operated_ypump_cond: bool, + nsw_pin_inserted_cond: bool, } //Implements low level logic for all hydraulics commands @@ -569,6 +598,10 @@ impl A320HydraulicLogic { blue_epump_has_fault: false, green_edp_has_fault: false, yellow_edp_has_fault: false, + + cargo_operated_ptu_cond: false, + cargo_operated_ypump_cond: false, + nsw_pin_inserted_cond: false, } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index cc3db890479..739c8131e1e 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -102,7 +102,7 @@ impl Default for Ptu { impl SimulationElement for Ptu { fn write(&self, writer: &mut SimulatorWriter) { writer.write_bool(&self.active_left_id, self.is_active_left); - writer.write_bool(&self.active_right_id, self.is_active_left); + writer.write_bool(&self.active_right_id, self.is_active_right); writer.write_f64(&self.flow_id, self.get_flow().get::()); writer.write_bool(&self.enabled_id, self.is_enabled()); } @@ -111,12 +111,12 @@ impl SimulationElement for Ptu { impl Ptu { //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.1; - const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.08; + const FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE: f64 = 0.2; + const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.2; //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.7; + const AGRESSIVENESS_FACTOR: f64 = 0.8; pub fn new(id: &str) -> Ptu { Ptu { From c5d9356e67f638ace0406b38259666b9ce631802 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 21 Mar 2021 15:22:44 +0100 Subject: [PATCH 036/122] Finalized merge with electrical consumption PR --- src/systems/a320_systems/src/hydraulic.rs | 13 ++--- .../systems/src/hydraulic/brakecircuit.rs | 11 ++-- src/systems/systems/src/hydraulic/mod.rs | 54 +++++++++---------- .../systems/src/simulation/update_context.rs | 15 ++++-- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 992a586b0bd..acf7f537157 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -159,16 +159,16 @@ impl A320Hydraulic { ) { let min_hyd_loop_timestep = Duration::from_millis(A320Hydraulic::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz - self.total_sim_time_elapsed += ct.delta; + self.total_sim_time_elapsed += ct.delta(); //time to catch up in our simulation = new delta + time not updated last iteration - let time_to_catch = ct.delta + self.lag_time_accumulator; + let time_to_catch = ct.delta() + self.lag_time_accumulator; //Number of time steps to do according to required time step let number_of_steps_f64 = time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); //updating rat stowed pos on all frames in case it's used for graphics - self.rat.update_stow_pos(&ct.delta); + self.rat.update_stow_pos(&ct.delta()); if number_of_steps_f64 < 1.0 { //Can't do a full time step @@ -254,7 +254,7 @@ impl A320Hydraulic { min_hyd_loop_timestep / A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X for _cur_loop in 0..num_of_actuators_update_loops { self.rat - .update_physics(&delta_time_physics, &ct.indicated_airspeed); + .update_physics(&delta_time_physics, &ct.indicated_airspeed()); } } } @@ -301,7 +301,7 @@ impl A320Hydraulic { //RAT Deployment //Todo check all other needed conditions if !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on - && ct.indicated_airspeed > Velocity::new::(100.) + && ct.indicated_airspeed() > Velocity::new::(100.) //Todo get speed from ADIRS { self.rat.set_active(); @@ -774,7 +774,4 @@ pub mod tests { logic.pushback_state = 2.0; assert!(logic.is_nsw_pin_inserted_flag(&update_delta)); } - - pub fn update(&mut self, _: &UpdateContext) {} } -impl SimulationElement for A320Hydraulic {} diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 057b535b8c0..985209381a9 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -298,7 +298,7 @@ impl AutoBrakeController { pub fn update(&mut self, delta_time: &Duration, ct: &UpdateContext) { self.current_filtered_accel = self.current_filtered_accel - + (ct._longitudinal_acceleration - self.current_filtered_accel) + + (ct.long_accel() - self.current_filtered_accel) * (1. - E.powf( -delta_time.as_secs_f64() / AutoBrakeController::LONG_ACC_FILTER_TIMECONST, @@ -307,7 +307,7 @@ impl AutoBrakeController { self.current_accel_error = self.current_filtered_accel - self.accel_targets[self.current_selected_mode]; - if self.is_enabled && ct.is_on_ground { + if self.is_enabled && ct.is_on_ground() { let pterm = self.current_accel_error.get::() * AutoBrakeController::CONTROLLER_P_GAIN; let dterm = (self.current_accel_error - self.accel_error_prev) @@ -480,16 +480,15 @@ mod tests { Acceleration::new::(-3.), Acceleration::new::(-15.), ]); - let mut context = context(Duration::from_secs_f64(0.)); - context._longitudinal_acceleration = Acceleration::new::(0.0); + let context = context(Duration::from_secs_f64(0.)); assert!(controller.get_brake_command() <= 0.0); - controller.update(&context.delta, &context); + controller.update(&context.delta(), &context); assert!(controller.get_brake_command() <= 0.0); controller.set_enable(true); - controller.update(&context.delta, &context); + controller.update(&context.delta(), &context); assert!(controller.get_brake_command() >= 0.0); } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 739c8131e1e..33966f487f8 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1185,8 +1185,8 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&ct.delta, &green_loop, &engine1); - green_loop.update(&ct.delta, Vec::new(), vec![&edp1], Vec::new(), Vec::new()); + edp1.update(&ct.delta(), &green_loop, &engine1); + green_loop.update(&ct.delta(), Vec::new(), vec![&edp1], Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1214,7 +1214,7 @@ mod tests { } green_loop_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), green_loop.loop_volume.get::(), @@ -1223,14 +1223,14 @@ mod tests { ], ); edp1_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ edp1.get_delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); accu_green_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), green_loop.accumulator_gas_pressure.get::(), @@ -1263,8 +1263,8 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } - epump.update(&ct.delta, &yellow_loop); - yellow_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), Vec::new()); + epump.update(&ct.delta(), &yellow_loop); + yellow_loop.update(&ct.delta(), vec![&epump], Vec::new(), Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1304,8 +1304,8 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } - epump.update(&ct.delta, &blue_loop); - blue_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), Vec::new()); + epump.update(&ct.delta(), &blue_loop); + blue_loop.update(&ct.delta(), vec![&epump], Vec::new(), Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1335,11 +1335,11 @@ mod tests { let timestep = 0.05; let ct = context(Duration::from_secs_f64(timestep)); - let mut indicated_airpseed = ct.indicated_airspeed; + let mut indicated_airpseed = ct.indicated_airspeed(); let mut time = 0.0; for x in 0..1500 { - rat.update_stow_pos(&ct.delta); + rat.update_stow_pos(&ct.delta()); if time >= 10. && time < 10. + timestep { println!("ASSERT RAT STOWED"); assert!(blue_loop.loop_pressure <= Pressure::new::(50.0)); @@ -1375,9 +1375,9 @@ mod tests { assert!(rat.prop.rpm <= 2500.); } - rat.update_physics(&ct.delta, &indicated_airpseed); - rat.update(&ct.delta, &blue_loop); - blue_loop.update(&ct.delta, Vec::new(), Vec::new(), vec![&rat], Vec::new()); + rat.update_physics(&ct.delta(), &indicated_airpseed); + rat.update(&ct.delta(), &blue_loop); + blue_loop.update(&ct.delta(), Vec::new(), Vec::new(), vec![&rat], Vec::new()); if x % 20 == 0 { println!("Iteration {} Time {}", x, time); println!("-------------------------------------------"); @@ -1571,14 +1571,14 @@ mod tests { } ptu.update(&green_loop, &yellow_loop); - edp1.update(&ct.delta, &green_loop, &engine1); - epump.update(&ct.delta, &yellow_loop); + edp1.update(&ct.delta(), &green_loop, &engine1); + epump.update(&ct.delta(), &yellow_loop); - yellow_loop.update(&ct.delta, vec![&epump], Vec::new(), Vec::new(), vec![&ptu]); - green_loop.update(&ct.delta, Vec::new(), vec![&edp1], Vec::new(), vec![&ptu]); + yellow_loop.update(&ct.delta(), vec![&epump], Vec::new(), Vec::new(), vec![&ptu]); + green_loop.update(&ct.delta(), Vec::new(), vec![&edp1], Vec::new(), vec![&ptu]); loop_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), yellow_loop.loop_pressure.get::(), @@ -1589,7 +1589,7 @@ mod tests { ], ); ptu_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ ptu.flow_to_left.get::(), ptu.flow_to_right.get::(), @@ -1600,7 +1600,7 @@ mod tests { ); accu_green_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), green_loop.accumulator_gas_pressure.get::(), @@ -1609,7 +1609,7 @@ mod tests { ], ); accu_yellow_history.update( - ct.delta.as_secs_f64(), + ct.delta().as_secs_f64(), vec![ yellow_loop.loop_pressure.get::(), yellow_loop.accumulator_gas_pressure.get::(), @@ -1779,10 +1779,10 @@ mod tests { for rpm in (0..10000).step_by(150) { green_loop.loop_pressure = Pressure::new::(pressure as f64); epump.rpm = rpm as f64; - epump.update(&context.delta, &green_loop); + epump.update(&context.delta(), &green_loop); rpm_tab.push(rpm as f64); let flow = epump.get_delta_vol_max() - / Time::new::(context.delta.as_secs_f64()); + / Time::new::(context.delta().as_secs_f64()); let flow_gal = flow.get::() as f64; flow_tab.push(flow_gal); } @@ -1808,7 +1808,7 @@ mod tests { edpump.start(); let context = context(Duration::from_secs_f64(1.0)); //Small dt to freeze spool up effect - edpump.update(&context.delta, &green_loop, &engine1); + edpump.update(&context.delta(), &green_loop, &engine1); for pressure in (0..3500).step_by(500) { let mut rpm_tab: Vec = Vec::new(); let mut flow_tab: Vec = Vec::new(); @@ -1820,10 +1820,10 @@ mod tests { / (EngineDrivenPump::PUMP_N2_GEAR_RATIO * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), ); - edpump.update(&context.delta, &green_loop, &engine1); + edpump.update(&context.delta(), &green_loop, &engine1); rpm_tab.push(rpm as f64); let flow = edpump.get_delta_vol_max() - / Time::new::(context.delta.as_secs_f64()); + / Time::new::(context.delta().as_secs_f64()); let flow_gal = flow.get::() as f64; flow_tab.push(flow_gal); } diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index 0156baa533a..c04f932c05e 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -15,13 +15,14 @@ pub struct UpdateContext { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, is_on_ground: bool, - _longitudinal_acceleration: Acceleration, + 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, @@ -29,7 +30,7 @@ impl UpdateContext { indicated_altitude: Length, ambient_temperature: ThermodynamicTemperature, is_on_ground: bool, - _longitudinal_acceleration: Acceleration, + longitudinal_acceleration: Acceleration, ) -> UpdateContext { UpdateContext { delta, @@ -37,7 +38,7 @@ impl UpdateContext { indicated_altitude, ambient_temperature, is_on_ground, - _longitudinal_acceleration: Acceleration::new::(0.), + longitudinal_acceleration, } } @@ -55,8 +56,8 @@ impl UpdateContext { ), is_on_ground: reader.read_bool(UpdateContext::IS_ON_GROUND_KEY), delta: delta_time, - _longitudinal_acceleration: Acceleration::new::( - reader.read_f64("ACCELERATION BODY Z"), + longitudinal_acceleration: Acceleration::new::( + reader.read_f64(UpdateContext::ACCEL_BODY_Z_KEY), ), } } @@ -84,4 +85,8 @@ impl UpdateContext { pub fn is_on_ground(&self) -> bool { self.is_on_ground } + + pub fn long_accel(&self) -> Acceleration { + self.longitudinal_acceleration + } } From 9b3e4bfbdccce3616ff292bad08b9516d461092b Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:49:36 +0100 Subject: [PATCH 037/122] Added basic tests for A320 hydraulics --- Cargo.lock | 1 + src/systems/a320_systems/src/hydraulic.rs | 541 ++++++++++++++++++++- src/systems/a320_systems/src/lib.rs | 2 +- src/systems/systems/src/hydraulic/mod.rs | 24 +- src/systems/systems/src/simulation/test.rs | 12 +- 5 files changed, 560 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f0616d9c8f..b23a72c576f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,6 +731,7 @@ dependencies = [ "ntest", "num-derive", "num-traits", + "plotlib", "rand", "rustplotlib", "uom", diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index acf7f537157..cb70c3a13d0 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -502,15 +502,17 @@ impl A320HydraulicBrakingLogic { + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) * self.right_brake_yellow_command; if !self.anti_skid_activated { - self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.5); + self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.5); //0.5 is temporary implementation of pressure limitation around 1000psi self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.5); + //0.5 is temporary implementation of pressure limitation around 1000psi } } else { //Else we just use parking brake self.left_brake_yellow_command += dynamic_increment; - self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.7); + self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.7); //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes self.right_brake_yellow_command += dynamic_increment; self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.7); + //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes } self.left_brake_green_command -= dynamic_increment; self.right_brake_green_command -= dynamic_increment; @@ -754,24 +756,533 @@ impl SimulationElement for A320HydraulicOverheadPanel { } } +// #[cfg(test)] +// pub mod tests { +// use super::A320HydraulicLogic; +// use std::time::Duration; + +// fn hyd_logic() -> A320HydraulicLogic { +// A320HydraulicLogic::new() +// } + +// #[test] +// fn is_nws_pin_engaged_test() { +// let mut logic = hyd_logic(); + +// let update_delta = Duration::from_secs_f64(0.08); +// assert!(!logic.is_nsw_pin_inserted_flag(&update_delta)); + +// logic.pushback_angle = 1.001; +// logic.pushback_state = 2.0; +// assert!(logic.is_nsw_pin_inserted_flag(&update_delta)); +// } +// } + #[cfg(test)] -pub mod tests { - use super::A320HydraulicLogic; - use std::time::Duration; +mod a320_hydraulic_simvars { + use super::*; + use systems::simulation::test::SimulationTestBed; - fn hyd_logic() -> A320HydraulicLogic { - A320HydraulicLogic::new() + #[test] + fn writes_its_state() { + let mut hyd_logic = A320HydraulicLogic::new(); + let mut test_bed = SimulationTestBed::new(); + test_bed.run_without_update(&mut hyd_logic); + + assert!(test_bed.contains_key("HYD_GREEN_EDPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("HYD_BLUE_EPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("HYD_YELLOW_EDPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT")); + assert!(test_bed.contains_key("OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT")); + assert!(test_bed.contains_key("OVHD_HYD_EPUMPB_PB_HAS_FAULT")); + assert!(test_bed.contains_key("OVHD_HYD_EPUMPY_PB_HAS_FAULT")); } +} - #[test] - fn is_nws_pin_engaged_test() { - let mut logic = hyd_logic(); +#[cfg(test)] +mod tests { + use super::*; + + mod a320_hydraulics { + use super::*; + use systems::simulation::{test::SimulationTestBed, Aircraft}; + use uom::si::{length::foot, ratio::percent, velocity::knot}; + + struct A320HydraulicsTestAircraft { + engine_1: Engine, + engine_2: Engine, + hydraulics: A320Hydraulic, + overhead: A320HydraulicOverheadPanel, + } + impl A320HydraulicsTestAircraft { + fn new() -> Self { + Self { + engine_1: Engine::new(1), + engine_2: Engine::new(2), + hydraulics: A320Hydraulic::new(), + overhead: A320HydraulicOverheadPanel::new(), + } + } + + fn is_ptu_ena(&self) -> bool { + self.hydraulics.ptu.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() + } + + fn set_engine_1_n2(&mut self, n2: Ratio) { + self.engine_1.corrected_n2 = n2; + } + fn set_engine_2_n2(&mut self, n2: Ratio) { + self.engine_2.corrected_n2 = n2; + } + } + + 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.overhead.update_pb_faults(&self.hydraulics); + } + } + impl SimulationElement for A320HydraulicsTestAircraft { + fn accept(&mut self, visitor: &mut T) { + self.hydraulics.accept(visitor); + self.overhead.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, + )) + } + + 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_ptu_enabled(&self) -> bool { + self.aircraft.is_ptu_ena() + } + + 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 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 set_gear_compressed_switch(mut self, is_compressed: bool) -> Self { + self.simulation_test_bed.set_on_ground(is_compressed); + self + } + + fn set_cargo_door_state(mut self, position: f64) -> Self { + self.simulation_test_bed.write_f64("EXIT OPEN 5", position); + self + } + + fn start_eng1(mut self, n2: Ratio) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG1 STARTER ACTIVE", true); + self.aircraft.set_engine_1_n2(n2); + + self + } + + fn start_eng2(mut self, n2: Ratio) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG2 STARTER ACTIVE", true); + self.aircraft.set_engine_2_n2(n2); + + self + } + + fn stop_eng1(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG1 STARTER ACTIVE", false); + self.aircraft.set_engine_1_n2(Ratio::new::(0.)); + + self + } + + fn stop_eng2(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG2 STARTER ACTIVE", false); + self.aircraft.set_engine_2_n2(Ratio::new::(0.)); + + 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_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 + } - let update_delta = Duration::from_secs_f64(0.08); - assert!(!logic.is_nsw_pin_inserted_flag(&update_delta)); + 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_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.) + } + } + + fn test_bed() -> A320HydraulicsTestBed { + A320HydraulicsTestBed::new() + } + + fn test_bed_with() -> A320HydraulicsTestBed { + test_bed() + } - logic.pushback_angle = 1.001; - logic.pushback_state = 2.0; - assert!(logic.is_nsw_pin_inserted_flag(&update_delta)); + #[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::(50.)) + .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(Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU )); //Should re enabled after 40s + assert!(test_bed.is_ptu_enabled()); + } + + #[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()); + + //No pressure + 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.)); + + //Yellow epump ON / Waiting 10s + test_bed = test_bed + .set_yellow_e_pump(false) + .run_waiting_for(Duration::from_secs(10)); + + 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.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.)); + + //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 green_edp_buildup_test() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + //No pressure + 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.)); + + //Starting eng 1 + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .run_one_tick(); + //ALMOST No pressure + 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::(500.)); //Blue is auto run + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + + //Waiting for 5s pressure hsould be at 3000 psi + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .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 yellow_edp_buildup_test() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + //No pressure + 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.)); + + //Starting eng 1 + test_bed = test_bed + .start_eng2(Ratio::new::(50.)) + .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()); + assert!(test_bed.blue_pressure() < Pressure::new::(500.)); //Blue is auto run + assert!(!test_bed.is_yellow_pressurised()); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); + + //Waiting for 5s pressure hsould be at 3000 psi + test_bed = test_bed + .start_eng2(Ratio::new::(50.)) + .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::(2900.)); + + //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.)); + } } } diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 423561e09e7..6e89a09908b 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -110,7 +110,7 @@ impl Aircraft for A320 { ); self.hydraulic_overhead.update_pb_faults(&self.hydraulic); - + self.power_consumption.update(context); } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 33966f487f8..641c1d5c2ea 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1264,7 +1264,13 @@ mod tests { assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } epump.update(&ct.delta(), &yellow_loop); - yellow_loop.update(&ct.delta(), vec![&epump], Vec::new(), Vec::new(), Vec::new()); + yellow_loop.update( + &ct.delta(), + vec![&epump], + Vec::new(), + Vec::new(), + Vec::new(), + ); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1305,7 +1311,13 @@ mod tests { assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } epump.update(&ct.delta(), &blue_loop); - blue_loop.update(&ct.delta(), vec![&epump], Vec::new(), Vec::new(), Vec::new()); + blue_loop.update( + &ct.delta(), + vec![&epump], + Vec::new(), + Vec::new(), + Vec::new(), + ); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1574,7 +1586,13 @@ mod tests { edp1.update(&ct.delta(), &green_loop, &engine1); epump.update(&ct.delta(), &yellow_loop); - yellow_loop.update(&ct.delta(), vec![&epump], Vec::new(), Vec::new(), vec![&ptu]); + yellow_loop.update( + &ct.delta(), + vec![&epump], + Vec::new(), + Vec::new(), + vec![&ptu], + ); green_loop.update(&ct.delta(), Vec::new(), vec![&edp1], Vec::new(), vec![&ptu]); loop_history.update( diff --git a/src/systems/systems/src/simulation/test.rs b/src/systems/systems/src/simulation/test.rs index 88c4139024d..248ea684209 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; @@ -160,6 +163,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, From 7b7a15e9d1be50af6e25f8839b8d5927ad356644 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:54:33 +0100 Subject: [PATCH 038/122] cargo fmt --- src/systems/a320_systems/src/hydraulic.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index cb70c3a13d0..0b7001aec28 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1126,7 +1126,9 @@ mod tests { 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(Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU )); //Should re enabled after 40s + test_bed = test_bed.run_waiting_for(Duration::from_secs_f64( + A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU, + )); //Should re enabled after 40s assert!(test_bed.is_ptu_enabled()); } From 1d6c90107aaed97e950c178885999da09fa60593 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 24 Mar 2021 12:37:53 +0100 Subject: [PATCH 039/122] Firevalve tests --- src/systems/a320_systems/src/hydraulic.rs | 133 ++++++++++++++++++---- src/systems/systems/src/hydraulic/mod.rs | 6 +- 2 files changed, 113 insertions(+), 26 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index cb70c3a13d0..1c201236b41 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -756,28 +756,6 @@ impl SimulationElement for A320HydraulicOverheadPanel { } } -// #[cfg(test)] -// pub mod tests { -// use super::A320HydraulicLogic; -// use std::time::Duration; - -// fn hyd_logic() -> A320HydraulicLogic { -// A320HydraulicLogic::new() -// } - -// #[test] -// fn is_nws_pin_engaged_test() { -// let mut logic = hyd_logic(); - -// let update_delta = Duration::from_secs_f64(0.08); -// assert!(!logic.is_nsw_pin_inserted_flag(&update_delta)); - -// logic.pushback_angle = 1.001; -// logic.pushback_state = 2.0; -// assert!(logic.is_nsw_pin_inserted_flag(&update_delta)); -// } -// } - #[cfg(test)] mod a320_hydraulic_simvars { use super::*; @@ -919,6 +897,28 @@ mod tests { Pressure::new::(self.simulation_test_bed.read_f64("HYD_YELLOW_PRESSURE")) } + 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() } @@ -937,6 +937,18 @@ mod tests { 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 @@ -1024,6 +1036,8 @@ mod tests { 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) @@ -1126,7 +1140,9 @@ mod tests { 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(Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU )); //Should re enabled after 40s + test_bed = test_bed.run_waiting_for(Duration::from_secs_f64( + A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU, + )); //Should re enabled after 40s assert!(test_bed.is_ptu_enabled()); } @@ -1284,5 +1300,76 @@ mod tests { assert!(!test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); } + + #[test] + fn yellow_green_edp_firevalve_test() { + 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()); + + //No pressure + 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.)); + + 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::(50.)) + .start_eng1(Ratio::new::(50.)) + .run_waiting_for(Duration::from_secs(5)); + + //Waiting for 5s pressure hsould 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::(2900.)); + + 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.)); + } } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 641c1d5c2ea..b11d2925b0d 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -257,7 +257,7 @@ impl SimulationElement for HydLoop { self.get_reservoir_volume().get::(), ); if self.has_fire_valve { - writer.write_bool(&self.fire_valve_id, self.get_fire_shutoff_valve_state()); + writer.write_bool(&self.fire_valve_id, self.is_fire_shutoff_valve_opened()); } } } @@ -359,7 +359,7 @@ impl HydLoop { self.fire_shutoff_valve_opened = opened; } - pub fn get_fire_shutoff_valve_state(&self) -> bool { + pub fn is_fire_shutoff_valve_opened(&self) -> bool { self.fire_shutoff_valve_opened } @@ -464,7 +464,7 @@ impl HydLoop { let mut reservoir_return = Volume::new::(0.); let mut delta_vol = Volume::new::(0.); - if self.fire_shutoff_valve_opened && self.has_fire_valve { + if self.fire_shutoff_valve_opened { for p in engine_driven_pumps { delta_vol_max += p.get_delta_vol_max(); delta_vol_min += p.get_delta_vol_min(); From e2f89845542d288876ea59e4ad9a7bdb4d9fccea Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 24 Mar 2021 14:24:56 +0100 Subject: [PATCH 040/122] Added brake acc test --- src/systems/a320_systems/src/hydraulic.rs | 114 ++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 1c201236b41..2364d226e14 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -897,6 +897,41 @@ mod tests { Pressure::new::(self.simulation_test_bed.read_f64("HYD_YELLOW_PRESSURE")) } + 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_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 is_fire_valve_eng1_closed(&mut self) -> bool { !self .simulation_test_bed @@ -1046,6 +1081,20 @@ mod tests { .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.)) + } + + 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 } } @@ -1371,5 +1420,70 @@ mod tests { assert!(!test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); } + + #[test] + fn yellow_brake_accumulator_test() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + //No pressure + 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.)); + + //Accumulator empty on cold start + assert!(test_bed.get_brake_yellow_accumulator_pressure() < Pressure::new::(50.)); + //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() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + + //No brakes even if we brake + 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::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_yellow_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(15)); + + assert!(test_bed.is_yellow_pressurised()); + 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(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::(2000.)); + assert!(test_bed.get_brake_right_yellow_pressure() > Pressure::new::(2000.)); + + assert!(test_bed.get_brake_yellow_accumulator_pressure() > Pressure::new::(2500.)); + } } } From 05f8e88715fb786748e9f3ee5c0eab7f186010df Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 24 Mar 2021 20:14:04 +0100 Subject: [PATCH 041/122] More brake tests --- src/systems/a320_systems/src/hydraulic.rs | 156 ++++++++++++++++------ 1 file changed, 116 insertions(+), 40 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 2364d226e14..9d8aae910df 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1206,14 +1206,6 @@ mod tests { //Enabled on cold start assert!(test_bed.is_ptu_enabled()); - //No pressure - 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.)); - //Yellow epump ON / Waiting 10s test_bed = test_bed .set_yellow_e_pump(false) @@ -1252,14 +1244,6 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //No pressure - 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.)); - //Starting eng 1 test_bed = test_bed .start_eng1(Ratio::new::(50.)) @@ -1305,14 +1289,6 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //No pressure - 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.)); - //Starting eng 1 test_bed = test_bed .start_eng2(Ratio::new::(50.)) @@ -1362,14 +1338,6 @@ mod tests { test_bed = test_bed.set_ptu_state(false).run_one_tick(); assert!(!test_bed.is_ptu_enabled()); - //No pressure - 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.)); - assert!(!test_bed.is_fire_valve_eng1_closed()); assert!(!test_bed.is_fire_valve_eng2_closed()); @@ -1429,14 +1397,6 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //No pressure - 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.)); - //Accumulator empty on cold start assert!(test_bed.get_brake_yellow_accumulator_pressure() < Pressure::new::(50.)); //No brakes @@ -1485,5 +1445,121 @@ mod tests { assert!(test_bed.get_brake_yellow_accumulator_pressure() > Pressure::new::(2500.)); } + + #[test] + fn norm_brake_vs_altn_brake_test() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .run_one_tick(); + + //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() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_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_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_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_right_yellow_pressure() > Pressure::new::(950.)); + } + + #[test] + fn check_brake_inversion_test() { + 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::(1000.)); + 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.)); + } } } From f950b34a1d266d823b4ba275e531aa00cca43b8b Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 24 Mar 2021 20:27:03 +0100 Subject: [PATCH 042/122] brake test corrected --- src/systems/a320_systems/src/hydraulic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 9d8aae910df..ef063f72ddf 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1523,7 +1523,7 @@ mod tests { .set_right_brake(Ratio::new::(0.)) .run_waiting_for(Duration::from_secs(1)); - assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(1000.)); + 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.)); From 4528f6fe897f09546031e8670dbf328554a3489a Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 25 Mar 2021 12:02:56 +0100 Subject: [PATCH 043/122] Added autoOn overhead button Used for yellow Epump --- src/systems/a320_systems/src/hydraulic.rs | 15 +++--- src/systems/systems/src/overhead/mod.rs | 65 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index ef063f72ddf..904e3854a4e 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -4,7 +4,9 @@ use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::g use systems::engine::Engine; use systems::hydraulic::brakecircuit::BrakeCircuit; use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RatPump}; -use systems::overhead::{AutoOffFaultPushButton, FirePushButton, OnOffFaultPushButton}; +use systems::overhead::{ + AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, +}; use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; @@ -282,7 +284,7 @@ impl A320Hydraulic { fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { let ptu_inhibit = self.hyd_logic_inputs.cargo_operated_ptu_cond - && overhead_panel.yellow_epump_push_button.is_auto(); //TODO is auto will change once auto/on button is created in overhead library + && overhead_panel.yellow_epump_push_button.is_auto(); if overhead_panel.ptu_push_button.is_auto() && (!self.hyd_logic_inputs.weight_on_wheels || self.hyd_logic_inputs.eng_1_master_on && self.hyd_logic_inputs.eng_2_master_on @@ -298,7 +300,8 @@ impl A320Hydraulic { } fn update_rat_deploy(&mut self, ct: &UpdateContext) { - //RAT Deployment //Todo check all other needed conditions + //RAT Deployment + //Todo check all other needed conditions this is faked with engine master while it should check elec buses if !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on && ct.indicated_airspeed() > Velocity::new::(100.) @@ -385,7 +388,7 @@ impl A320Hydraulic { } fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - if overhead_panel.yellow_epump_push_button.is_off() + if overhead_panel.yellow_epump_push_button.is_on() || self.hyd_logic_inputs.cargo_operated_ypump_cond { self.yellow_electric_pump.start(); @@ -707,7 +710,7 @@ pub struct A320HydraulicOverheadPanel { pub blue_epump_push_button: AutoOffFaultPushButton, pub ptu_push_button: AutoOffFaultPushButton, pub rat_push_button: AutoOffFaultPushButton, - pub yellow_epump_push_button: AutoOffFaultPushButton, + pub yellow_epump_push_button: AutoOnFaultPushButton, pub blue_epump_override_push_button: OnOffFaultPushButton, pub eng1_fire_pb: FirePushButton, pub eng2_fire_pb: FirePushButton, @@ -721,7 +724,7 @@ impl A320HydraulicOverheadPanel { 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: AutoOffFaultPushButton::new_off("HYD_EPUMPY"), + yellow_epump_push_button: AutoOnFaultPushButton::new_auto("HYD_EPUMPY"), blue_epump_override_push_button: OnOffFaultPushButton::new_off("HYD_EPUMPY_OVRD"), eng1_fire_pb: FirePushButton::new("ENG1"), eng2_fire_pb: FirePushButton::new("ENG2"), diff --git a/src/systems/systems/src/overhead/mod.rs b/src/systems/systems/src/overhead/mod.rs index 95cc1295f32..3d8cc6f4954 100644 --- a/src/systems/systems/src/overhead/mod.rs +++ b/src/systems/systems/src/overhead/mod.rs @@ -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, From ef163ee6f4f0fbf06cdec107b275a3fbe3c88343 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 25 Mar 2021 17:49:51 +0100 Subject: [PATCH 044/122] Removed code duplication in timeout handling --- Cargo.lock | 1 + src/systems/a320_systems/Cargo.toml | 1 + src/systems/a320_systems/src/hydraulic.rs | 142 +++++++++++++++++----- 3 files changed, 113 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b23a72c576f..83e51a7a156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,6 +4,7 @@ name = "a320_systems" version = "0.1.0" dependencies = [ + "rand", "systems", "uom", ] 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 904e3854a4e..e35f866c57a 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -609,56 +609,64 @@ impl A320HydraulicLogic { } } - //TODO, code duplication to handle timeouts: generic function to do + //Given a condition and associated timer and timeout value, returns if timeout is over after condition went to false + fn timeout_condition_update( + current_timer: &mut Duration, + condition: bool, + max_duration: f64, + delta_time: &Duration, + ) -> bool { + if condition { + *current_timer = Duration::from_secs_f64(max_duration); + } else if *current_timer > *delta_time { + *current_timer -= *delta_time; + } else { + *current_timer = Duration::from_secs(0); + } + + *current_timer > Duration::from_secs_f64(0.0) + } + + //Checks if yellow pump should be activated by the cargo door maintenance panel operation pub fn is_cargo_operation_flag(&mut self, delta_time_update: &Duration) -> bool { let cargo_door_moved = (self.cargo_door_back_pos - self.cargo_door_back_pos_prev).abs() > f64::EPSILON || (self.cargo_door_front_pos - self.cargo_door_front_pos_prev).abs() > f64::EPSILON; - if cargo_door_moved { - self.cargo_door_timer = - Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP); - } else if self.cargo_door_timer > *delta_time_update { - self.cargo_door_timer -= *delta_time_update; - } else { - self.cargo_door_timer = Duration::from_secs(0); - } - - self.cargo_door_timer > Duration::from_secs_f64(0.0) + A320HydraulicLogic::timeout_condition_update( + &mut self.cargo_door_timer, + cargo_door_moved, + A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP, + delta_time_update, + ) } + //Checks if ptu inhibit condition is still active after the cargo door operation pub fn is_cargo_operation_ptu_flag(&mut self, delta_time_update: &Duration) -> bool { let cargo_door_moved = (self.cargo_door_back_pos - self.cargo_door_back_pos_prev).abs() > f64::EPSILON || (self.cargo_door_front_pos - self.cargo_door_front_pos_prev).abs() > f64::EPSILON; - if cargo_door_moved { - self.cargo_door_timer_ptu = - Duration::from_secs_f64(A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU); - } else if self.cargo_door_timer_ptu > *delta_time_update { - self.cargo_door_timer_ptu -= *delta_time_update; - } else { - self.cargo_door_timer_ptu = Duration::from_secs(0); - } - - self.cargo_door_timer_ptu > Duration::from_secs_f64(0.0) + A320HydraulicLogic::timeout_condition_update( + &mut self.cargo_door_timer_ptu, + cargo_door_moved, + A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU, + delta_time_update, + ) } + //Checks if pin is removed by ground crew after a pushback operation pub fn is_nsw_pin_inserted_flag(&mut self, delta_time_update: &Duration) -> bool { let pushback_in_progress = (self.pushback_angle - self.pushback_angle_prev).abs() > f64::EPSILON && (self.pushback_state - 3.0).abs() > f64::EPSILON; - if pushback_in_progress { - self.nws_tow_engaged_timer = - Duration::from_secs_f64(A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT); - } else if self.nws_tow_engaged_timer > *delta_time_update { - self.nws_tow_engaged_timer -= *delta_time_update; //TODO CHECK if rollover issue to expect if not limiting to 0 - } else { - self.nws_tow_engaged_timer = Duration::from_secs(0); - } - - self.nws_tow_engaged_timer > Duration::from_secs(0) + A320HydraulicLogic::timeout_condition_update( + &mut self.nws_tow_engaged_timer, + pushback_in_progress, + A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT, + delta_time_update, + ) } } @@ -784,6 +792,7 @@ mod a320_hydraulic_simvars { #[cfg(test)] mod tests { use super::*; + use rand::Rng; mod a320_hydraulics { use super::*; @@ -806,6 +815,14 @@ mod tests { } } + fn is_nws_pin_inserted(&self) -> bool { + self.hydraulics.hyd_logic_inputs.nsw_pin_inserted_cond + } + + fn is_cargo_powering_yellow_epump(&self) -> bool { + self.hydraulics.hyd_logic_inputs.cargo_operated_ypump_cond + } + fn is_ptu_ena(&self) -> bool { self.hydraulics.ptu.is_enabled() } @@ -992,6 +1009,19 @@ mod tests { 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 ENG1 STARTER ACTIVE", true); @@ -1198,6 +1228,56 @@ mod tests { 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(Duration::from_secs_f64( + A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT, + )); + + 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(Duration::from_secs_f64( + A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP, + )); + + assert!(!test_bed.aircraft.is_cargo_powering_yellow_epump()); + } + #[test] fn ptu_pressurise_green_from_yellow_epump() { let mut test_bed = test_bed_with() From 35fd2332101fdac1d9bb93d0b310ffed55a4fd29 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 26 Mar 2021 12:20:59 +0100 Subject: [PATCH 045/122] testing new core hydraulic regulation --- src/systems/a320_systems/src/hydraulic.rs | 4 ++-- src/systems/systems/src/hydraulic/mod.rs | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index e35f866c57a..0059621c4bc 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1394,7 +1394,7 @@ mod tests { 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.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2800.)); //Stoping engine, pressure should fall in 20s test_bed = test_bed @@ -1436,7 +1436,7 @@ mod tests { 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.)); + assert!(test_bed.yellow_pressure() > Pressure::new::(2800.)); assert!(!test_bed.is_fire_valve_eng1_closed()); assert!(!test_bed.is_fire_valve_eng2_closed()); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index b11d2925b0d..0dec2ac8ed8 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -270,7 +270,7 @@ impl HydLoop { //Low pass filter on pressure. This has to be pretty high not to modify behavior of the loop, but still dampening numerical instability const PRESSURE_LOW_PASS_FILTER: f64 = 0.75; - const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.1; + const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.4; const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; @@ -535,23 +535,22 @@ impl HydLoop { .min(delta_vol_min.max(delta_vol_max.min(volume_needed_to_reach_pressure_target))); delta_vol += actual_volume_added_to_pressurise; - //Loop Pressure update From Bulk modulus - let press_delta = self.delta_pressure_from_delta_volume(delta_vol); - let new_raw_press = self.loop_pressure + press_delta; //New raw pressure before we filter it - - self.loop_pressure = HydLoop::PRESSURE_LOW_PASS_FILTER * new_raw_press - + (1. - HydLoop::PRESSURE_LOW_PASS_FILTER) * self.loop_pressure; - self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure //Update reservoir self.reservoir_volume -= actual_volume_added_to_pressurise; //%limit to 0 min? for case of negative added? 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 = HydLoop::DELTA_VOL_LOW_PASS_FILTER * delta_vol + (1. - HydLoop::DELTA_VOL_LOW_PASS_FILTER) * self.current_delta_vol; - self.loop_volume += delta_vol; + + //Loop Pressure update From Bulk modulus + let press_delta = self.delta_pressure_from_delta_volume(delta_vol); + self.loop_pressure += press_delta; + self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure + self.current_delta_vol = delta_vol; self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); @@ -1174,10 +1173,10 @@ mod tests { for x in 0..600 { if x == 50 { //After 5s - assert!(green_loop.loop_pressure >= Pressure::new::(2950.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); } if x == 200 { - assert!(green_loop.loop_pressure >= Pressure::new::(2950.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); edp1.stop(); } if x >= 500 { From db9721891482a5d2e95d528566f024ea8befe5ae Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 29 Mar 2021 20:17:11 +0200 Subject: [PATCH 046/122] UpdateContext vars renamed --- src/systems/a320_systems/src/hydraulic.rs | 20 +++++++++---------- .../systems/src/hydraulic/brakecircuit.rs | 7 +++---- src/systems/systems/src/hydraulic/mod.rs | 12 +++++------ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 0059621c4bc..6e9ae65ad78 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -154,23 +154,23 @@ impl A320Hydraulic { pub fn update( &mut self, - ct: &UpdateContext, + context: &UpdateContext, engine1: &Engine, engine2: &Engine, overhead_panel: &A320HydraulicOverheadPanel, ) { let min_hyd_loop_timestep = Duration::from_millis(A320Hydraulic::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz - self.total_sim_time_elapsed += ct.delta(); + 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 = ct.delta() + self.lag_time_accumulator; + let time_to_catch = context.delta() + self.lag_time_accumulator; //Number of time steps to do according to required time step let number_of_steps_f64 = time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); //updating rat stowed pos on all frames in case it's used for graphics - self.rat.update_stow_pos(&ct.delta()); + self.rat.update_stow_pos(&context.delta()); if number_of_steps_f64 < 1.0 { //Can't do a full time step @@ -190,7 +190,7 @@ impl A320Hydraulic { //UPDATING HYDRAULICS AT FIXED STEP for _cur_loop in 0..num_of_update_loops { //Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly - self.update_logic(&min_hyd_loop_timestep, overhead_panel, ct); + self.update_logic(&min_hyd_loop_timestep, overhead_panel, context); //Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic @@ -256,7 +256,7 @@ impl A320Hydraulic { min_hyd_loop_timestep / A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X for _cur_loop in 0..num_of_actuators_update_loops { self.rat - .update_physics(&delta_time_physics, &ct.indicated_airspeed()); + .update_physics(&delta_time_physics, &context.indicated_airspeed()); } } } @@ -265,7 +265,7 @@ impl A320Hydraulic { &mut self, delta_time_update: &Duration, overhead_panel: &A320HydraulicOverheadPanel, - ct: &UpdateContext, + context: &UpdateContext, ) { self.update_external_cond(&delta_time_update); @@ -273,7 +273,7 @@ impl A320Hydraulic { self.update_pump_faults(); - self.update_rat_deploy(ct); + self.update_rat_deploy(context); self.update_ed_pump_states(overhead_panel); @@ -299,12 +299,12 @@ impl A320Hydraulic { } } - fn update_rat_deploy(&mut self, ct: &UpdateContext) { + fn update_rat_deploy(&mut self, context: &UpdateContext) { //RAT Deployment //Todo check all other needed conditions this is faked with engine master while it should check elec buses if !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on - && ct.indicated_airspeed() > Velocity::new::(100.) + && context.indicated_airspeed() > Velocity::new::(100.) //Todo get speed from ADIRS { self.rat.set_active(); diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 985209381a9..034ca44f709 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -140,7 +140,6 @@ impl BrakeCircuit { pub fn update( &mut self, delta_time: &Duration, - //context: &UpdateContext, hyd_loop: &HydLoop, ) { let delta_vol = ((self.demanded_brake_position_left - self.current_brake_position_left) @@ -296,9 +295,9 @@ impl AutoBrakeController { } } - pub fn update(&mut self, delta_time: &Duration, ct: &UpdateContext) { + pub fn update(&mut self, delta_time: &Duration, context: &UpdateContext) { self.current_filtered_accel = self.current_filtered_accel - + (ct.long_accel() - self.current_filtered_accel) + + (context.long_accel() - self.current_filtered_accel) * (1. - E.powf( -delta_time.as_secs_f64() / AutoBrakeController::LONG_ACC_FILTER_TIMECONST, @@ -307,7 +306,7 @@ impl AutoBrakeController { self.current_accel_error = self.current_filtered_accel - self.accel_targets[self.current_selected_mode]; - if self.is_enabled && ct.is_on_ground() { + if self.is_enabled && context.is_on_ground() { let pterm = self.current_accel_error.get::() * AutoBrakeController::CONTROLLER_P_GAIN; let dterm = (self.current_accel_error - self.accel_error_prev) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 0dec2ac8ed8..e33c30e9213 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1135,7 +1135,7 @@ mod tests { let init_n2 = Ratio::new::(55.0); let engine1 = engine(init_n2); - let ct = context(Duration::from_millis(100)); + let context = context(Duration::from_millis(100)); let green_acc_var_names = vec![ "Loop Pressure".to_string(), @@ -1184,8 +1184,8 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&ct.delta(), &green_loop, &engine1); - green_loop.update(&ct.delta(), Vec::new(), vec![&edp1], Vec::new(), Vec::new()); + edp1.update(&context.delta(), &green_loop, &engine1); + green_loop.update(&context.delta(), Vec::new(), vec![&edp1], Vec::new(), Vec::new()); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1213,7 +1213,7 @@ mod tests { } green_loop_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), green_loop.loop_volume.get::(), @@ -1222,14 +1222,14 @@ mod tests { ], ); edp1_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ edp1.get_delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); accu_green_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), green_loop.accumulator_gas_pressure.get::(), From 1009f63d747f639be5da798d63fe229e36306ba5 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 09:34:38 +0200 Subject: [PATCH 047/122] Format and hydraulic update subfunction split --- src/systems/a320_systems/src/hydraulic.rs | 175 ++++++++++-------- .../systems/src/hydraulic/brakecircuit.rs | 6 +- src/systems/systems/src/hydraulic/mod.rs | 125 +++++++------ 3 files changed, 165 insertions(+), 141 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 6e9ae65ad78..2495af14943 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -107,7 +107,7 @@ impl A320Hydraulic { } } - //Updates pressure available state based on pressure switches + // Updates pressure available state based on pressure switches fn update_hyd_avail_states(&mut self) { if self.green_loop.get_pressure() <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) @@ -163,93 +163,49 @@ impl A320Hydraulic { self.total_sim_time_elapsed += context.delta(); - //time to catch up in our simulation = new delta + time not updated last iteration + // 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 to do according to required time step - let number_of_steps_f64 = time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); + // 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(); - //updating rat stowed pos on all frames in case it's used for graphics + // Updating rat stowed pos on all frames in case it's used for graphics self.rat.update_stow_pos(&context.delta()); - if number_of_steps_f64 < 1.0 { - //Can't do a full time step - //we can either do an update with smaller step or wait next iteration + 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 - self.lag_time_accumulator = - Duration::from_secs_f64(number_of_steps_f64 * min_hyd_loop_timestep.as_secs_f64()); - //Time lag is float part of num of steps * fixed time step to get a result in time + // 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 { - let num_of_update_loops = number_of_steps_f64.floor() as u32; //Int part is the actual number of loops to do - //Rest of floating part goes into accumulator + // 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_f64 - (num_of_update_loops as 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 - - //UPDATING HYDRAULICS AT FIXED STEP - for _cur_loop in 0..num_of_update_loops { - //Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly - self.update_logic(&min_hyd_loop_timestep, overhead_panel, context); - - //Process brake logic (which circuit brakes) and send brake demands (how much) - self.hyd_brake_logic - .update_brake_demands(&min_hyd_loop_timestep, &self.green_loop); - self.hyd_brake_logic.send_brake_demands( - &mut self.braking_circuit_norm, - &mut self.braking_circuit_altn, - ); - - //UPDATE HYDRAULICS FIXED TIME STEP - self.ptu.update(&self.green_loop, &self.yellow_loop); - self.engine_driven_pump_1.update( - &min_hyd_loop_timestep, - &self.green_loop, - &engine1, - ); - self.engine_driven_pump_2.update( - &min_hyd_loop_timestep, - &self.yellow_loop, - &engine2, - ); - self.yellow_electric_pump - .update(&min_hyd_loop_timestep, &self.yellow_loop); - self.blue_electric_pump - .update(&min_hyd_loop_timestep, &self.blue_loop); - - self.rat.update(&min_hyd_loop_timestep, &self.blue_loop); - self.green_loop.update( - &min_hyd_loop_timestep, - Vec::new(), - vec![&self.engine_driven_pump_1], - Vec::new(), - vec![&self.ptu], - ); - self.yellow_loop.update( + ); // Keep track of time left after all fixed loop are done + + // UPDATING HYDRAULICS AT FIXED STEP + for _ in 0..num_of_update_loops { + self.update_fixed_step( + context, + engine1, + engine2, + overhead_panel, &min_hyd_loop_timestep, - vec![&self.yellow_electric_pump], - vec![&self.engine_driven_pump_2], - Vec::new(), - vec![&self.ptu], ); - self.blue_loop.update( - &min_hyd_loop_timestep, - vec![&self.blue_electric_pump], - Vec::new(), - vec![&self.rat], - Vec::new(), - ); - - self.braking_circuit_norm - .update(&min_hyd_loop_timestep, &self.green_loop); - self.braking_circuit_altn - .update(&min_hyd_loop_timestep, &self.yellow_loop); - self.braking_circuit_norm.reset_accumulators(); - self.braking_circuit_altn.reset_accumulators(); } - //UPDATING ACTUATOR PHYSICS AT "FIXED STEP / ACTUATORS_SIM_TIME_STEP_MULT" - //Here put everything that needs higher simulation rates + // UPDATING ACTUATOR PHYSICS AT "FIXED STEP / ACTUATORS_SIM_TIME_STEP_MULT" + // Here put everything that needs higher simulation rates let num_of_actuators_update_loops = num_of_update_loops * A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; let delta_time_physics = @@ -261,6 +217,67 @@ impl A320Hydraulic { } } + // 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: &Engine, + engine2: &Engine, + overhead_panel: &A320HydraulicOverheadPanel, + min_hyd_loop_timestep: &Duration, + ) { + // Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly + self.update_logic(&min_hyd_loop_timestep, overhead_panel, context); + + // Process brake logic (which circuit brakes) and send brake demands (how much) + self.hyd_brake_logic + .update_brake_demands(&min_hyd_loop_timestep, &self.green_loop); + self.hyd_brake_logic.send_brake_demands( + &mut self.braking_circuit_norm, + &mut self.braking_circuit_altn, + ); + + self.ptu.update(&self.green_loop, &self.yellow_loop); + self.engine_driven_pump_1 + .update(&min_hyd_loop_timestep, &self.green_loop, &engine1); + self.engine_driven_pump_2 + .update(&min_hyd_loop_timestep, &self.yellow_loop, &engine2); + self.yellow_electric_pump + .update(&min_hyd_loop_timestep, &self.yellow_loop); + self.blue_electric_pump + .update(&min_hyd_loop_timestep, &self.blue_loop); + + self.rat.update(&min_hyd_loop_timestep, &self.blue_loop); + self.green_loop.update( + &min_hyd_loop_timestep, + Vec::new(), + vec![&self.engine_driven_pump_1], + Vec::new(), + vec![&self.ptu], + ); + self.yellow_loop.update( + &min_hyd_loop_timestep, + vec![&self.yellow_electric_pump], + vec![&self.engine_driven_pump_2], + Vec::new(), + vec![&self.ptu], + ); + self.blue_loop.update( + &min_hyd_loop_timestep, + vec![&self.blue_electric_pump], + Vec::new(), + vec![&self.rat], + Vec::new(), + ); + + self.braking_circuit_norm + .update(&min_hyd_loop_timestep, &self.green_loop); + self.braking_circuit_altn + .update(&min_hyd_loop_timestep, &self.yellow_loop); + self.braking_circuit_norm.reset_accumulators(); + self.braking_circuit_altn.reset_accumulators(); + } + fn update_logic( &mut self, delta_time_update: &Duration, @@ -300,7 +317,7 @@ impl A320Hydraulic { } fn update_rat_deploy(&mut self, context: &UpdateContext) { - //RAT Deployment + // RAT Deployment //Todo check all other needed conditions this is faked with engine master while it should check elec buses if !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on @@ -312,7 +329,7 @@ impl A320Hydraulic { } fn update_external_cond(&mut self, delta_time_update: &Duration) { - //Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update + // Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update if self.hyd_logic_inputs.weight_on_wheels { self.hyd_logic_inputs.cargo_operated_ptu_cond = self .hyd_logic_inputs @@ -327,8 +344,8 @@ impl A320Hydraulic { } fn update_pump_faults(&mut self) { - //Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - //At current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong if self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() { self.hyd_logic_inputs.yellow_epump_has_fault = true; } else { diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 034ca44f709..9811b0c93c3 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -137,11 +137,7 @@ impl BrakeCircuit { } } - pub fn update( - &mut self, - delta_time: &Duration, - hyd_loop: &HydLoop, - ) { + pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydLoop) { let delta_vol = ((self.demanded_brake_position_left - self.current_brake_position_left) + (self.demanded_brake_position_right - self.current_brake_position_right)) * self.total_displacement; diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index e33c30e9213..7120497462c 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -265,10 +265,6 @@ impl SimulationElement for HydLoop { impl HydLoop { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI const ACCUMULATOR_MAX_VOLUME: f64 = 0.264; // in gallons - //const HYDRAULIC_FLUID_DENSITY: f64 = 1000.55; // Exxon Hyjet IV, kg/m^3 - - //Low pass filter on pressure. This has to be pretty high not to modify behavior of the loop, but still dampening numerical instability - const PRESSURE_LOW_PASS_FILTER: f64 = 0.75; const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.4; @@ -279,8 +275,8 @@ impl HydLoop { #[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 + 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, @@ -332,7 +328,7 @@ impl HydLoop { drawn } - //Returns the max flow that can be output from reservoir in dt time + // Returns the max flow that can be output from reservoir in dt time pub fn get_usable_reservoir_flow(&self, amount: VolumeRate, delta_time: Time) -> VolumeRate { let mut drawn = amount; @@ -343,13 +339,13 @@ impl HydLoop { drawn } - //Method to update pressure of a loop. The more delta volume is added, the more pressure rises - //Directly from bulk modulus equation + // Method to update pressure of a loop. The more delta volume is added, the more pressure rises + // directly from bulk modulus equation pub fn delta_pressure_from_delta_volume(&self, delta_vol: Volume) -> Pressure { return delta_vol / self.high_pressure_volume * self.fluid.get_bulk_mod(); } - //Gives the exact volume of fluid needed to get to any target_press pressure + // Gives the exact volume of fluid needed to get to any target_press pressure pub fn vol_to_target(&self, target_press: Pressure) -> Volume { (target_press - self.loop_pressure) * (self.high_pressure_volume) / self.fluid.get_bulk_mod() @@ -371,9 +367,9 @@ impl HydLoop { accumulator_delta_press.get::().abs(), )); - //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 + // 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 { let volume_from_acc = self .accumulator_fluid_volume @@ -411,7 +407,7 @@ impl HydLoop { ptu_act = true; } if ptu.flow_to_left > VolumeRate::new::(0.0) { - //were are left side of PTU and positive flow so we receive flow using own reservoir + // 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, Time::new::(delta_time.as_secs_f64()), @@ -419,8 +415,8 @@ impl HydLoop { self.reservoir_volume -= actual_flow * Time::new::(delta_time.as_secs_f64()); } else { - //we are using own flow to power right side so we send that back - //to our own reservoir + // 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 * Time::new::(delta_time.as_secs_f64()); @@ -431,7 +427,7 @@ impl HydLoop { ptu_act = true; } if ptu.flow_to_right > VolumeRate::new::(0.0) { - //were are right side of PTU and positive flow so we receive flow using own reservoir + // 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, Time::new::(delta_time.as_secs_f64()), @@ -439,8 +435,8 @@ impl HydLoop { self.reservoir_volume -= actual_flow * Time::new::(delta_time.as_secs_f64()); } else { - //we are using own flow to power left side so we send that back - //to our own reservoir + // 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 * Time::new::(delta_time.as_secs_f64()); @@ -479,10 +475,10 @@ impl HydLoop { delta_vol_min += p.get_delta_vol_min(); } - //Storing max pump capacity available. for now used in PTU model to limit it's input flow + // Storing max pump capacity available. for now used in PTU model to limit it's input flow self.current_max_flow = delta_vol_max / Time::new::(delta_time.as_secs_f64()); - //Static leaks + // 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::( @@ -496,10 +492,10 @@ impl HydLoop { //Updates current delta_vol and reservoir return quantity based on current ptu flows self.update_ptu_flows(delta_time, ptus, &mut delta_vol, &mut reservoir_return); - //Updates current accumulator state and updates loop delta_vol + // Updates current accumulator state and updates loop delta_vol self.update_accumulator(delta_time, &mut delta_vol); - //Priming the loop if not filled in yet + // 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; @@ -522,36 +518,34 @@ impl HydLoop { delta_vol -= used_fluid_qty; - //How much we need to reach target of 3000? + // 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. + // 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 + // 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 + // Update reservoir self.reservoir_volume -= actual_volume_added_to_pressurise; //%limit to 0 min? for case of negative added? self.reservoir_volume += reservoir_return; - //Update Volumes + // Update Volumes self.loop_volume += delta_vol; - //Low pass filter on final delta vol to help with stability and final flow noise + // Low pass filter on final delta vol to help with stability and final flow noise delta_vol = HydLoop::DELTA_VOL_LOW_PASS_FILTER * delta_vol + (1. - HydLoop::DELTA_VOL_LOW_PASS_FILTER) * self.current_delta_vol; - //Loop Pressure update From Bulk modulus + // Loop Pressure update From Bulk modulus let press_delta = self.delta_pressure_from_delta_volume(delta_vol); self.loop_pressure += press_delta; self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure - self.current_delta_vol = delta_vol; self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); } @@ -562,7 +556,7 @@ pub struct Pump { delta_vol_min: Volume, press_breakpoints: [f64; 9], displacement_carac: [f64; 9], - displacement_dynamic: f64, //Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic + displacement_dynamic: f64, // Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic } impl Pump { fn new( @@ -1185,7 +1179,13 @@ mod tests { } edp1.update(&context.delta(), &green_loop, &engine1); - green_loop.update(&context.delta(), Vec::new(), vec![&edp1], Vec::new(), Vec::new()); + green_loop.update( + &context.delta(), + Vec::new(), + vec![&edp1], + Vec::new(), + Vec::new(), + ); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); @@ -1251,7 +1251,7 @@ mod tests { let mut yellow_loop = hydraulic_loop("YELLOW"); epump.active = true; - let ct = context(Duration::from_millis(100)); + let context = context(Duration::from_millis(100)); for x in 0..800 { if x == 400 { assert!(yellow_loop.loop_pressure >= Pressure::new::(2800.0)); @@ -1262,9 +1262,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } - epump.update(&ct.delta(), &yellow_loop); + epump.update(&context.delta(), &yellow_loop); yellow_loop.update( - &ct.delta(), + &context.delta(), vec![&epump], Vec::new(), Vec::new(), @@ -1298,7 +1298,7 @@ mod tests { let mut blue_loop = hydraulic_loop("BLUE"); epump.active = true; - let ct = context(Duration::from_millis(100)); + let context = context(Duration::from_millis(100)); for x in 0..800 { if x == 400 { assert!(blue_loop.loop_pressure >= Pressure::new::(2800.0)); @@ -1309,9 +1309,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } - epump.update(&ct.delta(), &blue_loop); + epump.update(&context.delta(), &blue_loop); blue_loop.update( - &ct.delta(), + &context.delta(), vec![&epump], Vec::new(), Vec::new(), @@ -1345,12 +1345,12 @@ mod tests { let mut blue_loop = hydraulic_loop("BLUE"); let timestep = 0.05; - let ct = context(Duration::from_secs_f64(timestep)); - let mut indicated_airpseed = ct.indicated_airspeed(); + let context = context(Duration::from_secs_f64(timestep)); + let mut indicated_airpseed = context.indicated_airspeed(); let mut time = 0.0; for x in 0..1500 { - rat.update_stow_pos(&ct.delta()); + rat.update_stow_pos(&context.delta()); if time >= 10. && time < 10. + timestep { println!("ASSERT RAT STOWED"); assert!(blue_loop.loop_pressure <= Pressure::new::(50.0)); @@ -1386,9 +1386,15 @@ mod tests { assert!(rat.prop.rpm <= 2500.); } - rat.update_physics(&ct.delta(), &indicated_airpseed); - rat.update(&ct.delta(), &blue_loop); - blue_loop.update(&ct.delta(), Vec::new(), Vec::new(), vec![&rat], Vec::new()); + rat.update_physics(&context.delta(), &indicated_airpseed); + rat.update(&context.delta(), &blue_loop); + blue_loop.update( + &context.delta(), + Vec::new(), + Vec::new(), + vec![&rat], + Vec::new(), + ); if x % 20 == 0 { println!("Iteration {} Time {}", x, time); println!("-------------------------------------------"); @@ -1462,7 +1468,7 @@ mod tests { let mut ptu = Ptu::new(""); - let ct = context(Duration::from_millis(100)); + let context = context(Duration::from_millis(100)); loop_history.init( 0.0, @@ -1582,20 +1588,26 @@ mod tests { } ptu.update(&green_loop, &yellow_loop); - edp1.update(&ct.delta(), &green_loop, &engine1); - epump.update(&ct.delta(), &yellow_loop); + edp1.update(&context.delta(), &green_loop, &engine1); + epump.update(&context.delta(), &yellow_loop); yellow_loop.update( - &ct.delta(), + &context.delta(), vec![&epump], Vec::new(), Vec::new(), vec![&ptu], ); - green_loop.update(&ct.delta(), Vec::new(), vec![&edp1], Vec::new(), vec![&ptu]); + green_loop.update( + &context.delta(), + Vec::new(), + vec![&edp1], + Vec::new(), + vec![&ptu], + ); loop_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), yellow_loop.loop_pressure.get::(), @@ -1606,7 +1618,7 @@ mod tests { ], ); ptu_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ ptu.flow_to_left.get::(), ptu.flow_to_right.get::(), @@ -1617,7 +1629,7 @@ mod tests { ); accu_green_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ green_loop.loop_pressure.get::(), green_loop.accumulator_gas_pressure.get::(), @@ -1626,7 +1638,7 @@ mod tests { ], ); accu_yellow_history.update( - ct.delta().as_secs_f64(), + context.delta().as_secs_f64(), vec![ yellow_loop.loop_pressure.get::(), yellow_loop.accumulator_gas_pressure.get::(), @@ -1817,7 +1829,6 @@ mod tests { fn engine_d_pump_charac() { let mut output_caracteristics: Vec = Vec::new(); let mut edpump = EngineDrivenPump::new("GREEN"); - //let context = context(Duration::from_secs_f64(0.0001) ); //Small dt to freeze spool up effect let mut green_loop = hydraulic_loop("GREEN"); let mut engine1 = engine(Ratio::new::(0.0)); From 08d90c97a47f847e95b1eb6fce1f34b0f96174f8 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 10:11:51 +0200 Subject: [PATCH 048/122] fast rate update subfunction --- src/systems/a320_systems/src/hydraulic.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 2495af14943..1d209d43856 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -193,7 +193,7 @@ impl A320Hydraulic { * min_hyd_loop_timestep.as_secs_f64(), ); // Keep track of time left after all fixed loop are done - // UPDATING HYDRAULICS AT FIXED STEP + // This is main update loop at base fixed step for _ in 0..num_of_update_loops { self.update_fixed_step( context, @@ -204,19 +204,24 @@ impl A320Hydraulic { ); } - // UPDATING ACTUATOR PHYSICS AT "FIXED STEP / ACTUATORS_SIM_TIME_STEP_MULT" - // Here put everything that needs higher simulation rates + // 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 * A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; let delta_time_physics = min_hyd_loop_timestep / A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X - for _cur_loop in 0..num_of_actuators_update_loops { - self.rat - .update_physics(&delta_time_physics, &context.indicated_airspeed()); + for _ in 0..num_of_actuators_update_loops { + self.update_fast_rate(&context, &delta_time_physics); } } } + // All the higher frequency updates like physics + fn update_fast_rate(&mut self, context: &UpdateContext, delta_time_physics: &Duration) { + self.rat + .update_physics(&delta_time_physics, &context.indicated_airspeed()); + } + // All the core hydraulics updates that needs to be done at the slowest fixed step rate fn update_fixed_step( &mut self, From 5765c65a8288e43122268c78b88ce602937a0caa Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 12:25:41 +0200 Subject: [PATCH 049/122] Split engine fire overhead --- src/systems/a320_systems/src/hydraulic.rs | 64 ++++++++++++++++++----- src/systems/a320_systems/src/lib.rs | 6 ++- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 1d209d43856..8126a6e42dc 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -158,6 +158,7 @@ impl A320Hydraulic { engine1: &Engine, engine2: &Engine, overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, ) { let min_hyd_loop_timestep = Duration::from_millis(A320Hydraulic::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz @@ -200,6 +201,7 @@ impl A320Hydraulic { engine1, engine2, overhead_panel, + engine_fire_overhead, &min_hyd_loop_timestep, ); } @@ -229,10 +231,16 @@ impl A320Hydraulic { engine1: &Engine, engine2: &Engine, overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, min_hyd_loop_timestep: &Duration, ) { // Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly - self.update_logic(&min_hyd_loop_timestep, overhead_panel, context); + self.update_logic( + &min_hyd_loop_timestep, + overhead_panel, + engine_fire_overhead, + context, + ); // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic @@ -287,6 +295,7 @@ impl A320Hydraulic { &mut self, delta_time_update: &Duration, overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, context: &UpdateContext, ) { self.update_external_cond(&delta_time_update); @@ -297,7 +306,7 @@ impl A320Hydraulic { self.update_rat_deploy(context); - self.update_ed_pump_states(overhead_panel); + self.update_ed_pump_states(overhead_panel, engine_fire_overhead); self.update_e_pump_states(overhead_panel); @@ -373,10 +382,14 @@ impl A320Hydraulic { } } - fn update_ed_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + fn update_ed_pump_states( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + ) { if overhead_panel.edp1_push_button.is_auto() && self.hyd_logic_inputs.eng_1_master_on - && !overhead_panel.eng1_fire_pb.is_released() + && !engine_fire_overhead.eng1_fire_pb.is_released() { self.engine_driven_pump_1.start(); } else if overhead_panel.edp1_push_button.is_off() { @@ -384,7 +397,7 @@ impl A320Hydraulic { } //FIRE valves logic for EDP1 - if overhead_panel.eng1_fire_pb.is_released() { + if engine_fire_overhead.eng1_fire_pb.is_released() { self.engine_driven_pump_1.stop(); self.green_loop.set_fire_shutoff_valve_state(false); } else { @@ -393,7 +406,7 @@ impl A320Hydraulic { if overhead_panel.edp2_push_button.is_auto() && self.hyd_logic_inputs.eng_2_master_on - && !overhead_panel.eng2_fire_pb.is_released() + && !engine_fire_overhead.eng2_fire_pb.is_released() { self.engine_driven_pump_2.start(); } else if overhead_panel.edp2_push_button.is_off() { @@ -401,7 +414,7 @@ impl A320Hydraulic { } //FIRE valves logic for EDP2 - if overhead_panel.eng2_fire_pb.is_released() { + if engine_fire_overhead.eng2_fire_pb.is_released() { self.engine_driven_pump_2.stop(); self.yellow_loop.set_fire_shutoff_valve_state(false); } else { @@ -742,8 +755,6 @@ pub struct A320HydraulicOverheadPanel { pub rat_push_button: AutoOffFaultPushButton, pub yellow_epump_push_button: AutoOnFaultPushButton, pub blue_epump_override_push_button: OnOffFaultPushButton, - pub eng1_fire_pb: FirePushButton, - pub eng2_fire_pb: FirePushButton, } impl A320HydraulicOverheadPanel { @@ -756,8 +767,6 @@ impl A320HydraulicOverheadPanel { 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"), - eng1_fire_pb: FirePushButton::new("ENG1"), - eng2_fire_pb: FirePushButton::new("ENG2"), } } @@ -782,6 +791,27 @@ impl SimulationElement for A320HydraulicOverheadPanel { 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 struct A320EngineFireOverheadPanel { + pub eng1_fire_pb: FirePushButton, + pub eng2_fire_pb: FirePushButton, +} + +impl A320EngineFireOverheadPanel { + pub fn new() -> A320EngineFireOverheadPanel { + A320EngineFireOverheadPanel { + eng1_fire_pb: FirePushButton::new("ENG1"), + eng2_fire_pb: FirePushButton::new("ENG2"), + } + } +} + +impl SimulationElement for A320EngineFireOverheadPanel { + fn accept(&mut self, visitor: &mut T) { self.eng1_fire_pb.accept(visitor); self.eng2_fire_pb.accept(visitor); @@ -826,6 +856,7 @@ mod tests { engine_2: Engine, hydraulics: A320Hydraulic, overhead: A320HydraulicOverheadPanel, + engine_fire_overhead: A320EngineFireOverheadPanel, } impl A320HydraulicsTestAircraft { fn new() -> Self { @@ -834,6 +865,7 @@ mod tests { engine_2: Engine::new(2), hydraulics: A320Hydraulic::new(), overhead: A320HydraulicOverheadPanel::new(), + engine_fire_overhead: A320EngineFireOverheadPanel::new(), } } @@ -871,8 +903,13 @@ mod tests { 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.hydraulics.update( + context, + &self.engine_1, + &self.engine_2, + &self.overhead, + &self.engine_fire_overhead, + ); self.overhead.update_pb_faults(&self.hydraulics); } @@ -881,6 +918,7 @@ mod tests { fn accept(&mut self, visitor: &mut T) { self.hydraulics.accept(visitor); self.overhead.accept(visitor); + self.engine_fire_overhead.accept(visitor); visitor.visit(self); } diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 6e89a09908b..6b7ff20cb67 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -6,7 +6,7 @@ mod power_consumption; use self::{fuel::A320Fuel, pneumatic::A320PneumaticOverheadPanel}; use electrical::{A320Electrical, A320ElectricalOverheadPanel, A320ElectricalUpdateArguments}; -use hydraulic::{A320Hydraulic, A320HydraulicOverheadPanel}; +use hydraulic::{A320EngineFireOverheadPanel, A320Hydraulic, A320HydraulicOverheadPanel}; use power_consumption::A320PowerConsumption; use systems::{ apu::{ @@ -28,6 +28,7 @@ pub struct A320 { fuel: A320Fuel, engine_1: Engine, engine_2: Engine, + engine_fire_overhead: A320EngineFireOverheadPanel, electrical: A320Electrical, power_consumption: A320PowerConsumption, ext_pwr: ExternalPowerSource, @@ -46,6 +47,7 @@ impl A320 { fuel: A320Fuel::new(), engine_1: Engine::new(1), engine_2: Engine::new(2), + engine_fire_overhead: A320EngineFireOverheadPanel::new(), electrical: A320Electrical::new(), power_consumption: A320PowerConsumption::new(), ext_pwr: ExternalPowerSource::new(), @@ -107,6 +109,7 @@ impl Aircraft for A320 { &self.engine_1, &self.engine_2, &self.hydraulic_overhead, + &self.engine_fire_overhead, ); self.hydraulic_overhead.update_pb_faults(&self.hydraulic); @@ -128,6 +131,7 @@ 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); From 490e8480b5f88437f99c62aeaa40f8eb97d1e4b4 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 12:40:50 +0200 Subject: [PATCH 050/122] refactor PB faults update function --- src/systems/a320_systems/src/hydraulic.rs | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 8126a6e42dc..7bbf7300b3a 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -107,6 +107,22 @@ impl A320Hydraulic { } } + pub fn green_edp_has_fault(&self) -> bool { + self.engine_driven_pump_1.is_active() && !self.is_green_pressurised() + } + + pub fn yellow_epump_has_fault(&self) -> bool { + self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() + } + + pub fn yellow_edp_has_fault(&self) -> bool { + self.engine_driven_pump_2.is_active() && !self.is_yellow_pressurised() + } + + pub fn blue_epump_has_fault(&self) -> bool { + self.blue_electric_pump.is_active() && !self.is_blue_pressurised() + } + // Updates pressure available state based on pressure switches fn update_hyd_avail_states(&mut self) { if self.green_loop.get_pressure() @@ -771,14 +787,12 @@ impl A320HydraulicOverheadPanel { } pub fn update_pb_faults(&mut self, hyd: &A320Hydraulic) { - self.edp1_push_button - .set_fault(hyd.hyd_logic_inputs.green_edp_has_fault); - self.edp2_push_button - .set_fault(hyd.hyd_logic_inputs.yellow_edp_has_fault); + self.edp1_push_button.set_fault(hyd.green_edp_has_fault()); + self.edp2_push_button.set_fault(hyd.yellow_edp_has_fault()); self.blue_epump_push_button - .set_fault(hyd.hyd_logic_inputs.blue_epump_has_fault); + .set_fault(hyd.blue_epump_has_fault()); self.yellow_epump_push_button - .set_fault(hyd.hyd_logic_inputs.yellow_epump_has_fault); + .set_fault(hyd.yellow_epump_has_fault()); } } From 1872e550685f2182591d213420791714ae613780 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 12:55:57 +0200 Subject: [PATCH 051/122] Renamed braking vars --- src/systems/a320_systems/src/hydraulic.rs | 92 +++++++++++------------ 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 7bbf7300b3a..5d36e3b90aa 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -488,12 +488,12 @@ impl SimulationElement for A320Hydraulic { pub struct A320HydraulicBrakingLogic { parking_brake_demand: bool, weight_on_wheels: bool, - left_brake_command: f64, - right_brake_command: f64, - left_brake_green_command: f64, //Actual command sent to left green circuit - left_brake_yellow_command: f64, //Actual command sent to left yellow circuit - right_brake_green_command: f64, //Actual command sent to right green circuit - right_brake_yellow_command: f64, //Actual command sent to right yellow circuit + 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, anti_skid_activated: bool, autobrakes_setting: u8, } @@ -508,12 +508,12 @@ impl A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { parking_brake_demand: true, //Position of parking brake lever weight_on_wheels: true, - left_brake_command: 1.0, //Command read from pedals - right_brake_command: 1.0, //Command read from pedals - left_brake_green_command: 0.0, //Actual command sent to left green circuit - left_brake_yellow_command: 1.0, //Actual command sent to left yellow circuit - right_brake_green_command: 0.0, //Actual command sent to right green circuit - right_brake_yellow_command: 1.0, //Actual command sent to right yellow circuit + left_brake_pilot_input: 1.0, // read from brake pedals + right_brake_pilot_input: 1.0, // read from brake pedals + left_brake_green_output: 0.0, //Actual command sent to left green circuit + left_brake_yellow_output: 1.0, //Actual command sent to left yellow circuit. Init 1 as considering park brake on on init + right_brake_green_output: 0.0, //Actual command sent to right green circuit + right_brake_yellow_output: 1.0, //Actual command sent to right yellow circuit. Init 1 as considering park brake on on init anti_skid_activated: true, autobrakes_setting: 0, } @@ -531,59 +531,59 @@ impl A320HydraulicBrakingLogic { A320HydraulicBrakingLogic::PARK_BRAKE_DEMAND_DYNAMIC * delta_time_update.as_secs_f64(); if green_used_for_brakes { - self.left_brake_green_command = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.left_brake_command + self.left_brake_green_output = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND + * self.left_brake_pilot_input + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.left_brake_green_command; - self.right_brake_green_command = - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND * self.right_brake_command - + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.right_brake_green_command; - - self.left_brake_yellow_command -= dynamic_increment; - self.right_brake_yellow_command -= dynamic_increment; + * self.left_brake_green_output; + self.right_brake_green_output = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND + * self.right_brake_pilot_input + + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) + * self.right_brake_green_output; + + self.left_brake_yellow_output -= dynamic_increment; + self.right_brake_yellow_output -= dynamic_increment; } else { if !self.parking_brake_demand { //Normal braking but using alternate circuit - self.left_brake_yellow_command = + self.left_brake_yellow_output = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.left_brake_command + * self.left_brake_pilot_input + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.left_brake_yellow_command; - self.right_brake_yellow_command = + * self.left_brake_yellow_output; + self.right_brake_yellow_output = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.right_brake_command + * self.right_brake_pilot_input + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.right_brake_yellow_command; + * self.right_brake_yellow_output; if !self.anti_skid_activated { - self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.5); //0.5 is temporary implementation of pressure limitation around 1000psi - self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.5); + self.left_brake_yellow_output = self.left_brake_yellow_output.min(0.5); //0.5 is temporary implementation of pressure limitation around 1000psi + self.right_brake_yellow_output = self.right_brake_yellow_output.min(0.5); //0.5 is temporary implementation of pressure limitation around 1000psi } } else { //Else we just use parking brake - self.left_brake_yellow_command += dynamic_increment; - self.left_brake_yellow_command = self.left_brake_yellow_command.min(0.7); //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes - self.right_brake_yellow_command += dynamic_increment; - self.right_brake_yellow_command = self.right_brake_yellow_command.min(0.7); + self.left_brake_yellow_output += dynamic_increment; + self.left_brake_yellow_output = self.left_brake_yellow_output.min(0.7); //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes + self.right_brake_yellow_output += dynamic_increment; + self.right_brake_yellow_output = self.right_brake_yellow_output.min(0.7); //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes } - self.left_brake_green_command -= dynamic_increment; - self.right_brake_green_command -= dynamic_increment; + self.left_brake_green_output -= dynamic_increment; + self.right_brake_green_output -= dynamic_increment; } //limiting final values - self.left_brake_yellow_command = self.left_brake_yellow_command.min(1.).max(0.); - self.right_brake_yellow_command = self.right_brake_yellow_command.min(1.).max(0.); - self.left_brake_green_command = self.left_brake_green_command.min(1.).max(0.); - self.right_brake_green_command = self.right_brake_green_command.min(1.).max(0.); + 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.); } pub fn send_brake_demands(&mut self, norm: &mut BrakeCircuit, altn: &mut BrakeCircuit) { - norm.set_brake_demand_left(self.left_brake_green_command); - norm.set_brake_demand_right(self.right_brake_green_command); - altn.set_brake_demand_left(self.left_brake_yellow_command); - altn.set_brake_demand_right(self.right_brake_yellow_command); + 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); } } @@ -596,8 +596,8 @@ impl SimulationElement for A320HydraulicBrakingLogic { self.parking_brake_demand = state.read_bool("BRAKE PARKING INDICATOR"); self.weight_on_wheels = state.read_bool("SIM ON GROUND"); self.anti_skid_activated = state.read_bool("ANTISKID BRAKES ACTIVE"); - self.left_brake_command = state.read_f64("BRAKE LEFT POSITION") / 100.0; - self.right_brake_command = state.read_f64("BRAKE RIGHT POSITION") / 100.0; + 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; } } From f6b7092cbf012429233e76fde8023ab3cf39fd67 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 14:44:16 +0200 Subject: [PATCH 052/122] Moved interpolation to shared crate --- src/systems/systems/src/hydraulic/mod.rs | 98 +----------------------- src/systems/systems/src/shared/mod.rs | 92 +++++++++++++++++++++- 2 files changed, 91 insertions(+), 99 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 7120497462c..361189e32f0 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -12,35 +12,10 @@ use uom::si::{ }; use crate::engine::Engine; +use crate::shared::interpolation; use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; pub mod brakecircuit; -// //Interpolate values_map_y at point value_at_point in breakpoints break_points_x -pub 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); - // The function also assumes xs are ordered from small to large. Consider adding a debug_assert! for that as well. - - 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]) - } -} - // Trait common to all hydraulic pumps // Max gives maximum available volume at that time as if it is a variable displacement // pump it can be adjusted by pump regulation @@ -1865,77 +1840,6 @@ mod tests { } } - #[cfg(test)] - mod utility_tests { - use crate::hydraulic::interpolation; - use rand::Rng; - use std::time::Instant; - - #[test] - fn interp_test() { - let xs1 = [ - -100.0, -10.0, 10.0, 240.0, 320.0, 435.3, 678.9, 890.3, 10005.0, 203493.7, - ]; - let ys1 = [ - -200.0, 10.0, 40.0, -553.0, 238.4, 30423.3, 23000.2, 32000.4, 43200.2, 34.2, - ]; - - //Check before first element - assert!((interpolation(&xs1, &ys1, -500.0) - ys1[0]).abs() < f64::EPSILON); - - //Check after last - assert!( - (interpolation(&xs1, &ys1, 100000000.0) - *ys1.last().unwrap()).abs() - < f64::EPSILON - ); - - //Check equal first - assert!( - (interpolation(&xs1, &ys1, *xs1.first().unwrap()) - *ys1.first().unwrap()).abs() - < f64::EPSILON - ); - - //Check equal last - assert!( - (interpolation(&xs1, &ys1, *xs1.last().unwrap()) - *ys1.last().unwrap()).abs() - < f64::EPSILON - ); - - //Check interp middle - let res = interpolation(&xs1, &ys1, 358.0); - assert!((res - 10186.589).abs() < 0.001); - - //Check interp last segment - let res = interpolation(&xs1, &ys1, 22200.0); - assert!((res - 40479.579).abs() < 0.001); - - //Check interp first segment - let res = interpolation(&xs1, &ys1, -50.0); - assert!((res - (-83.3333)).abs() < 0.001); - - //Speed check - let mut rng = rand::thread_rng(); - let time_start = Instant::now(); - for _idx in 0..1000000 { - let test_val = rng.gen_range(xs1[0]..*xs1.last().unwrap()); - let mut _res = interpolation(&xs1, &ys1, test_val); - _res += 2.78; - } - let time_elapsed = time_start.elapsed(); - - println!( - "Time elapsed for 1000000 calls {} s", - time_elapsed.as_secs_f64() - ); - } - } - #[cfg(test)] - mod loop_tests {} - - #[cfg(test)] - mod epump_tests {} - - //TODO to update according to new caracteristics, spoolup times and displacement dynamic #[cfg(test)] mod edp_tests { use super::*; diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index c5381538bef..bf013de46c5 100644 --- a/src/systems/systems/src/shared/mod.rs +++ b/src/systems/systems/src/shared/mod.rs @@ -88,6 +88,32 @@ 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); + // The function also assumes xs are ordered from small to large. Consider adding a debug_assert! for that as well. + + 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 +211,72 @@ mod delayed_true_logic_gate_tests { } #[cfg(test)] -mod calculate_towards_target_temperature_tests { - use ntest::assert_about_eq; +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() { From 677b3176d4ff7835cc892e94ce5b20676e136c59 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 30 Mar 2021 16:53:12 +0200 Subject: [PATCH 053/122] Removed graphics from tests Graphics are moved to a separate application, which is disabled for now --- Cargo.lock | 115 --- Cargo.toml | 3 +- .../src/main.rs | 714 ++++++++++++++++++ src/systems/systems/Cargo.toml | 4 - src/systems/systems/src/hydraulic/mod.rs | 441 ----------- 5 files changed, 716 insertions(+), 561 deletions(-) create mode 100644 src/systems/a320_hydraulic_simulation_graphs/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 83e51a7a156..ac0ac256d5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,21 +19,6 @@ 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" @@ -69,20 +54,6 @@ 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" @@ -185,28 +156,6 @@ 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" @@ -313,12 +262,6 @@ 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" @@ -395,16 +338,6 @@ 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" @@ -504,12 +437,6 @@ 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" @@ -534,16 +461,6 @@ 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" @@ -653,24 +570,12 @@ 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" @@ -695,12 +600,6 @@ 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" @@ -712,18 +611,6 @@ 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" @@ -732,9 +619,7 @@ dependencies = [ "ntest", "num-derive", "num-traits", - "plotlib", "rand", - "rustplotlib", "uom", ] diff --git a/Cargo.toml b/Cargo.toml index ae97ebaaaae..2321dc9f857 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/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..5ec64194216 --- /dev/null +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -0,0 +1,714 @@ +use systems::simulation::UpdateContext; +use systems::engine::Engine; +pub use systems::hydraulic::*; + +use std::time::Duration; +use uom::si::{ + ratio::percent, + acceleration::foot_per_second_squared, + f64::*, + length::foot, + pressure::{pascal, psi}, + thermodynamic_temperature::degree_celsius, + time::second, + volume::{gallon, liter}, + volume_rate::gallon_per_second, + velocity::knot, +}; + +use plotlib::page::Page; +use plotlib::repr::Plot; +use plotlib::style::LineStyle; +use plotlib::view::ContinuousView; + +extern crate rustplotlib; +use rustplotlib::Figure; + + + +fn main() { + println!("Launching hyd simulation..."); + + green_loop_edp_simulation(); +} + +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 +pub struct History { + time_vector: Vec, //Simulation time starting from 0 + name_vector: Vec, //Name of each var saved + data_vector: Vec>, //Vector data for each var saved + _data_size: usize, +} + +impl History { + pub 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 + pub 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 + pub fn update(&mut self, delta_time: f64, values: Vec) { + self.time_vector + .push(self.time_vector.last().unwrap() + delta_time); + self.push_data(values); + } + + pub 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 + pub 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 + pub fn show_matplotlib(&self, figure_title: &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 _result = mpl.savefig(figure_title); + + mpl.wait().unwrap(); + } +} + +//Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s +fn green_loop_edp_simulation() { + 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(), "n2 ratio".to_string()]; + let mut edp1_history = History::new(edp1_var_names); + + let mut edp1 = engine_driven_pump(); + let mut green_loop = hydraulic_loop("GREEN"); + edp1.active = true; + + let init_n2 = Ratio::new::(55.0); + let engine1 = engine(init_n2); + 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.loop_pressure.get::(), + green_loop.loop_volume.get::(), + green_loop.reservoir_volume.get::(), + green_loop.current_flow.get::(), + ], + ); + edp1_history.init( + 0.0, + vec![ + edp1.get_delta_vol_max().get::(), + engine1.corrected_n2.get::() as f64, + ], + ); + accu_green_history.init( + 0.0, + vec![ + green_loop.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.loop_pressure >= Pressure::new::(2850.0)); + } + if x == 200 { + assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); + edp1.stop(); + } + if x >= 500 { + //Shutdown + 30s + assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); + } + + edp1.update(&context.delta(), &green_loop, &engine1); + green_loop.update( + &context.delta(), + Vec::new(), + vec![&edp1], + Vec::new(), + Vec::new(), + ); + if x % 20 == 0 { + println!("Iteration {}", x); + println!("-------------------------------------------"); + println!("---PSI: {}", green_loop.loop_pressure.get::()); + println!( + "--------Reservoir Volume (g): {}", + green_loop.reservoir_volume.get::() + ); + println!( + "--------Loop Volume (g): {}", + green_loop.loop_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.loop_pressure.get::(), + green_loop.loop_volume.get::(), + green_loop.reservoir_volume.get::(), + green_loop.current_flow.get::(), + ], + ); + edp1_history.update( + context.delta().as_secs_f64(), + vec![ + edp1.get_delta_vol_max().get::(), + engine1.corrected_n2.get::() as f64, + ], + ); + accu_green_history.update( + context.delta().as_secs_f64(), + vec![ + green_loop.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"); + edp1_history.show_matplotlib("green_loop_edp_simulation_EDP1 data"); + accu_green_history.show_matplotlib("green_loop_edp_simulation_Green Accum data"); +} + +fn yellow_green_ptu_loop_simulation() { + 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![ + "GREEN side flow".to_string(), + "YELLOW side 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(); + epump.stop(); + let mut yellow_loop = hydraulic_loop("YELLOW"); + + let mut edp1 = engine_driven_pump(); + assert!(!edp1.active); //Is off when created? + + let mut engine1 = engine(Ratio::new::(0.0)); + + let mut green_loop = hydraulic_loop("GREEN"); + + let mut ptu = Ptu::new(""); + + let context = context(Duration::from_millis(100)); + + loop_history.init( + 0.0, + vec![ + green_loop.loop_pressure.get::(), + yellow_loop.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_to_left.get::(), + ptu.flow_to_right.get::(), + green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), + ptu.is_active_left as i8 as f64, + ptu.is_active_right as i8 as f64, + ], + ); + accu_green_history.init( + 0.0, + vec![ + green_loop.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.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; + + engine1.corrected_n2 = Ratio::new::(100.0); + for x in 0..800 { + if x == 10 { + //After 1s powering electric pump + println!("------------YELLOW EPUMP ON------------"); + assert!(yellow_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(yellow_loop.reservoir_volume == yellow_res_at_start); + + assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume == green_res_at_start); + + epump.start(); + } + + if x == 110 { + //10s later enabling ptu + println!("--------------PTU ENABLED--------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2950.0)); + assert!(yellow_loop.reservoir_volume <= yellow_res_at_start); + + assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume == green_res_at_start); + + ptu.enabling(true); + } + + if x == 300 { + //@30s, ptu should be supplying green loop + println!("----------PTU SUPPLIES GREEN------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2400.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2400.0)); + } + + if x == 400 { + //@40s enabling edp + println!("------------GREEN EDP1 ON------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); + edp1.start(); + } + + if (500..=600).contains(&x) { + //10s later and during 10s, ptu should stay inactive + println!("------------IS PTU ACTIVE??------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(!ptu.is_active_left && !ptu.is_active_right); + } + + if x == 600 { + //@60s diabling edp and epump + println!("-------------ALL PUMPS OFF------------"); + assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); + assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); + edp1.stop(); + epump.stop(); + } + + if x == 800 { + //@80s diabling edp and epump + println!("-----------IS PRESSURE OFF?-----------"); + assert!(yellow_loop.loop_pressure < Pressure::new::(50.0)); + assert!(green_loop.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); + edp1.update(&context.delta(), &green_loop, &engine1); + epump.update(&context.delta(), &yellow_loop); + + yellow_loop.update( + &context.delta(), + vec![&epump], + Vec::new(), + Vec::new(), + vec![&ptu], + ); + green_loop.update( + &context.delta(), + Vec::new(), + vec![&edp1], + Vec::new(), + vec![&ptu], + ); + + loop_history.update( + context.delta().as_secs_f64(), + vec![ + green_loop.loop_pressure.get::(), + yellow_loop.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_to_left.get::(), + ptu.flow_to_right.get::(), + green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), + ptu.is_active_left as i8 as f64, + ptu.is_active_right as i8 as f64, + ], + ); + + accu_green_history.update( + context.delta().as_secs_f64(), + vec![ + green_loop.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.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.loop_pressure.get::()); + println!("---RPM YELLOW: {}", epump.rpm); + println!( + "---Priming State: {}/{}", + yellow_loop.loop_volume.get::(), + yellow_loop.max_loop_volume.get::() + ); + println!("---PSI GREEN: {}", green_loop.loop_pressure.get::()); + println!("---N2 GREEN: {}", engine1.corrected_n2.get::()); + println!( + "---Priming State: {}/{}", + green_loop.loop_volume.get::(), + green_loop.max_loop_volume.get::() + ); + } + } + + loop_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Loop_press"); + ptu_history.show_matplotlib("yellow_green_ptu_loop_simulation()_PTU"); + + accu_green_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Green_acc"); + accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); +} + +fn hydraulic_loop(loop_color: &str) -> HydLoop { + match loop_color { + "GREEN" => HydLoop::new( + loop_color, + true, + false, + Volume::new::(26.41), + Volume::new::(26.41), + Volume::new::(10.0), + Volume::new::(3.83), + HydFluid::new(Pressure::new::(1450000000.0)), + true, + ), + "YELLOW" => HydLoop::new( + loop_color, + false, + true, + Volume::new::(10.2), + Volume::new::(10.2), + Volume::new::(8.0), + Volume::new::(3.3), + HydFluid::new(Pressure::new::(1450000000.0)), + true, + ), + _ => HydLoop::new( + loop_color, + false, + false, + Volume::new::(15.85), + Volume::new::(15.85), + Volume::new::(8.0), + Volume::new::(1.5), + HydFluid::new(Pressure::new::(1450000000.0)), + false, + ), + } +} + +fn electric_pump() -> ElectricPump { + ElectricPump::new("DEFAULT") +} + +fn engine_driven_pump() -> EngineDrivenPump { + EngineDrivenPump::new("DEFAULT") +} + +fn engine(n2: Ratio) -> Engine { + let mut engine = Engine::new(1); + engine.corrected_n2 = n2; + + engine +} + +fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + ) +} + +struct PressureCaracteristic { + pressure: Pressure, + rpm_tab: Vec, + flow_tab: Vec, +} + + +fn show_carac(figure_title: &str, output_caracteristics: &[PressureCaracteristic]) { + use rustplotlib::{Axes2D, Line2D}; + + let mut all_axis: Vec> = Vec::new(); + let colors = ["blue", "yellow", "red", "black", "cyan", "magenta", "green"]; + let linestyles = ["--", "-.", "-"]; + let mut curr_axis = Axes2D::new(); + curr_axis = curr_axis.grid(true); + let mut color_idx = 0; + let mut style_idx = 0; + for cur_pressure in output_caracteristics { + let press_str = format!("P={:.0}", cur_pressure.pressure.get::()); + curr_axis = curr_axis + .add( + Line2D::new(press_str.as_str()) + .data(&cur_pressure.rpm_tab, &cur_pressure.flow_tab) + .color(colors[color_idx]) + //.marker("x") + .linestyle(linestyles[style_idx]) + .linewidth(1.0), + ) + .xlabel("RPM") + .ylabel("Max Flow") + .legend("best") + .xlim(0.0, *cur_pressure.rpm_tab.last().unwrap()); + //.ylim(-2.0, 2.0); + color_idx = (color_idx + 1) % colors.len(); + style_idx = (style_idx + 1) % linestyles.len(); + } + all_axis.push(Some(curr_axis)); + let fig = Figure::new().subplots(all_axis.len() as u32, 1, all_axis); + + use rustplotlib::backend::Matplotlib; + use rustplotlib::Backend; + let mut mpl = Matplotlib::new().unwrap(); + mpl.set_style("ggplot").unwrap(); + + fig.apply(&mut mpl).unwrap(); + + let _result = mpl.savefig(figure_title); + + mpl.wait().unwrap(); +} + +fn epump_charac() { + let mut output_caracteristics: Vec = Vec::new(); + let mut epump = ElectricPump::new("YELLOW"); + let context = context(Duration::from_secs_f64(0.0001)); //Small dt to freeze spool up effect + + let mut green_loop = hydraulic_loop("GREEN"); + + epump.start(); + for pressure in (0..3500).step_by(500) { + let mut rpm_tab: Vec = Vec::new(); + let mut flow_tab: Vec = Vec::new(); + for rpm in (0..10000).step_by(150) { + green_loop.loop_pressure = Pressure::new::(pressure as f64); + epump.rpm = rpm as f64; + epump.update(&context.delta(), &green_loop); + rpm_tab.push(rpm as f64); + let flow = epump.get_delta_vol_max() + / Time::new::(context.delta().as_secs_f64()); + let flow_gal = flow.get::() as f64; + flow_tab.push(flow_gal); + } + output_caracteristics.push(PressureCaracteristic { + pressure: green_loop.loop_pressure, + rpm_tab, + flow_tab, + }); + } + show_carac("Epump_carac", &output_caracteristics); +} + +fn engine_d_pump_charac() { + let mut output_caracteristics: Vec = Vec::new(); + let mut edpump = EngineDrivenPump::new("GREEN"); + + let mut green_loop = hydraulic_loop("GREEN"); + let mut engine1 = engine(Ratio::new::(0.0)); + + edpump.start(); + let context = context(Duration::from_secs_f64(1.0)); //Small dt to freeze spool up effect + + edpump.update(&context.delta(), &green_loop, &engine1); + for pressure in (0..3500).step_by(500) { + let mut rpm_tab: Vec = Vec::new(); + let mut flow_tab: Vec = Vec::new(); + for rpm in (0..10000).step_by(150) { + green_loop.loop_pressure = Pressure::new::(pressure as f64); + + engine1.corrected_n2 = Ratio::new::( + (rpm as f64) + / (EngineDrivenPump::PUMP_N2_GEAR_RATIO + * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), + ); + edpump.update(&context.delta(), &green_loop, &engine1); + rpm_tab.push(rpm as f64); + let flow = edpump.get_delta_vol_max() + / Time::new::(context.delta().as_secs_f64()); + let flow_gal = flow.get::() as f64; + flow_tab.push(flow_gal); + } + output_caracteristics.push(PressureCaracteristic { + pressure: green_loop.loop_pressure, + rpm_tab, + flow_tab, + }); + } + show_carac("Eng_Driv_pump_carac", &output_caracteristics); +} diff --git a/src/systems/systems/Cargo.toml b/src/systems/systems/Cargo.toml index aed94530182..9cc75ab5bfb 100644 --- a/src/systems/systems/Cargo.toml +++ b/src/systems/systems/Cargo.toml @@ -11,7 +11,3 @@ ntest = "0.7.2" num-derive = "0.3.3" num-traits = "0.2.14" itertools = "0.10.0" - -[dev-dependencies] -plotlib = "0.5.1" -rustplotlib = "0.0.4" diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 361189e32f0..7fe00599051 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -963,141 +963,12 @@ mod tests { thermodynamic_temperature::degree_celsius, time::second, 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; - - 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 - pub struct History { - time_vector: Vec, //Simulation time starting from 0 - name_vector: Vec, //Name of each var saved - data_vector: Vec>, //Vector data for each var saved - _data_size: usize, - } - - impl History { - pub 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 - pub 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 - pub fn update(&mut self, delta_time: f64, values: Vec) { - self.time_vector - .push(self.time_vector.last().unwrap() + delta_time); - self.push_data(values); - } - - pub 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 - pub 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 - pub fn show_matplotlib(&self, figure_title: &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 _result = mpl.savefig(figure_title); - - mpl.wait().unwrap(); - } - } - use super::*; #[test] //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn green_loop_edp_simulation() { - 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(), "n2 ratio".to_string()]; - let mut edp1_history = History::new(edp1_var_names); - let mut edp1 = engine_driven_pump(); let mut green_loop = hydraulic_loop("GREEN"); edp1.active = true; @@ -1106,39 +977,6 @@ mod tests { let engine1 = engine(init_n2); 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.loop_pressure.get::(), - green_loop.loop_volume.get::(), - green_loop.reservoir_volume.get::(), - green_loop.current_flow.get::(), - ], - ); - edp1_history.init( - 0.0, - vec![ - edp1.get_delta_vol_max().get::(), - engine1.corrected_n2.get::() as f64, - ], - ); - accu_green_history.init( - 0.0, - vec![ - green_loop.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 @@ -1186,37 +1024,7 @@ mod tests { green_loop.accumulator_gas_pressure.get::() ); } - - green_loop_history.update( - context.delta().as_secs_f64(), - vec![ - green_loop.loop_pressure.get::(), - green_loop.loop_volume.get::(), - green_loop.reservoir_volume.get::(), - green_loop.current_flow.get::(), - ], - ); - edp1_history.update( - context.delta().as_secs_f64(), - vec![ - edp1.get_delta_vol_max().get::(), - engine1.corrected_n2.get::() as f64, - ], - ); - accu_green_history.update( - context.delta().as_secs_f64(), - vec![ - green_loop.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"); - edp1_history.show_matplotlib("green_loop_edp_simulation_EDP1 data"); - accu_green_history.show_matplotlib("green_loop_edp_simulation_Green Accum data"); } #[test] @@ -1395,41 +1203,6 @@ mod tests { //shut green edp off, check drop of pressure and ptu effect //shut yellow epump, check drop of pressure in both loops fn yellow_green_ptu_loop_simulation() { - 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![ - "GREEN side flow".to_string(), - "YELLOW side 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(); epump.stop(); let mut yellow_loop = hydraulic_loop("YELLOW"); @@ -1445,46 +1218,6 @@ mod tests { let context = context(Duration::from_millis(100)); - loop_history.init( - 0.0, - vec![ - green_loop.loop_pressure.get::(), - yellow_loop.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_to_left.get::(), - ptu.flow_to_right.get::(), - green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), - ptu.is_active_left as i8 as f64, - ptu.is_active_right as i8 as f64, - ], - ); - accu_green_history.init( - 0.0, - vec![ - green_loop.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.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; @@ -1581,47 +1314,6 @@ mod tests { vec![&ptu], ); - loop_history.update( - context.delta().as_secs_f64(), - vec![ - green_loop.loop_pressure.get::(), - yellow_loop.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_to_left.get::(), - ptu.flow_to_right.get::(), - green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), - ptu.is_active_left as i8 as f64, - ptu.is_active_right as i8 as f64, - ], - ); - - accu_green_history.update( - context.delta().as_secs_f64(), - vec![ - green_loop.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.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!("-------------------------------------------"); @@ -1641,12 +1333,6 @@ mod tests { ); } } - - loop_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Loop_press"); - ptu_history.show_matplotlib("yellow_green_ptu_loop_simulation()_PTU"); - - accu_green_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Green_acc"); - accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); } fn hydraulic_loop(loop_color: &str) -> HydLoop { @@ -1713,133 +1399,6 @@ mod tests { ) } - #[cfg(test)] - - struct PressureCaracteristic { - pressure: Pressure, - rpm_tab: Vec, - flow_tab: Vec, - } - - mod characteristics_tests { - use super::*; - - fn show_carac(figure_title: &str, output_caracteristics: &[PressureCaracteristic]) { - use rustplotlib::{Axes2D, Line2D}; - - let mut all_axis: Vec> = Vec::new(); - let colors = ["blue", "yellow", "red", "black", "cyan", "magenta", "green"]; - let linestyles = ["--", "-.", "-"]; - let mut curr_axis = Axes2D::new(); - curr_axis = curr_axis.grid(true); - let mut color_idx = 0; - let mut style_idx = 0; - for cur_pressure in output_caracteristics { - let press_str = format!("P={:.0}", cur_pressure.pressure.get::()); - curr_axis = curr_axis - .add( - Line2D::new(press_str.as_str()) - .data(&cur_pressure.rpm_tab, &cur_pressure.flow_tab) - .color(colors[color_idx]) - //.marker("x") - .linestyle(linestyles[style_idx]) - .linewidth(1.0), - ) - .xlabel("RPM") - .ylabel("Max Flow") - .legend("best") - .xlim(0.0, *cur_pressure.rpm_tab.last().unwrap()); - //.ylim(-2.0, 2.0); - color_idx = (color_idx + 1) % colors.len(); - style_idx = (style_idx + 1) % linestyles.len(); - } - all_axis.push(Some(curr_axis)); - let fig = Figure::new().subplots(all_axis.len() as u32, 1, all_axis); - - use rustplotlib::backend::Matplotlib; - use rustplotlib::Backend; - let mut mpl = Matplotlib::new().unwrap(); - mpl.set_style("ggplot").unwrap(); - - fig.apply(&mut mpl).unwrap(); - - let _result = mpl.savefig(figure_title); - - mpl.wait().unwrap(); - } - - #[test] - fn epump_charac() { - let mut output_caracteristics: Vec = Vec::new(); - let mut epump = ElectricPump::new("YELLOW"); - let context = context(Duration::from_secs_f64(0.0001)); //Small dt to freeze spool up effect - - let mut green_loop = hydraulic_loop("GREEN"); - - epump.start(); - for pressure in (0..3500).step_by(500) { - let mut rpm_tab: Vec = Vec::new(); - let mut flow_tab: Vec = Vec::new(); - for rpm in (0..10000).step_by(150) { - green_loop.loop_pressure = Pressure::new::(pressure as f64); - epump.rpm = rpm as f64; - epump.update(&context.delta(), &green_loop); - rpm_tab.push(rpm as f64); - let flow = epump.get_delta_vol_max() - / Time::new::(context.delta().as_secs_f64()); - let flow_gal = flow.get::() as f64; - flow_tab.push(flow_gal); - } - output_caracteristics.push(PressureCaracteristic { - pressure: green_loop.loop_pressure, - rpm_tab, - flow_tab, - }); - } - show_carac("Epump_carac", &output_caracteristics); - } - - #[test] - //TODO broken until rpm relation repaired - fn engine_d_pump_charac() { - let mut output_caracteristics: Vec = Vec::new(); - let mut edpump = EngineDrivenPump::new("GREEN"); - - let mut green_loop = hydraulic_loop("GREEN"); - let mut engine1 = engine(Ratio::new::(0.0)); - - edpump.start(); - let context = context(Duration::from_secs_f64(1.0)); //Small dt to freeze spool up effect - - edpump.update(&context.delta(), &green_loop, &engine1); - for pressure in (0..3500).step_by(500) { - let mut rpm_tab: Vec = Vec::new(); - let mut flow_tab: Vec = Vec::new(); - for rpm in (0..10000).step_by(150) { - green_loop.loop_pressure = Pressure::new::(pressure as f64); - - engine1.corrected_n2 = Ratio::new::( - (rpm as f64) - / (EngineDrivenPump::PUMP_N2_GEAR_RATIO - * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), - ); - edpump.update(&context.delta(), &green_loop, &engine1); - rpm_tab.push(rpm as f64); - let flow = edpump.get_delta_vol_max() - / Time::new::(context.delta().as_secs_f64()); - let flow_gal = flow.get::() as f64; - flow_tab.push(flow_gal); - } - output_caracteristics.push(PressureCaracteristic { - pressure: green_loop.loop_pressure, - rpm_tab, - flow_tab, - }); - } - show_carac("Eng_Driv_pump_carac", &output_caracteristics); - } - } - #[cfg(test)] mod edp_tests { use super::*; From 398b829bad8bccd865b55ceebac850abbacdc0ae Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 31 Mar 2021 08:52:01 +0200 Subject: [PATCH 054/122] add some getters in core objects --- src/systems/systems/src/hydraulic/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 7fe00599051..04fabec9186 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -295,6 +295,10 @@ impl HydLoop { self.reservoir_volume } + pub fn get_loop_fluid_volume(&self) -> Volume { + self.loop_volume + } + pub fn get_usable_reservoir_fluid(&self, amount: Volume) -> Volume { let mut drawn = amount; if amount > self.reservoir_volume { @@ -634,6 +638,10 @@ impl ElectricPump { self.active = false; } + pub fn get_rpm(&self) -> f64 { + self.rpm + } + pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { //TODO Simulate speed of pump depending on pump load (flow?/ current?) //Pump startup/shutdown process From 3275332ce60024779e4eb63cce30b9ad792c49be Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:20:34 +0200 Subject: [PATCH 055/122] Refactored hydraulics logic --- src/systems/a320_systems/src/hydraulic.rs | 439 ++++++++++++++-------- 1 file changed, 276 insertions(+), 163 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 5d36e3b90aa..7de55f74dc7 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -12,7 +12,7 @@ use systems::simulation::{ }; pub struct A320Hydraulic { - hyd_logic_inputs: A320HydraulicLogic, + hyd_logic: A320HydraulicLogic, hyd_brake_logic: A320HydraulicBrakingLogic, blue_loop: HydLoop, green_loop: HydLoop, @@ -41,7 +41,7 @@ impl A320Hydraulic { pub fn new() -> A320Hydraulic { A320Hydraulic { - hyd_logic_inputs: A320HydraulicLogic::new(), + hyd_logic: A320HydraulicLogic::new(), hyd_brake_logic: A320HydraulicBrakingLogic::new(), blue_loop: HydLoop::new( @@ -108,19 +108,19 @@ impl A320Hydraulic { } pub fn green_edp_has_fault(&self) -> bool { - self.engine_driven_pump_1.is_active() && !self.is_green_pressurised() + self.hyd_logic.green_edp_has_fault() } pub fn yellow_epump_has_fault(&self) -> bool { - self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() + self.hyd_logic.yellow_epump_has_fault() } pub fn yellow_edp_has_fault(&self) -> bool { - self.engine_driven_pump_2.is_active() && !self.is_yellow_pressurised() + self.hyd_logic.yellow_edp_has_fault() } pub fn blue_epump_has_fault(&self) -> bool { - self.blue_electric_pump.is_active() && !self.is_blue_pressurised() + self.hyd_logic.blue_epump_has_fault() } // Updates pressure available state based on pressure switches @@ -156,6 +156,51 @@ impl A320Hydraulic { } } + fn set_hydraulics_logic_feedbacks(&mut self) { + self.hyd_logic + .set_green_pressurised(self.is_green_pressurised()); + self.hyd_logic + .set_blue_pressurised(self.is_blue_pressurised()); + self.hyd_logic + .set_yellow_pressurised(self.is_yellow_pressurised()); + } + + fn update_hydraulics_logic_outputs(&mut self) { + self.ptu.enabling(self.hyd_logic.ptu_ena_output()); + + if self.hyd_logic.rat_on_output() { + self.rat.set_active() + } + + if self.hyd_logic.blue_electric_pump_output() { + self.blue_electric_pump.start(); + } else { + self.blue_electric_pump.stop(); + } + + if self.hyd_logic.yellow_electric_pump_output() { + self.yellow_electric_pump.start(); + } else { + self.yellow_electric_pump.stop(); + } + + self.yellow_loop + .set_fire_shutoff_valve_state(self.hyd_logic.yellow_loop_fire_valve_output()); + self.green_loop + .set_fire_shutoff_valve_state(self.hyd_logic.green_loop_fire_valve_output()); + + if self.hyd_logic.engine_driven_pump_1_output() { + self.engine_driven_pump_1.start(); + } else { + self.engine_driven_pump_1.stop(); + } + if self.hyd_logic.engine_driven_pump_2_output() { + self.engine_driven_pump_2.start(); + } else { + self.engine_driven_pump_2.stop(); + } + } + pub fn is_blue_pressurised(&self) -> bool { self.is_blue_pressurised } @@ -250,13 +295,17 @@ impl A320Hydraulic { engine_fire_overhead: &A320EngineFireOverheadPanel, min_hyd_loop_timestep: &Duration, ) { + self.update_hyd_avail_states(); + self.set_hydraulics_logic_feedbacks(); + // Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly - self.update_logic( + self.hyd_logic.update( &min_hyd_loop_timestep, overhead_panel, engine_fire_overhead, context, ); + self.update_hydraulics_logic_outputs(); // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic @@ -306,159 +355,6 @@ impl A320Hydraulic { self.braking_circuit_norm.reset_accumulators(); self.braking_circuit_altn.reset_accumulators(); } - - fn update_logic( - &mut self, - delta_time_update: &Duration, - overhead_panel: &A320HydraulicOverheadPanel, - engine_fire_overhead: &A320EngineFireOverheadPanel, - context: &UpdateContext, - ) { - self.update_external_cond(&delta_time_update); - - self.update_hyd_avail_states(); - - self.update_pump_faults(); - - self.update_rat_deploy(context); - - self.update_ed_pump_states(overhead_panel, engine_fire_overhead); - - self.update_e_pump_states(overhead_panel); - - self.update_ptu_logic(overhead_panel); - } - - fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - let ptu_inhibit = self.hyd_logic_inputs.cargo_operated_ptu_cond - && overhead_panel.yellow_epump_push_button.is_auto(); - if overhead_panel.ptu_push_button.is_auto() - && (!self.hyd_logic_inputs.weight_on_wheels - || self.hyd_logic_inputs.eng_1_master_on && self.hyd_logic_inputs.eng_2_master_on - || !self.hyd_logic_inputs.eng_1_master_on && !self.hyd_logic_inputs.eng_2_master_on - || (!self.hyd_logic_inputs.parking_brake_lever_pos - && !self.hyd_logic_inputs.nsw_pin_inserted_cond)) - && !ptu_inhibit - { - self.ptu.enabling(true); - } else { - self.ptu.enabling(false); - } - } - - fn update_rat_deploy(&mut self, context: &UpdateContext) { - // RAT Deployment - //Todo check all other needed conditions this is faked with engine master while it should check elec buses - if !self.hyd_logic_inputs.eng_1_master_on - && !self.hyd_logic_inputs.eng_2_master_on - && context.indicated_airspeed() > Velocity::new::(100.) - //Todo get speed from ADIRS - { - self.rat.set_active(); - } - } - - fn update_external_cond(&mut self, delta_time_update: &Duration) { - // Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update - if self.hyd_logic_inputs.weight_on_wheels { - self.hyd_logic_inputs.cargo_operated_ptu_cond = self - .hyd_logic_inputs - .is_cargo_operation_ptu_flag(&delta_time_update); - self.hyd_logic_inputs.cargo_operated_ypump_cond = self - .hyd_logic_inputs - .is_cargo_operation_flag(&delta_time_update); - self.hyd_logic_inputs.nsw_pin_inserted_cond = self - .hyd_logic_inputs - .is_nsw_pin_inserted_flag(&delta_time_update); - } - } - - fn update_pump_faults(&mut self) { - // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - if self.yellow_electric_pump.is_active() && !self.is_yellow_pressurised() { - self.hyd_logic_inputs.yellow_epump_has_fault = true; - } else { - self.hyd_logic_inputs.yellow_epump_has_fault = false; - } - if self.engine_driven_pump_1.is_active() && !self.is_green_pressurised() { - self.hyd_logic_inputs.green_edp_has_fault = true; - } else { - self.hyd_logic_inputs.green_edp_has_fault = false; - } - if self.engine_driven_pump_2.is_active() && !self.is_yellow_pressurised() { - self.hyd_logic_inputs.yellow_edp_has_fault = true; - } else { - self.hyd_logic_inputs.yellow_edp_has_fault = false; - } - if self.blue_electric_pump.is_active() && !self.is_blue_pressurised() { - self.hyd_logic_inputs.blue_epump_has_fault = true; - } else { - self.hyd_logic_inputs.blue_epump_has_fault = false; - } - } - - fn update_ed_pump_states( - &mut self, - overhead_panel: &A320HydraulicOverheadPanel, - engine_fire_overhead: &A320EngineFireOverheadPanel, - ) { - if overhead_panel.edp1_push_button.is_auto() - && self.hyd_logic_inputs.eng_1_master_on - && !engine_fire_overhead.eng1_fire_pb.is_released() - { - self.engine_driven_pump_1.start(); - } else if overhead_panel.edp1_push_button.is_off() { - self.engine_driven_pump_1.stop(); - } - - //FIRE valves logic for EDP1 - if engine_fire_overhead.eng1_fire_pb.is_released() { - self.engine_driven_pump_1.stop(); - self.green_loop.set_fire_shutoff_valve_state(false); - } else { - self.green_loop.set_fire_shutoff_valve_state(true); - } - - if overhead_panel.edp2_push_button.is_auto() - && self.hyd_logic_inputs.eng_2_master_on - && !engine_fire_overhead.eng2_fire_pb.is_released() - { - self.engine_driven_pump_2.start(); - } else if overhead_panel.edp2_push_button.is_off() { - self.engine_driven_pump_2.stop(); - } - - //FIRE valves logic for EDP2 - if engine_fire_overhead.eng2_fire_pb.is_released() { - self.engine_driven_pump_2.stop(); - self.yellow_loop.set_fire_shutoff_valve_state(false); - } else { - self.yellow_loop.set_fire_shutoff_valve_state(true); - } - } - - fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - if overhead_panel.yellow_epump_push_button.is_on() - || self.hyd_logic_inputs.cargo_operated_ypump_cond - { - self.yellow_electric_pump.start(); - } else if overhead_panel.yellow_epump_push_button.is_auto() { - self.yellow_electric_pump.stop(); - } - if overhead_panel.blue_epump_push_button.is_auto() { - if self.hyd_logic_inputs.eng_1_master_on - || self.hyd_logic_inputs.eng_2_master_on - || overhead_panel.blue_epump_override_push_button.is_on() - { - self.blue_electric_pump.start(); - } else { - self.blue_electric_pump.stop(); - } - } else if overhead_panel.blue_epump_push_button.is_off() { - self.blue_electric_pump.stop(); - } - } } impl SimulationElement for A320Hydraulic { @@ -475,7 +371,7 @@ impl SimulationElement for A320Hydraulic { self.green_loop.accept(visitor); self.yellow_loop.accept(visitor); - self.hyd_logic_inputs.accept(visitor); + self.hyd_logic.accept(visitor); self.hyd_brake_logic.accept(visitor); self.braking_circuit_norm.accept(visitor); @@ -625,6 +521,19 @@ pub struct A320HydraulicLogic { cargo_operated_ptu_cond: bool, cargo_operated_ypump_cond: bool, nsw_pin_inserted_cond: bool, + + ptu_ena_output: bool, + rat_on_output: bool, + blue_electric_pump_output: bool, + yellow_electric_pump_output: bool, + green_loop_fire_valve_output: bool, + yellow_loop_fire_valve_output: bool, + engine_driven_pump_2_output: bool, + engine_driven_pump_1_output: bool, + + green_loop_pressurised_feedback: bool, + blue_loop_pressurised_feedback: bool, + yellow_loop_pressurised_feedback: bool, } //Implements low level logic for all hydraulics commands @@ -657,9 +566,71 @@ impl A320HydraulicLogic { cargo_operated_ptu_cond: false, cargo_operated_ypump_cond: false, nsw_pin_inserted_cond: false, + + ptu_ena_output: false, + rat_on_output: false, + blue_electric_pump_output: false, + yellow_electric_pump_output: false, + green_loop_fire_valve_output: false, + yellow_loop_fire_valve_output: false, + engine_driven_pump_2_output: true, + engine_driven_pump_1_output: true, + + green_loop_pressurised_feedback: false, + blue_loop_pressurised_feedback: false, + yellow_loop_pressurised_feedback: false, } } + pub fn ptu_ena_output(&self) -> bool { + self.ptu_ena_output + } + pub fn rat_on_output(&self) -> bool { + self.rat_on_output + } + pub fn blue_electric_pump_output(&self) -> bool { + self.blue_electric_pump_output + } + pub fn yellow_electric_pump_output(&self) -> bool { + self.yellow_electric_pump_output + } + pub fn green_loop_fire_valve_output(&self) -> bool { + self.green_loop_fire_valve_output + } + pub fn yellow_loop_fire_valve_output(&self) -> bool { + self.yellow_loop_fire_valve_output + } + pub fn engine_driven_pump_2_output(&self) -> bool { + self.engine_driven_pump_2_output + } + pub fn engine_driven_pump_1_output(&self) -> bool { + self.engine_driven_pump_1_output + } + pub fn green_edp_has_fault(&self) -> bool { + self.green_edp_has_fault + } + pub fn yellow_edp_has_fault(&self) -> bool { + self.yellow_edp_has_fault + } + pub fn yellow_epump_has_fault(&self) -> bool { + self.yellow_epump_has_fault + } + pub fn blue_epump_has_fault(&self) -> bool { + self.blue_epump_has_fault + } + + pub fn set_green_pressurised(&mut self, is_pressurised: bool) { + self.green_loop_pressurised_feedback = is_pressurised; + } + + pub fn set_blue_pressurised(&mut self, is_pressurised: bool) { + self.blue_loop_pressurised_feedback = is_pressurised; + } + + pub fn set_yellow_pressurised(&mut self, is_pressurised: bool) { + self.yellow_loop_pressurised_feedback = is_pressurised; + } + //Given a condition and associated timer and timeout value, returns if timeout is over after condition went to false fn timeout_condition_update( current_timer: &mut Duration, @@ -719,6 +690,148 @@ impl A320HydraulicLogic { delta_time_update, ) } + + fn update( + &mut self, + delta_time_update: &Duration, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + context: &UpdateContext, + ) { + self.update_external_cond(&delta_time_update); + + self.update_pump_faults(); + + self.update_rat_deploy(context); + + self.update_ed_pump_states(overhead_panel, engine_fire_overhead); + + self.update_e_pump_states(overhead_panel); + + self.update_ptu_logic(overhead_panel); + } + + fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + let ptu_inhibit = + self.cargo_operated_ptu_cond && overhead_panel.yellow_epump_push_button.is_auto(); + if 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.nsw_pin_inserted_cond)) + && !ptu_inhibit + { + self.ptu_ena_output = true; + } else { + self.ptu_ena_output = false; + } + } + + fn update_rat_deploy(&mut self, context: &UpdateContext) { + // RAT Deployment + //Todo check all other needed conditions this is faked with engine master while it should check elec buses + if !self.eng_1_master_on + && !self.eng_2_master_on + && context.indicated_airspeed() > Velocity::new::(100.) + //Todo get speed from ADIRS + { + self.rat_on_output = true; + } + } + + fn update_external_cond(&mut self, delta_time_update: &Duration) { + // Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update + if self.weight_on_wheels { + self.cargo_operated_ptu_cond = self.is_cargo_operation_ptu_flag(&delta_time_update); + self.cargo_operated_ypump_cond = self.is_cargo_operation_flag(&delta_time_update); + self.nsw_pin_inserted_cond = self.is_nsw_pin_inserted_flag(&delta_time_update); + } + } + + fn update_pump_faults(&mut self) { + // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + if self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback { + self.yellow_epump_has_fault = true; + } else { + self.yellow_epump_has_fault = false; + } + if self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback { + self.green_edp_has_fault = true; + } else { + self.green_edp_has_fault = false; + } + if self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback { + self.yellow_edp_has_fault = true; + } else { + self.yellow_edp_has_fault = false; + } + if self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback { + self.blue_epump_has_fault = true; + } else { + self.blue_epump_has_fault = false; + } + } + + fn update_ed_pump_states( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + ) { + if overhead_panel.edp1_push_button.is_auto() + && self.eng_1_master_on + && !engine_fire_overhead.eng1_fire_pb.is_released() + { + self.engine_driven_pump_1_output = true; + } else if overhead_panel.edp1_push_button.is_off() { + self.engine_driven_pump_1_output = false; + } + + //FIRE valves logic for EDP1 + if engine_fire_overhead.eng1_fire_pb.is_released() { + self.engine_driven_pump_1_output = false; + self.green_loop_fire_valve_output = false; + } else { + self.green_loop_fire_valve_output = true; + } + + if overhead_panel.edp2_push_button.is_auto() + && self.eng_2_master_on + && !engine_fire_overhead.eng2_fire_pb.is_released() + { + self.engine_driven_pump_2_output = true; + } else if overhead_panel.edp2_push_button.is_off() { + self.engine_driven_pump_2_output = false; + } + + //FIRE valves logic for EDP2 + if engine_fire_overhead.eng2_fire_pb.is_released() { + self.engine_driven_pump_2_output = false; + self.yellow_loop_fire_valve_output = false; + } else { + self.yellow_loop_fire_valve_output = true; + } + } + + fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + if overhead_panel.yellow_epump_push_button.is_on() || self.cargo_operated_ypump_cond { + self.yellow_electric_pump_output = true; + } else if overhead_panel.yellow_epump_push_button.is_auto() { + self.yellow_electric_pump_output = false; + } + if overhead_panel.blue_epump_push_button.is_auto() { + if self.eng_1_master_on + || self.eng_2_master_on + || overhead_panel.blue_epump_override_push_button.is_on() + { + self.blue_electric_pump_output = true; + } else { + self.blue_electric_pump_output = false; + } + } else if overhead_panel.blue_epump_push_button.is_off() { + self.blue_electric_pump_output = false; + } + } } impl SimulationElement for A320HydraulicLogic { @@ -884,11 +997,11 @@ mod tests { } fn is_nws_pin_inserted(&self) -> bool { - self.hydraulics.hyd_logic_inputs.nsw_pin_inserted_cond + self.hydraulics.hyd_logic.nsw_pin_inserted_cond } fn is_cargo_powering_yellow_epump(&self) -> bool { - self.hydraulics.hyd_logic_inputs.cargo_operated_ypump_cond + self.hydraulics.hyd_logic.cargo_operated_ypump_cond } fn is_ptu_ena(&self) -> bool { From 8b5d2a4b8a1fc5a5ecb5472e5075a0a028bd1e82 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:43:12 +0200 Subject: [PATCH 056/122] simpler bool updates --- src/systems/a320_systems/src/hydraulic.rs | 31 ++++++++--------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 7de55f74dc7..cda38de481c 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -751,26 +751,17 @@ impl A320HydraulicLogic { fn update_pump_faults(&mut self) { // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - if self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback { - self.yellow_epump_has_fault = true; - } else { - self.yellow_epump_has_fault = false; - } - if self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback { - self.green_edp_has_fault = true; - } else { - self.green_edp_has_fault = false; - } - if self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback { - self.yellow_edp_has_fault = true; - } else { - self.yellow_edp_has_fault = false; - } - if self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback { - self.blue_epump_has_fault = true; - } else { - self.blue_epump_has_fault = false; - } + self.yellow_epump_has_fault = + self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback; + + self.green_edp_has_fault = + self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback; + + self.yellow_edp_has_fault = + self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback; + + self.blue_epump_has_fault = + self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback; } fn update_ed_pump_states( From 65d15a473ef60477260707e7aba414a5c1950b8a Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:43:12 +0200 Subject: [PATCH 057/122] simpler bool updates --- src/systems/a320_systems/src/hydraulic.rs | 31 ++++++++--------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 7de55f74dc7..cda38de481c 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -751,26 +751,17 @@ impl A320HydraulicLogic { fn update_pump_faults(&mut self) { // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - if self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback { - self.yellow_epump_has_fault = true; - } else { - self.yellow_epump_has_fault = false; - } - if self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback { - self.green_edp_has_fault = true; - } else { - self.green_edp_has_fault = false; - } - if self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback { - self.yellow_edp_has_fault = true; - } else { - self.yellow_edp_has_fault = false; - } - if self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback { - self.blue_epump_has_fault = true; - } else { - self.blue_epump_has_fault = false; - } + self.yellow_epump_has_fault = + self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback; + + self.green_edp_has_fault = + self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback; + + self.yellow_edp_has_fault = + self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback; + + self.blue_epump_has_fault = + self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback; } fn update_ed_pump_states( From 0e126bf9c228986e79aae0fcbe9d39bd15bb4fb1 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 31 Mar 2021 15:53:19 +0200 Subject: [PATCH 058/122] Added depressurised mode for EDP --- src/systems/a320_systems/src/hydraulic.rs | 44 ++++++++++++++++++- src/systems/systems/src/hydraulic/mod.rs | 51 +++++++++++++++-------- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index cda38de481c..1b796bb7e92 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -536,7 +536,8 @@ pub struct A320HydraulicLogic { yellow_loop_pressurised_feedback: bool, } -//Implements low level logic for all hydraulics commands +// Implements low level logic for all hydraulics commands +// takes inputs from hydraulics and from plane systems, update outputs for pumps and all other hyd systems impl A320HydraulicLogic { const CARGO_OPERATED_TIMEOUT_YPUMP: f64 = 20.0; //Timeout to shut off yellow epump after cargo operation const CARGO_OPERATED_TIMEOUT_PTU: f64 = 40.0; //Timeout to keep ptu inhibited after cargo operation @@ -1542,6 +1543,47 @@ mod tests { assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); } + #[test] + fn edp_deactivation_test() { + 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 + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .start_eng2(Ratio::new::(50.)) + .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_test() { let mut test_bed = test_bed_with() diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 04fabec9186..75c0e3f1717 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -241,6 +241,8 @@ impl HydLoop { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI const ACCUMULATOR_MAX_VOLUME: f64 = 0.264; // in gallons + const STATIC_LEAK_FLOW: f64 = 0.05; //Gallon per s of flow lost to reservoir @ 3000psi + const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.4; const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = @@ -461,7 +463,10 @@ impl HydLoop { //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::( - 0.04 * delta_time.as_secs_f64() * (self.loop_pressure.get::() - 14.7) / 3000.0, + HydLoop::STATIC_LEAK_FLOW + * delta_time.as_secs_f64() + * (self.loop_pressure.get::() - 14.7) + / 3000.0, ); // Draw delta_vol from reservoir @@ -533,9 +538,11 @@ impl HydLoop { pub struct Pump { delta_vol_max: Volume, delta_vol_min: Volume, + current_displacement: Volume, press_breakpoints: [f64; 9], displacement_carac: [f64; 9], displacement_dynamic: f64, // Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic + is_pressurised: bool, } impl Pump { fn new( @@ -546,29 +553,41 @@ impl Pump { Pump { delta_vol_max: Volume::new::(0.), delta_vol_min: Volume::new::(0.), + current_displacement: Volume::new::(0.), press_breakpoints, displacement_carac, displacement_dynamic, + is_pressurised: true, } } + pub fn set_pressurised_state(&mut self, is_pressurised: bool) { + self.is_pressurised = is_pressurised; + } + fn update(&mut self, delta_time: &Duration, line: &HydLoop, rpm: f64) { - let displacement = self.calculate_displacement(line.get_pressure()); + let theoretical_displacement = self.calculate_displacement(line.get_pressure()); - let flow = - Pump::calculate_flow(rpm, displacement).max(VolumeRate::new::(0.)); + //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; - self.delta_vol_max = (1.0 - self.displacement_dynamic) * self.delta_vol_max - + self.displacement_dynamic * flow * Time::new::(delta_time.as_secs_f64()); + let flow = Pump::calculate_flow(rpm, self.current_displacement) + .max(VolumeRate::new::(0.)); + + self.delta_vol_max = flow * Time::new::(delta_time.as_secs_f64()); self.delta_vol_min = Volume::new::(0.0); } fn calculate_displacement(&self, pressure: Pressure) -> Volume { - Volume::new::(interpolation( - &self.press_breakpoints, - &self.displacement_carac, - pressure.get::(), - )) + if self.is_pressurised { + return Volume::new::(interpolation( + &self.press_breakpoints, + &self.displacement_carac, + pressure.get::(), + )); + } + Volume::new::(0.) } fn calculate_flow(rpm: f64, displacement: Volume) -> VolumeRate { @@ -721,22 +740,19 @@ impl EngineDrivenPump { pub fn update(&mut self, delta_time: &Duration, line: &HydLoop, engine: &Engine) { let n2_rpm = engine.corrected_n2().get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; - let mut pump_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; + let pump_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; - //TODO Activate pumps realistically, adding a pressurised/unpressurised mode - if !self.active { - //Hack for pump desactivation - pump_rpm = 0.0; - } self.pump.update(delta_time, line, pump_rpm); } pub fn start(&mut self) { self.active = true; + self.pump.set_pressurised_state(true); } pub fn stop(&mut self) { self.active = false; + self.pump.set_pressurised_state(false); } pub fn is_active(&self) -> bool { @@ -1217,6 +1233,7 @@ mod tests { let mut edp1 = engine_driven_pump(); assert!(!edp1.active); //Is off when created? + edp1.stop(); let mut engine1 = engine(Ratio::new::(0.0)); From 1619ea088a0ec4154f29c3d4eac74b1e5f71da69 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Fri, 2 Apr 2021 12:13:13 +0200 Subject: [PATCH 059/122] refactor: untangle A320HydraulicLogic part 1 --- src/systems/a320_systems/src/hydraulic.rs | 242 +++++++++--------- src/systems/systems/src/shared/mod.rs | 137 ++++++++++ .../systems/src/simulation/update_context.rs | 9 +- 3 files changed, 262 insertions(+), 126 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 1b796bb7e92..df1c5f67b0c 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,8 +1,7 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; -use systems::engine::Engine; -use systems::hydraulic::brakecircuit::BrakeCircuit; +use systems::{hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate}; use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RatPump}; use systems::overhead::{ AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, @@ -10,6 +9,7 @@ use systems::overhead::{ use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; +use systems::{engine::Engine}; pub struct A320Hydraulic { hyd_logic: A320HydraulicLogic, @@ -263,7 +263,7 @@ impl A320Hydraulic { engine2, overhead_panel, engine_fire_overhead, - &min_hyd_loop_timestep, + min_hyd_loop_timestep, ); } @@ -293,17 +293,16 @@ impl A320Hydraulic { engine2: &Engine, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, - min_hyd_loop_timestep: &Duration, + min_hyd_loop_timestep: Duration, ) { self.update_hyd_avail_states(); self.set_hydraulics_logic_feedbacks(); // Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly self.hyd_logic.update( - &min_hyd_loop_timestep, overhead_panel, engine_fire_overhead, - context, + &context.with_delta(min_hyd_loop_timestep), ); self.update_hydraulics_logic_outputs(); @@ -498,30 +497,83 @@ impl SimulationElement for A320HydraulicBrakingLogic { } } +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 + state: f64, +} +impl PushbackTug { + const STATE_NO_PUSHBACK: f64 = 3.; + + fn new() -> Self { + Self { + angle: 0., + previous_angle: 0., + state: 0., + } + } + + fn is_pushing(&self) -> bool { + // The angle keeps changing while pushing. + (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 struct A320HydraulicLogic { + forward_cargo_door: Door, + aft_cargo_door: Door, + pushback_tug: PushbackTug, + should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate, + should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate, + nose_wheel_steering_pin_inserted: DelayedFalseLogicGate, + parking_brake_lever_pos: bool, weight_on_wheels: bool, eng_1_master_on: bool, eng_2_master_on: bool, - nws_tow_engaged_timer: Duration, - cargo_door_front_pos: f64, - cargo_door_back_pos: f64, - cargo_door_front_pos_prev: f64, - cargo_door_back_pos_prev: f64, - cargo_door_timer: Duration, - cargo_door_timer_ptu: Duration, - pushback_angle: f64, - pushback_angle_prev: f64, - pushback_state: f64, yellow_epump_has_fault: bool, blue_epump_has_fault: bool, green_edp_has_fault: bool, yellow_edp_has_fault: bool, - cargo_operated_ptu_cond: bool, - cargo_operated_ypump_cond: bool, - nsw_pin_inserted_cond: bool, - ptu_ena_output: bool, rat_on_output: bool, blue_electric_pump_output: bool, @@ -539,35 +591,34 @@ pub struct A320HydraulicLogic { // Implements low level logic for all hydraulics commands // takes inputs from hydraulics and from plane systems, update outputs for pumps and all other hyd systems impl A320HydraulicLogic { - const CARGO_OPERATED_TIMEOUT_YPUMP: f64 = 20.0; //Timeout to shut off yellow epump after cargo operation - const CARGO_OPERATED_TIMEOUT_PTU: f64 = 40.0; //Timeout to keep ptu inhibited after cargo operation - const NWS_PIN_REMOVE_TIMEOUT: f64 = 15.0; //Time for ground crew to remove pin after tow + const DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION: Duration = Duration::from_secs(20); + 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); pub fn new() -> A320HydraulicLogic { A320HydraulicLogic { + forward_cargo_door: Door::new(5), + aft_cargo_door: Door::new(3), + pushback_tug: PushbackTug::new(), + should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate::new( + A320HydraulicLogic::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, + ), + should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate::new( + A320HydraulicLogic::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION + ), + nose_wheel_steering_pin_inserted: DelayedFalseLogicGate::new( + A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK + ), + parking_brake_lever_pos: true, weight_on_wheels: true, eng_1_master_on: false, eng_2_master_on: false, - nws_tow_engaged_timer: Duration::from_secs_f64(0.0), - cargo_door_front_pos: 0.0, - cargo_door_back_pos: 0.0, - cargo_door_front_pos_prev: 0.0, - cargo_door_back_pos_prev: 0.0, - cargo_door_timer: Duration::from_secs_f64(0.0), - cargo_door_timer_ptu: Duration::from_secs_f64(0.0), - pushback_angle: 0.0, - pushback_angle_prev: 0.0, - pushback_state: 0.0, yellow_epump_has_fault: false, blue_epump_has_fault: false, green_edp_has_fault: false, yellow_edp_has_fault: false, - cargo_operated_ptu_cond: false, - cargo_operated_ypump_cond: false, - nsw_pin_inserted_cond: false, - ptu_ena_output: false, rat_on_output: false, blue_electric_pump_output: false, @@ -632,74 +683,31 @@ impl A320HydraulicLogic { self.yellow_loop_pressurised_feedback = is_pressurised; } - //Given a condition and associated timer and timeout value, returns if timeout is over after condition went to false - fn timeout_condition_update( - current_timer: &mut Duration, - condition: bool, - max_duration: f64, - delta_time: &Duration, - ) -> bool { - if condition { - *current_timer = Duration::from_secs_f64(max_duration); - } else if *current_timer > *delta_time { - *current_timer -= *delta_time; - } else { - *current_timer = Duration::from_secs(0); - } - - *current_timer > Duration::from_secs_f64(0.0) + fn should_activate_yellow_pump_for_cargo_door_operation(&self) -> bool { + self.should_activate_yellow_pump_for_cargo_door_operation.output() } - //Checks if yellow pump should be activated by the cargo door maintenance panel operation - pub fn is_cargo_operation_flag(&mut self, delta_time_update: &Duration) -> bool { - let cargo_door_moved = (self.cargo_door_back_pos - self.cargo_door_back_pos_prev).abs() - > f64::EPSILON - || (self.cargo_door_front_pos - self.cargo_door_front_pos_prev).abs() > f64::EPSILON; - - A320HydraulicLogic::timeout_condition_update( - &mut self.cargo_door_timer, - cargo_door_moved, - A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP, - delta_time_update, - ) + fn should_inhibit_ptu_after_cargo_door_operation(&self) -> bool { + self.should_inhibit_ptu_after_cargo_door_operation.output() } - //Checks if ptu inhibit condition is still active after the cargo door operation - pub fn is_cargo_operation_ptu_flag(&mut self, delta_time_update: &Duration) -> bool { - let cargo_door_moved = (self.cargo_door_back_pos - self.cargo_door_back_pos_prev).abs() - > f64::EPSILON - || (self.cargo_door_front_pos - self.cargo_door_front_pos_prev).abs() > f64::EPSILON; - - A320HydraulicLogic::timeout_condition_update( - &mut self.cargo_door_timer_ptu, - cargo_door_moved, - A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU, - delta_time_update, - ) + fn nose_wheel_steering_pin_is_inserted(&self) -> bool { + self.nose_wheel_steering_pin_inserted.output() } - //Checks if pin is removed by ground crew after a pushback operation - pub fn is_nsw_pin_inserted_flag(&mut self, delta_time_update: &Duration) -> bool { - let pushback_in_progress = (self.pushback_angle - self.pushback_angle_prev).abs() - > f64::EPSILON - && (self.pushback_state - 3.0).abs() > f64::EPSILON; - - A320HydraulicLogic::timeout_condition_update( - &mut self.nws_tow_engaged_timer, - pushback_in_progress, - A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT, - delta_time_update, - ) + fn any_cargo_door_has_moved(&self) -> bool { + self.forward_cargo_door.has_moved() || self.aft_cargo_door.has_moved() } fn update( &mut self, - delta_time_update: &Duration, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, context: &UpdateContext, ) { - self.update_external_cond(&delta_time_update); + self.should_activate_yellow_pump_for_cargo_door_operation.update(context, self.any_cargo_door_has_moved()); + self.should_inhibit_ptu_after_cargo_door_operation.update(context, self.any_cargo_door_has_moved()); + self.nose_wheel_steering_pin_inserted.update(context, self.pushback_tug.is_pushing()); self.update_pump_faults(); @@ -714,12 +722,12 @@ impl A320HydraulicLogic { fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { let ptu_inhibit = - self.cargo_operated_ptu_cond && overhead_panel.yellow_epump_push_button.is_auto(); + self.should_inhibit_ptu_after_cargo_door_operation() && overhead_panel.yellow_epump_push_button.is_auto(); if 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.nsw_pin_inserted_cond)) + || (!self.parking_brake_lever_pos && !self.nose_wheel_steering_pin_is_inserted())) && !ptu_inhibit { self.ptu_ena_output = true; @@ -740,15 +748,6 @@ impl A320HydraulicLogic { } } - fn update_external_cond(&mut self, delta_time_update: &Duration) { - // Only evaluate ground conditions if on ground, if superman needs to operate cargo door in flight feel free to update - if self.weight_on_wheels { - self.cargo_operated_ptu_cond = self.is_cargo_operation_ptu_flag(&delta_time_update); - self.cargo_operated_ypump_cond = self.is_cargo_operation_flag(&delta_time_update); - self.nsw_pin_inserted_cond = self.is_nsw_pin_inserted_flag(&delta_time_update); - } - } - fn update_pump_faults(&mut self) { // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong @@ -806,7 +805,7 @@ impl A320HydraulicLogic { } fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - if overhead_panel.yellow_epump_push_button.is_on() || self.cargo_operated_ypump_cond { + if overhead_panel.yellow_epump_push_button.is_on() || self.should_activate_yellow_pump_for_cargo_door_operation() { self.yellow_electric_pump_output = true; } else if overhead_panel.yellow_epump_push_button.is_auto() { self.yellow_electric_pump_output = false; @@ -828,6 +827,10 @@ impl A320HydraulicLogic { impl SimulationElement for A320HydraulicLogic { fn accept(&mut self, visitor: &mut T) { + self.forward_cargo_door.accept(visitor); + self.aft_cargo_door.accept(visitor); + self.pushback_tug.accept(visitor); + visitor.visit(self); } @@ -836,17 +839,6 @@ impl SimulationElement for A320HydraulicLogic { self.eng_1_master_on = state.read_bool("GENERAL ENG1 STARTER ACTIVE"); self.eng_2_master_on = state.read_bool("GENERAL ENG2 STARTER ACTIVE"); self.weight_on_wheels = state.read_bool("SIM ON GROUND"); - - //Handling here of the previous values of cargo doors - self.cargo_door_front_pos_prev = self.cargo_door_front_pos; - self.cargo_door_front_pos = state.read_f64("EXIT OPEN 5"); - self.cargo_door_back_pos_prev = self.cargo_door_back_pos; - self.cargo_door_back_pos = state.read_f64("EXIT OPEN 3"); - - //Handling here of the previous values of pushback angle. Angle keeps moving while towed. Feel free to find better hack - self.pushback_angle_prev = self.pushback_angle; - self.pushback_angle = state.read_f64("PUSHBACK ANGLE"); - self.pushback_state = state.read_f64("PUSHBACK STATE"); } fn write(&self, writer: &mut SimulatorWriter) { @@ -989,11 +981,11 @@ mod tests { } fn is_nws_pin_inserted(&self) -> bool { - self.hydraulics.hyd_logic.nsw_pin_inserted_cond + self.hydraulics.hyd_logic.nose_wheel_steering_pin_is_inserted() } fn is_cargo_powering_yellow_epump(&self) -> bool { - self.hydraulics.hyd_logic.cargo_operated_ypump_cond + self.hydraulics.hyd_logic.should_activate_yellow_pump_for_cargo_door_operation() } fn is_ptu_ena(&self) -> bool { @@ -1401,9 +1393,9 @@ mod tests { 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(Duration::from_secs_f64( - A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_PTU, - )); //Should re enabled after 40s + test_bed = test_bed.run_waiting_for( + A320HydraulicLogic::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, + ); //Should re enabled after 40s assert!(test_bed.is_ptu_enabled()); } @@ -1427,9 +1419,9 @@ mod tests { test_bed = test_bed .set_pushback_state(false) - .run_waiting_for(Duration::from_secs_f64( - A320HydraulicLogic::NWS_PIN_REMOVE_TIMEOUT, - )); + .run_waiting_for( + A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, + ); assert!(!test_bed.aircraft.is_nws_pin_inserted()); } @@ -1450,9 +1442,9 @@ mod tests { 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(Duration::from_secs_f64( - A320HydraulicLogic::CARGO_OPERATED_TIMEOUT_YPUMP, - )); + test_bed = test_bed.run_waiting_for( + A320HydraulicLogic::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, + ); assert!(!test_bed.aircraft.is_cargo_powering_yellow_epump()); } diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index bf013de46c5..307615dfb9f 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( @@ -210,6 +241,112 @@ mod delayed_true_logic_gate_tests { } } +#[cfg(test)] +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::*; diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index c04f932c05e..71b05205f8c 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -8,7 +8,7 @@ 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, @@ -89,4 +89,11 @@ impl UpdateContext { pub fn long_accel(&self) -> Acceleration { self.longitudinal_acceleration } + + pub fn with_delta(&self, delta: Duration) -> Self { + let mut cloned: UpdateContext = (*self).clone(); + cloned.delta = delta; + + cloned + } } From 4f245be9ff0a2b745d635af86c343552c5759e71 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Fri, 2 Apr 2021 15:22:23 +0200 Subject: [PATCH 060/122] refactor: remove pump fault fields --- src/systems/a320_systems/src/hydraulic.rs | 156 ++++++++++------------ src/systems/systems/src/shared/mod.rs | 9 +- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index df1c5f67b0c..53b842ff21f 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,7 +1,7 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; -use systems::{hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate}; +use systems::engine::Engine; use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RatPump}; use systems::overhead::{ AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, @@ -9,7 +9,7 @@ use systems::overhead::{ use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; -use systems::{engine::Engine}; +use systems::{hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate}; pub struct A320Hydraulic { hyd_logic: A320HydraulicLogic, @@ -569,10 +569,6 @@ pub struct A320HydraulicLogic { weight_on_wheels: bool, eng_1_master_on: bool, eng_2_master_on: bool, - yellow_epump_has_fault: bool, - blue_epump_has_fault: bool, - green_edp_has_fault: bool, - yellow_edp_has_fault: bool, ptu_ena_output: bool, rat_on_output: bool, @@ -591,9 +587,11 @@ pub struct A320HydraulicLogic { // Implements low level logic for all hydraulics commands // takes inputs from hydraulics and from plane systems, update outputs for pumps and all other hyd systems impl A320HydraulicLogic { - const DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION: Duration = Duration::from_secs(20); + const DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION: Duration = + Duration::from_secs(20); 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); + const DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK: Duration = + Duration::from_secs(15); pub fn new() -> A320HydraulicLogic { A320HydraulicLogic { @@ -604,20 +602,16 @@ impl A320HydraulicLogic { A320HydraulicLogic::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, ), should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate::new( - A320HydraulicLogic::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION + A320HydraulicLogic::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, ), nose_wheel_steering_pin_inserted: DelayedFalseLogicGate::new( - A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK + A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, ), parking_brake_lever_pos: true, weight_on_wheels: true, eng_1_master_on: false, eng_2_master_on: false, - yellow_epump_has_fault: false, - blue_epump_has_fault: false, - green_edp_has_fault: false, - yellow_edp_has_fault: false, ptu_ena_output: false, rat_on_output: false, @@ -634,57 +628,77 @@ impl A320HydraulicLogic { } } - pub fn ptu_ena_output(&self) -> bool { + fn ptu_ena_output(&self) -> bool { self.ptu_ena_output } - pub fn rat_on_output(&self) -> bool { + + fn rat_on_output(&self) -> bool { self.rat_on_output } - pub fn blue_electric_pump_output(&self) -> bool { + + fn blue_electric_pump_output(&self) -> bool { self.blue_electric_pump_output } - pub fn yellow_electric_pump_output(&self) -> bool { + + fn yellow_electric_pump_output(&self) -> bool { self.yellow_electric_pump_output } - pub fn green_loop_fire_valve_output(&self) -> bool { + + fn green_loop_fire_valve_output(&self) -> bool { self.green_loop_fire_valve_output } - pub fn yellow_loop_fire_valve_output(&self) -> bool { + + fn yellow_loop_fire_valve_output(&self) -> bool { self.yellow_loop_fire_valve_output } - pub fn engine_driven_pump_2_output(&self) -> bool { + + fn engine_driven_pump_2_output(&self) -> bool { self.engine_driven_pump_2_output } - pub fn engine_driven_pump_1_output(&self) -> bool { + + fn engine_driven_pump_1_output(&self) -> bool { self.engine_driven_pump_1_output } - pub fn green_edp_has_fault(&self) -> bool { - self.green_edp_has_fault + + fn green_edp_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback } - pub fn yellow_edp_has_fault(&self) -> bool { - self.yellow_edp_has_fault + + fn yellow_edp_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback } - pub fn yellow_epump_has_fault(&self) -> bool { - self.yellow_epump_has_fault + + fn yellow_epump_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback } - pub fn blue_epump_has_fault(&self) -> bool { - self.blue_epump_has_fault + + fn blue_epump_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback } - pub fn set_green_pressurised(&mut self, is_pressurised: bool) { + fn set_green_pressurised(&mut self, is_pressurised: bool) { self.green_loop_pressurised_feedback = is_pressurised; } - pub fn set_blue_pressurised(&mut self, is_pressurised: bool) { + fn set_blue_pressurised(&mut self, is_pressurised: bool) { self.blue_loop_pressurised_feedback = is_pressurised; } - pub fn set_yellow_pressurised(&mut self, is_pressurised: bool) { + fn set_yellow_pressurised(&mut self, is_pressurised: bool) { self.yellow_loop_pressurised_feedback = is_pressurised; } fn should_activate_yellow_pump_for_cargo_door_operation(&self) -> bool { - self.should_activate_yellow_pump_for_cargo_door_operation.output() + self.should_activate_yellow_pump_for_cargo_door_operation + .output() } fn should_inhibit_ptu_after_cargo_door_operation(&self) -> bool { @@ -705,11 +719,12 @@ impl A320HydraulicLogic { engine_fire_overhead: &A320EngineFireOverheadPanel, context: &UpdateContext, ) { - self.should_activate_yellow_pump_for_cargo_door_operation.update(context, self.any_cargo_door_has_moved()); - self.should_inhibit_ptu_after_cargo_door_operation.update(context, self.any_cargo_door_has_moved()); - self.nose_wheel_steering_pin_inserted.update(context, self.pushback_tug.is_pushing()); - - self.update_pump_faults(); + self.should_activate_yellow_pump_for_cargo_door_operation + .update(context, self.any_cargo_door_has_moved()); + self.should_inhibit_ptu_after_cargo_door_operation + .update(context, self.any_cargo_door_has_moved()); + self.nose_wheel_steering_pin_inserted + .update(context, self.pushback_tug.is_pushing()); self.update_rat_deploy(context); @@ -721,8 +736,8 @@ impl A320HydraulicLogic { } fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - let ptu_inhibit = - self.should_inhibit_ptu_after_cargo_door_operation() && overhead_panel.yellow_epump_push_button.is_auto(); + let ptu_inhibit = self.should_inhibit_ptu_after_cargo_door_operation() + && overhead_panel.yellow_epump_push_button.is_auto(); if overhead_panel.ptu_push_button.is_auto() && (!self.weight_on_wheels || self.eng_1_master_on && self.eng_2_master_on @@ -748,22 +763,6 @@ impl A320HydraulicLogic { } } - fn update_pump_faults(&mut self) { - // Basic faults of pumps //TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.yellow_epump_has_fault = - self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback; - - self.green_edp_has_fault = - self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback; - - self.yellow_edp_has_fault = - self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback; - - self.blue_epump_has_fault = - self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback; - } - fn update_ed_pump_states( &mut self, overhead_panel: &A320HydraulicOverheadPanel, @@ -805,7 +804,9 @@ impl A320HydraulicLogic { } fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - if overhead_panel.yellow_epump_push_button.is_on() || self.should_activate_yellow_pump_for_cargo_door_operation() { + if overhead_panel.yellow_epump_push_button.is_on() + || self.should_activate_yellow_pump_for_cargo_door_operation() + { self.yellow_electric_pump_output = true; } else if overhead_panel.yellow_epump_push_button.is_auto() { self.yellow_electric_pump_output = false; @@ -842,21 +843,10 @@ impl SimulationElement for A320HydraulicLogic { } fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.green_edp_has_fault); - - writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.blue_epump_has_fault); - - writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.yellow_edp_has_fault); - - writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.yellow_epump_has_fault); - - writer.write_bool("OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT", self.green_edp_has_fault); - writer.write_bool( - "OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT", - self.yellow_edp_has_fault, - ); - writer.write_bool("OVHD_HYD_EPUMPB_PB_HAS_FAULT", self.blue_epump_has_fault); - writer.write_bool("OVHD_HYD_EPUMPY_PB_HAS_FAULT", self.yellow_epump_has_fault); + writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.green_edp_has_fault()); + writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.blue_epump_has_fault()); + writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.yellow_edp_has_fault()); + writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.yellow_epump_has_fault()); } } @@ -945,10 +935,6 @@ mod a320_hydraulic_simvars { assert!(test_bed.contains_key("HYD_BLUE_EPUMP_LOW_PRESS")); assert!(test_bed.contains_key("HYD_YELLOW_EDPUMP_LOW_PRESS")); assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT")); - assert!(test_bed.contains_key("OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT")); - assert!(test_bed.contains_key("OVHD_HYD_EPUMPB_PB_HAS_FAULT")); - assert!(test_bed.contains_key("OVHD_HYD_EPUMPY_PB_HAS_FAULT")); } } @@ -981,11 +967,15 @@ mod tests { } fn is_nws_pin_inserted(&self) -> bool { - self.hydraulics.hyd_logic.nose_wheel_steering_pin_is_inserted() + self.hydraulics + .hyd_logic + .nose_wheel_steering_pin_is_inserted() } fn is_cargo_powering_yellow_epump(&self) -> bool { - self.hydraulics.hyd_logic.should_activate_yellow_pump_for_cargo_door_operation() + self.hydraulics + .hyd_logic + .should_activate_yellow_pump_for_cargo_door_operation() } fn is_ptu_ena(&self) -> bool { @@ -1417,11 +1407,9 @@ mod tests { .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( - A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, - ); + test_bed = test_bed.set_pushback_state(false).run_waiting_for( + A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, + ); assert!(!test_bed.aircraft.is_nws_pin_inserted()); } diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index 307615dfb9f..a8aa8ab9066 100644 --- a/src/systems/systems/src/shared/mod.rs +++ b/src/systems/systems/src/shared/mod.rs @@ -276,7 +276,8 @@ mod delayed_false_logic_gate_tests { #[test] fn when_the_expression_is_false_initially_returns_false() { - let mut aircraft = TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(100))); + 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); @@ -286,7 +287,8 @@ mod delayed_false_logic_gate_tests { #[test] fn when_the_expression_is_true_returns_true() { - let mut aircraft = TestAircraft::new(DelayedFalseLogicGate::new(Duration::from_millis(100))); + 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); @@ -312,7 +314,8 @@ mod delayed_false_logic_gate_tests { #[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 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); From 2963812a449b09e60cef90d906f4666ccc62c2b8 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Fri, 2 Apr 2021 16:36:32 +0200 Subject: [PATCH 061/122] refactor: ram air turbine --- src/systems/a320_systems/src/hydraulic.rs | 49 +++--- src/systems/systems/src/hydraulic/mod.rs | 178 +++++++++++----------- 2 files changed, 107 insertions(+), 120 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 53b842ff21f..0486472c66d 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -2,7 +2,7 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; use systems::engine::Engine; -use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RatPump}; +use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RamAirTurbine}; use systems::overhead::{ AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, }; @@ -21,7 +21,7 @@ pub struct A320Hydraulic { engine_driven_pump_2: EngineDrivenPump, blue_electric_pump: ElectricPump, yellow_electric_pump: ElectricPump, - rat: RatPump, + ram_air_turbine: RamAirTurbine, ptu: Ptu, braking_circuit_norm: BrakeCircuit, braking_circuit_altn: BrakeCircuit, @@ -81,7 +81,7 @@ impl A320Hydraulic { engine_driven_pump_2: EngineDrivenPump::new("YELLOW"), blue_electric_pump: ElectricPump::new("BLUE"), yellow_electric_pump: ElectricPump::new("YELLOW"), - rat: RatPump::new(""), + ram_air_turbine: RamAirTurbine::new(""), ptu: Ptu::new(""), braking_circuit_norm: BrakeCircuit::new( @@ -165,11 +165,11 @@ impl A320Hydraulic { .set_yellow_pressurised(self.is_yellow_pressurised()); } - fn update_hydraulics_logic_outputs(&mut self) { + fn update_hydraulics_logic_outputs(&mut self, context: &UpdateContext) { self.ptu.enabling(self.hyd_logic.ptu_ena_output()); - if self.hyd_logic.rat_on_output() { - self.rat.set_active() + if self.hyd_logic.rat_should_be_deployed(context) { + self.ram_air_turbine.deploy(); } if self.hyd_logic.blue_electric_pump_output() { @@ -233,7 +233,7 @@ impl A320Hydraulic { time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); // Updating rat stowed pos on all frames in case it's used for graphics - self.rat.update_stow_pos(&context.delta()); + self.ram_air_turbine.update_position(&context.delta()); if number_of_steps_floating_point < 1.0 { // Can't do a full time step @@ -281,7 +281,7 @@ impl A320Hydraulic { // All the higher frequency updates like physics fn update_fast_rate(&mut self, context: &UpdateContext, delta_time_physics: &Duration) { - self.rat + self.ram_air_turbine .update_physics(&delta_time_physics, &context.indicated_airspeed()); } @@ -304,7 +304,7 @@ impl A320Hydraulic { engine_fire_overhead, &context.with_delta(min_hyd_loop_timestep), ); - self.update_hydraulics_logic_outputs(); + self.update_hydraulics_logic_outputs(&context.with_delta(min_hyd_loop_timestep)); // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic @@ -324,7 +324,7 @@ impl A320Hydraulic { self.blue_electric_pump .update(&min_hyd_loop_timestep, &self.blue_loop); - self.rat.update(&min_hyd_loop_timestep, &self.blue_loop); + self.ram_air_turbine.update(&min_hyd_loop_timestep, &self.blue_loop); self.green_loop.update( &min_hyd_loop_timestep, Vec::new(), @@ -343,7 +343,7 @@ impl A320Hydraulic { &min_hyd_loop_timestep, vec![&self.blue_electric_pump], Vec::new(), - vec![&self.rat], + vec![&self.ram_air_turbine], Vec::new(), ); @@ -362,7 +362,7 @@ impl SimulationElement for A320Hydraulic { self.blue_electric_pump.accept(visitor); self.engine_driven_pump_1.accept(visitor); self.engine_driven_pump_2.accept(visitor); - self.rat.accept(visitor); + self.ram_air_turbine.accept(visitor); self.ptu.accept(visitor); @@ -571,7 +571,6 @@ pub struct A320HydraulicLogic { eng_2_master_on: bool, ptu_ena_output: bool, - rat_on_output: bool, blue_electric_pump_output: bool, yellow_electric_pump_output: bool, green_loop_fire_valve_output: bool, @@ -614,7 +613,6 @@ impl A320HydraulicLogic { eng_2_master_on: false, ptu_ena_output: false, - rat_on_output: false, blue_electric_pump_output: false, yellow_electric_pump_output: false, green_loop_fire_valve_output: false, @@ -632,8 +630,13 @@ impl A320HydraulicLogic { self.ptu_ena_output } - fn rat_on_output(&self) -> bool { - self.rat_on_output + fn rat_should_be_deployed(&self, context: &UpdateContext) -> bool { + // RAT Deployment + // Todo check all other needed conditions this is faked with engine master while it should check elec buses + !self.eng_1_master_on + && !self.eng_2_master_on + //Todo get speed from ADIRS + && context.indicated_airspeed() > Velocity::new::(100.) } fn blue_electric_pump_output(&self) -> bool { @@ -726,8 +729,6 @@ impl A320HydraulicLogic { self.nose_wheel_steering_pin_inserted .update(context, self.pushback_tug.is_pushing()); - self.update_rat_deploy(context); - self.update_ed_pump_states(overhead_panel, engine_fire_overhead); self.update_e_pump_states(overhead_panel); @@ -751,18 +752,6 @@ impl A320HydraulicLogic { } } - fn update_rat_deploy(&mut self, context: &UpdateContext) { - // RAT Deployment - //Todo check all other needed conditions this is faked with engine master while it should check elec buses - if !self.eng_1_master_on - && !self.eng_2_master_on - && context.indicated_airspeed() > Velocity::new::(100.) - //Todo get speed from ADIRS - { - self.rat_on_output = true; - } - } - fn update_ed_pump_states( &mut self, overhead_panel: &A320HydraulicOverheadPanel, diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 75c0e3f1717..1ad0f3e85e8 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -433,7 +433,7 @@ impl HydLoop { delta_time: &Duration, electric_pumps: Vec<&ElectricPump>, engine_driven_pumps: Vec<&EngineDrivenPump>, - ram_air_pumps: Vec<&RatPump>, + ram_air_pumps: Vec<&RamAirTurbine>, ptus: Vec<&Ptu>, ) { let mut delta_vol_max = Volume::new::(0.); @@ -541,7 +541,8 @@ pub struct Pump { current_displacement: Volume, press_breakpoints: [f64; 9], displacement_carac: [f64; 9], - displacement_dynamic: f64, // Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic + // Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic + displacement_dynamic: f64, is_pressurised: bool, } impl Pump { @@ -568,7 +569,7 @@ impl Pump { fn update(&mut self, delta_time: &Duration, line: &HydLoop, rpm: f64) { let theoretical_displacement = self.calculate_displacement(line.get_pressure()); - //Actual displacement is the calculated one with a low pass filter applied to mimic displacement transients dynamic + // 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; @@ -768,31 +769,31 @@ impl PressureSource for EngineDrivenPump { } } -pub struct RatPropeller { - _id: String, +pub struct WindTurbine { rpm_id: String, - pos: f64, + position: f64, speed: f64, - acc: f64, + acceleration: f64, rpm: f64, torque_sum: f64, } -impl Default for RatPropeller { +impl Default for WindTurbine { fn default() -> Self { - RatPropeller::new("") + WindTurbine::new("") } } -impl SimulationElement for RatPropeller { +impl SimulationElement for WindTurbine { fn write(&self, writer: &mut SimulatorWriter) { writer.write_f64(&self.rpm_id, self.get_rpm()); } } -impl RatPropeller { - const LOW_SPEED_PHYSICS_ACTIVATION: f64 = 50.; //Low speed special calculation threshold. Under that value we compute resistant torque depending on pump angle and displacement +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] = [ @@ -800,13 +801,12 @@ impl RatPropeller { ]; const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 5., 1., 1.]; - pub fn new(id: &str) -> RatPropeller { - RatPropeller { - _id: String::from(id).to_uppercase(), + pub fn new(id: &str) -> Self { + Self { rpm_id: format!("HYD_{}RAT_RPM", id), - pos: RatPropeller::STOWED_ANGLE, + position: Self::STOWED_ANGLE, speed: 0., - acc: 0., + acceleration: 0., rpm: 0., torque_sum: 0., } @@ -818,37 +818,42 @@ impl RatPropeller { fn update_generated_torque(&mut self, indicated_speed: &Velocity, stow_pos: f64) { let cur_aplha = interpolation( - &RatPropeller::RPM_GOVERNOR_BREAKPTS, - &RatPropeller::PROP_ALPHA_MAP, + &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(); //simple model. stow pos sin simulates the angle of the blades vs wind while deploying + * (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 < RatPropeller::LOW_SPEED_PHYSICS_ACTIVATION { - pump_torque += (self.pos * 4.).cos() * displacement_ratio.max(0.35) * 35.; + 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; - self.torque_sum += pump_torque; //Static air drag of the propeller + //Static air drag of the propeller + self.torque_sum += pump_torque; } fn update_physics(&mut self, delta_time: &Duration) { - self.acc = self.torque_sum / RatPropeller::PROPELLER_INERTIA; - self.speed += self.acc * delta_time.as_secs_f64(); - self.pos += self.speed * delta_time.as_secs_f64(); + 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(); - self.rpm = self.speed * 30. / std::f64::consts::PI; //rad/s to RPM - self.torque_sum = 0.; //Reset torque accumulator at end of update + // rad/s to RPM + self.rpm = self.speed * 30. / std::f64::consts::PI; + + // Reset torque accumulator at end of update + self.torque_sum = 0.; } pub fn update( @@ -859,113 +864,106 @@ impl RatPropeller { displacement_ratio: f64, ) { if stow_pos > 0.1 { - //Do not update anything on the propeller if still stowed + // 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); } } } -pub struct RatPump { - _id: String, - stow_pos_id: String, +pub struct RamAirTurbine { + position_id: String, - active: bool, + deployment_commanded: bool, pump: Pump, - pub prop: RatPropeller, - stowed_position: f64, + wind_turbine: WindTurbine, + position: f64, max_displacement: f64, } -impl Default for RatPump { - fn default() -> Self { - RatPump::new("") - } -} - -impl SimulationElement for RatPump { +impl SimulationElement for RamAirTurbine { fn accept(&mut self, visitor: &mut T) { - self.prop.accept(visitor); + self.wind_turbine.accept(visitor); + visitor.visit(self); } fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64(&self.stow_pos_id, self.get_stow_position()); + writer.write_f64(&self.position_id, self.position); } } -impl RatPump { +impl RamAirTurbine { 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] = [1.15, 1.15, 1.15, 1.15, 1.15, 1.15, 0.5, 0.0, 0.0]; - const DISPLACEMENT_DYNAMICS: f64 = 0.2; //1 == no filtering. !!Warning, this will be affected by a different delta time + // 1 == no filtering. !!Warning, this will be affected by a different delta time + const DISPLACEMENT_DYNAMICS: f64 = 0.2; - const STOWING_SPEED: f64 = 1.; //Speed to go from 0 to 1 stow position per sec. 1 means full deploying in 1s + //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(id: &str) -> RatPump { + pub fn new(id: &str) -> Self { let mut max_disp = 0.; - for v in RatPump::DISPLACEMENT_MAP.iter() { + for v in Self::DISPLACEMENT_MAP.iter() { if v > &max_disp { max_disp = *v; } } - RatPump { - _id: String::from(id).to_uppercase(), - stow_pos_id: format!("HYD_{}RAT_STOW_POSITION", id), - active: false, + Self { + position_id: format!("HYD_{}RAT_STOW_POSITION", id), + deployment_commanded: false, pump: Pump::new( - RatPump::DISPLACEMENT_BREAKPTS, - RatPump::DISPLACEMENT_MAP, - RatPump::DISPLACEMENT_DYNAMICS, + Self::DISPLACEMENT_BREAKPTS, + Self::DISPLACEMENT_MAP, + Self::DISPLACEMENT_DYNAMICS, ), - prop: RatPropeller::new(id), - stowed_position: 0., + wind_turbine: WindTurbine::new(id), + position: 0., max_displacement: max_disp, } } pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { - self.pump.update(delta_time, line, self.prop.get_rpm()); + self.pump.update(delta_time, line, self.wind_turbine.get_rpm()); - //Now forcing min to max to force a true real time regulation. - self.pump.delta_vol_min = self.pump.delta_vol_max; //TODO: handle this properly by calculating who produced what volume at end of hyd loop update + // 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) { - let displacement_ratio = self.get_delta_vol_max().get::() / self.max_displacement; //Calculate the ratio of current displacement vs max displacement as an image of the load of the pump - self.prop.update( + // Calculate the ratio of current displacement vs max displacement as an image of the load of the pump + let displacement_ratio = self.get_delta_vol_max().get::() / self.max_displacement; + self.wind_turbine.update( &delta_time, &indicated_airspeed, - self.stowed_position, + self.position, displacement_ratio, ); } - pub fn update_stow_pos(&mut self, delta_time: &Duration) { - if self.active { - self.stowed_position += delta_time.as_secs_f64() * RatPump::STOWING_SPEED; + 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.stowed_position < 0. { - self.stowed_position = 0.; - } else if self.stowed_position > 1. { - self.stowed_position = 1.; + // Finally limiting pos in [0:1] range + if self.position < 0. { + self.position = 0.; + } else if self.position > 1. { + self.position = 1.; } } } - pub fn set_active(&mut self) { - self.active = true; - } - - pub fn get_stow_position(&self) -> f64 { - self.stowed_position + pub fn deploy(&mut self) { + self.deployment_commanded = true; } } -impl PressureSource for RatPump { +impl PressureSource for RamAirTurbine { fn get_delta_vol_max(&self) -> Volume { self.pump.get_delta_vol_max() } @@ -1148,7 +1146,7 @@ mod tests { #[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 = RatPump::new(""); + let mut rat = RamAirTurbine::new(""); let mut blue_loop = hydraulic_loop("BLUE"); let timestep = 0.05; @@ -1157,30 +1155,30 @@ mod tests { let mut time = 0.0; for x in 0..1500 { - rat.update_stow_pos(&context.delta()); + 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.active = false; - assert!(rat.stowed_position == 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.set_active(); + rat.deploy(); } if time >= 30. && time < 30. + timestep { println!("ASSERT RAT OUT AND SPINING"); assert!(blue_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(rat.stowed_position >= 0.999); - assert!(rat.prop.rpm >= 1000.); + 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::(2500.0)); - assert!(rat.prop.rpm >= 5000.); + assert!(rat.wind_turbine.rpm >= 5000.); } if time >= 70. && time < 70. + timestep { @@ -1190,7 +1188,7 @@ mod tests { if time >= 120. && time < 120. + timestep { println!("ASSERT RAT SLOWED DOWN"); - assert!(rat.prop.rpm <= 2500.); + assert!(rat.wind_turbine.rpm <= 2500.); } rat.update_physics(&context.delta(), &indicated_airpseed); @@ -1206,8 +1204,8 @@ mod tests { println!("Iteration {} Time {}", x, time); println!("-------------------------------------------"); println!("---PSI: {}", blue_loop.loop_pressure.get::()); - println!("---RAT stow pos: {}", rat.stowed_position); - println!("---RAT RPM: {}", rat.prop.rpm); + println!("---RAT stow pos: {}", rat.position); + println!("---RAT RPM: {}", rat.wind_turbine.rpm); println!("---RAT volMax: {}", rat.get_delta_vol_max().get::()); println!( "--------Reservoir Volume (g): {}", From 6408c667c7513af8d306b5099c0b701d579fa0f9 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Fri, 2 Apr 2021 16:58:47 +0200 Subject: [PATCH 062/122] refactor: struct impl ordering --- src/systems/a320_systems/src/hydraulic.rs | 11 +- src/systems/systems/src/hydraulic/mod.rs | 153 ++++++++---------- .../systems/src/simulation/update_context.rs | 6 +- 3 files changed, 74 insertions(+), 96 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 0486472c66d..05c49f44d61 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -324,7 +324,8 @@ impl A320Hydraulic { self.blue_electric_pump .update(&min_hyd_loop_timestep, &self.blue_loop); - self.ram_air_turbine.update(&min_hyd_loop_timestep, &self.blue_loop); + self.ram_air_turbine + .update(&min_hyd_loop_timestep, &self.blue_loop); self.green_loop.update( &min_hyd_loop_timestep, Vec::new(), @@ -392,7 +393,6 @@ pub struct A320HydraulicBrakingLogic { anti_skid_activated: bool, autobrakes_setting: u8, } - //Implements brakes computers logic impl A320HydraulicBrakingLogic { const PARK_BRAKE_DEMAND_DYNAMIC: f64 = 0.8; //Dynamic of the parking brake application/removal in (percent/100) per s @@ -481,7 +481,6 @@ impl A320HydraulicBrakingLogic { altn.set_brake_demand_right(self.right_brake_yellow_output); } } - impl SimulationElement for A320HydraulicBrakingLogic { fn accept(&mut self, visitor: &mut T) { visitor.visit(self); @@ -582,7 +581,6 @@ pub struct A320HydraulicLogic { blue_loop_pressurised_feedback: bool, yellow_loop_pressurised_feedback: bool, } - // Implements low level logic for all hydraulics commands // takes inputs from hydraulics and from plane systems, update outputs for pumps and all other hyd systems impl A320HydraulicLogic { @@ -814,7 +812,6 @@ impl A320HydraulicLogic { } } } - impl SimulationElement for A320HydraulicLogic { fn accept(&mut self, visitor: &mut T) { self.forward_cargo_door.accept(visitor); @@ -848,7 +845,6 @@ pub struct A320HydraulicOverheadPanel { pub yellow_epump_push_button: AutoOnFaultPushButton, pub blue_epump_override_push_button: OnOffFaultPushButton, } - impl A320HydraulicOverheadPanel { pub fn new() -> A320HydraulicOverheadPanel { A320HydraulicOverheadPanel { @@ -871,7 +867,6 @@ impl A320HydraulicOverheadPanel { .set_fault(hyd.yellow_epump_has_fault()); } } - impl SimulationElement for A320HydraulicOverheadPanel { fn accept(&mut self, visitor: &mut T) { self.edp1_push_button.accept(visitor); @@ -890,7 +885,6 @@ pub struct A320EngineFireOverheadPanel { pub eng1_fire_pb: FirePushButton, pub eng2_fire_pb: FirePushButton, } - impl A320EngineFireOverheadPanel { pub fn new() -> A320EngineFireOverheadPanel { A320EngineFireOverheadPanel { @@ -899,7 +893,6 @@ impl A320EngineFireOverheadPanel { } } } - impl SimulationElement for A320EngineFireOverheadPanel { fn accept(&mut self, visitor: &mut T) { self.eng1_fire_pb.accept(visitor); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 1ad0f3e85e8..31e3fb08c79 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -67,22 +67,6 @@ pub struct Ptu { flow_to_left: VolumeRate, last_flow: VolumeRate, } - -impl Default for Ptu { - fn default() -> Self { - Ptu::new("") - } -} - -impl SimulationElement for Ptu { - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool(&self.active_left_id, self.is_active_left); - writer.write_bool(&self.active_right_id, self.is_active_right); - writer.write_f64(&self.flow_id, self.get_flow().get::()); - writer.write_bool(&self.enabled_id, self.is_enabled()); - } -} - impl Ptu { //Low pass filter to handle flow dynamic: avoids instantaneous flow transient, // simulating RPM dynamic of PTU @@ -197,6 +181,19 @@ impl Ptu { self.is_enabled = enable_flag; } } +impl SimulationElement for Ptu { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_left_id, self.is_active_left); + writer.write_bool(&self.active_right_id, self.is_active_right); + writer.write_f64(&self.flow_id, self.get_flow().get::()); + writer.write_bool(&self.enabled_id, self.is_enabled()); + } +} +impl Default for Ptu { + fn default() -> Self { + Ptu::new("") + } +} pub struct HydLoop { _color_id: String, @@ -223,20 +220,6 @@ pub struct HydLoop { fire_shutoff_valve_opened: bool, has_fire_valve: bool, } - -impl SimulationElement for HydLoop { - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64(&self.pressure_id, self.get_pressure().get::()); - writer.write_f64( - &self.reservoir_id, - self.get_reservoir_volume().get::(), - ); - if self.has_fire_valve { - writer.write_bool(&self.fire_valve_id, self.is_fire_shutoff_valve_opened()); - } - } -} - impl HydLoop { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI const ACCUMULATOR_MAX_VOLUME: f64 = 0.264; // in gallons @@ -534,6 +517,18 @@ impl HydLoop { self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); } } +impl SimulationElement for HydLoop { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.pressure_id, self.get_pressure().get::()); + writer.write_f64( + &self.reservoir_id, + self.get_reservoir_volume().get::(), + ); + if self.has_fire_valve { + writer.write_bool(&self.fire_valve_id, self.is_fire_shutoff_valve_opened()); + } + } +} pub struct Pump { delta_vol_max: Volume, @@ -613,19 +608,6 @@ pub struct ElectricPump { rpm: f64, pump: Pump, } - -impl Default for ElectricPump { - fn default() -> Self { - ElectricPump::new("DEFAULT") - } -} - -impl SimulationElement for ElectricPump { - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool(&self.active_id, self.is_active()); - } -} - impl ElectricPump { const SPOOLUP_TIME: f64 = 4.0; const SPOOLDOWN_TIME: f64 = 4.0; @@ -691,6 +673,16 @@ impl PressureSource for ElectricPump { self.pump.get_delta_vol_min() } } +impl SimulationElement for ElectricPump { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_id, self.is_active()); + } +} +impl Default for ElectricPump { + fn default() -> Self { + ElectricPump::new("DEFAULT") + } +} pub struct EngineDrivenPump { _id: String, @@ -699,19 +691,6 @@ pub struct EngineDrivenPump { active: bool, pump: Pump, } - -impl Default for EngineDrivenPump { - fn default() -> Self { - EngineDrivenPump::new("DEFAULT") - } -} - -impl SimulationElement for EngineDrivenPump { - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool(&self.active_id, self.is_active()); - } -} - impl EngineDrivenPump { const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; //according to the Type Certificate Data Sheet of LEAP 1A26 //max N2 rpm is 116.5% @ 19391 RPM @@ -768,6 +747,16 @@ impl PressureSource for EngineDrivenPump { self.pump.get_delta_vol_max() } } +impl SimulationElement for EngineDrivenPump { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool(&self.active_id, self.is_active()); + } +} +impl Default for EngineDrivenPump { + fn default() -> Self { + EngineDrivenPump::new("DEFAULT") + } +} pub struct WindTurbine { rpm_id: String, @@ -778,19 +767,6 @@ pub struct WindTurbine { rpm: f64, torque_sum: f64, } - -impl Default for WindTurbine { - fn default() -> Self { - WindTurbine::new("") - } -} - -impl SimulationElement for WindTurbine { - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64(&self.rpm_id, self.get_rpm()); - } -} - 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.; @@ -871,6 +847,17 @@ impl WindTurbine { } } } +impl SimulationElement for WindTurbine { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_f64(&self.rpm_id, self.get_rpm()); + } +} +impl Default for WindTurbine { + fn default() -> Self { + WindTurbine::new("") + } +} + pub struct RamAirTurbine { position_id: String, @@ -880,19 +867,6 @@ pub struct RamAirTurbine { position: f64, max_displacement: f64, } - -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(&self.position_id, self.position); - } -} - impl RamAirTurbine { const DISPLACEMENT_BREAKPTS: [f64; 9] = [ 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, @@ -928,7 +902,8 @@ impl RamAirTurbine { } pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { - self.pump.update(delta_time, line, self.wind_turbine.get_rpm()); + self.pump + .update(delta_time, line, self.wind_turbine.get_rpm()); // 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 @@ -972,10 +947,20 @@ impl PressureSource for RamAirTurbine { self.pump.get_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(&self.position_id, self.position); + } +} #[cfg(test)] mod tests { - use crate::simulation::UpdateContext; use uom::si::{ acceleration::foot_per_second_squared, diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index 71b05205f8c..4099dfe422d 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -91,9 +91,9 @@ impl UpdateContext { } pub fn with_delta(&self, delta: Duration) -> Self { - let mut cloned: UpdateContext = (*self).clone(); - cloned.delta = delta; + let mut copy: UpdateContext = *self; + copy.delta = delta; - cloned + copy } } From 6dfd5d31dda03ec4ebe3f55518b58d7c5b549c23 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Fri, 2 Apr 2021 17:15:54 +0200 Subject: [PATCH 063/122] refactor: use Self, remove unused ids --- src/systems/a320_systems/src/hydraulic.rs | 8 +- src/systems/systems/src/hydraulic/mod.rs | 160 +++++++++------------- 2 files changed, 69 insertions(+), 99 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 05c49f44d61..07969f46408 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -2,7 +2,7 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; use systems::engine::Engine; -use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, Ptu, RamAirTurbine}; +use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, PowerTransferUnit, RamAirTurbine}; use systems::overhead::{ AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, }; @@ -22,7 +22,7 @@ pub struct A320Hydraulic { blue_electric_pump: ElectricPump, yellow_electric_pump: ElectricPump, ram_air_turbine: RamAirTurbine, - ptu: Ptu, + ptu: PowerTransferUnit, braking_circuit_norm: BrakeCircuit, braking_circuit_altn: BrakeCircuit, total_sim_time_elapsed: Duration, @@ -81,8 +81,8 @@ impl A320Hydraulic { engine_driven_pump_2: EngineDrivenPump::new("YELLOW"), blue_electric_pump: ElectricPump::new("BLUE"), yellow_electric_pump: ElectricPump::new("YELLOW"), - ram_air_turbine: RamAirTurbine::new(""), - ptu: Ptu::new(""), + ram_air_turbine: RamAirTurbine::new(), + ptu: PowerTransferUnit::new(), braking_circuit_norm: BrakeCircuit::new( "NORM", diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 31e3fb08c79..00490739e35 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -37,10 +37,9 @@ pub struct HydFluid { //temp : thermodynamic_temperature, current_bulk: Pressure, } - impl HydFluid { - pub fn new(bulk: Pressure) -> HydFluid { - HydFluid { + pub fn new(bulk: Pressure) -> Self { + Self { //temp:temp, current_bulk: bulk, } @@ -51,15 +50,8 @@ impl HydFluid { } } -//Power Transfer Unit //TODO enhance simulation with RPM and variable displacement on one side? -pub struct Ptu { - _id: String, - active_left_id: String, - active_right_id: String, - flow_id: String, - enabled_id: String, - +pub struct PowerTransferUnit { is_enabled: bool, is_active_right: bool, is_active_left: bool, @@ -67,7 +59,7 @@ pub struct Ptu { flow_to_left: VolumeRate, last_flow: VolumeRate, } -impl Ptu { +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.2; @@ -77,13 +69,8 @@ impl Ptu { // set to 0.5 PTU will only use half of the flow that all pumps are able to generate const AGRESSIVENESS_FACTOR: f64 = 0.8; - pub fn new(id: &str) -> Ptu { - Ptu { - _id: id.to_uppercase(), - active_left_id: format!("HYD_PTU{}_ACTIVE_L2R", id), - active_right_id: format!("HYD_PTU{}_ACTIVE_R2L", id), - flow_id: format!("HYD_PTU{}_MOTOR_FLOW", id), - enabled_id: format!("HYD_PTU{}_VALVE_OPENED", id), + pub fn new() -> Self { + Self { is_enabled: false, is_active_right: false, is_active_left: false, @@ -127,12 +114,12 @@ impl Ptu { //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::() * Ptu::AGRESSIVENESS_FACTOR, + loop_left.current_max_flow.get::() * Self::AGRESSIVENESS_FACTOR, ); //Low pass on flow - vr = Ptu::FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE * vr - + (1.0 - Ptu::FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE) + 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); @@ -147,12 +134,12 @@ impl Ptu { //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::() * Ptu::AGRESSIVENESS_FACTOR, + loop_right.current_max_flow.get::() * Self::AGRESSIVENESS_FACTOR, ); //Low pass on flow - vr = Ptu::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE * vr - + (1.0 - Ptu::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE) + 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 * 0.70); @@ -181,22 +168,21 @@ impl Ptu { self.is_enabled = enable_flag; } } -impl SimulationElement for Ptu { +impl SimulationElement for PowerTransferUnit { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool(&self.active_left_id, self.is_active_left); - writer.write_bool(&self.active_right_id, self.is_active_right); - writer.write_f64(&self.flow_id, self.get_flow().get::()); - writer.write_bool(&self.enabled_id, self.is_enabled()); + 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.get_flow().get::()); + writer.write_bool("HYD_PTU_VALVE_OPENED", self.is_enabled()); } } -impl Default for Ptu { +impl Default for PowerTransferUnit { fn default() -> Self { - Ptu::new("") + Self::new() } } pub struct HydLoop { - _color_id: String, pressure_id: String, reservoir_id: String, fire_valve_id: String, @@ -243,15 +229,14 @@ impl HydLoop { reservoir_volume: Volume, fluid: HydFluid, has_fire_valve: bool, - ) -> HydLoop { - HydLoop { - _color_id: id.to_uppercase(), + ) -> Self { + Self { pressure_id: format!("HYD_{}_PRESSURE", id), reservoir_id: format!("HYD_{}_RESERVOIR", id), fire_valve_id: format!("HYD_{}_FIRE_VALVE_OPENED", id), - accumulator_gas_pressure: Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE), - accumulator_gas_volume: Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME), + accumulator_gas_pressure: Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), + accumulator_gas_volume: Volume::new::(Self::ACCUMULATOR_MAX_VOLUME), accumulator_fluid_volume: Volume::new::(0.), connected_to_ptu_left_side, connected_to_ptu_right_side, @@ -264,8 +249,8 @@ impl HydLoop { fluid, current_delta_vol: Volume::new::(0.), current_flow: VolumeRate::new::(0.), - accumulator_press_breakpoints: HydLoop::ACCUMULATOR_PRESS_BREAKPTS, - accumulator_flow_carac: HydLoop::ACCUMULATOR_FLOW_CARAC, + accumulator_press_breakpoints: Self::ACCUMULATOR_PRESS_BREAKPTS, + accumulator_flow_carac: Self::ACCUMULATOR_FLOW_CARAC, current_max_flow: VolumeRate::new::(0.), fire_shutoff_valve_opened: true, has_fire_valve, @@ -350,16 +335,16 @@ impl HydLoop { *delta_vol -= volume_to_acc; } - self.accumulator_gas_pressure = (Pressure::new::(HydLoop::ACCUMULATOR_GAS_PRE_CHARGE) - * Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME)) - / (Volume::new::(HydLoop::ACCUMULATOR_MAX_VOLUME) + self.accumulator_gas_pressure = (Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE) + * Volume::new::(Self::ACCUMULATOR_MAX_VOLUME)) + / (Volume::new::(Self::ACCUMULATOR_MAX_VOLUME) - self.accumulator_fluid_volume); } fn update_ptu_flows( &mut self, delta_time: &Duration, - ptus: Vec<&Ptu>, + ptus: Vec<&PowerTransferUnit>, delta_vol: &mut Volume, reservoir_return: &mut Volume, ) { @@ -417,7 +402,7 @@ impl HydLoop { electric_pumps: Vec<&ElectricPump>, engine_driven_pumps: Vec<&EngineDrivenPump>, ram_air_pumps: Vec<&RamAirTurbine>, - ptus: Vec<&Ptu>, + ptus: Vec<&PowerTransferUnit>, ) { let mut delta_vol_max = Volume::new::(0.); let mut delta_vol_min = Volume::new::(0.); @@ -446,7 +431,7 @@ impl HydLoop { //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::( - HydLoop::STATIC_LEAK_FLOW + Self::STATIC_LEAK_FLOW * delta_time.as_secs_f64() * (self.loop_pressure.get::() - 14.7) / 3000.0, @@ -505,8 +490,8 @@ impl HydLoop { // Update Volumes self.loop_volume += delta_vol; // Low pass filter on final delta vol to help with stability and final flow noise - delta_vol = HydLoop::DELTA_VOL_LOW_PASS_FILTER * delta_vol - + (1. - HydLoop::DELTA_VOL_LOW_PASS_FILTER) * self.current_delta_vol; + 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); @@ -545,8 +530,8 @@ impl Pump { press_breakpoints: [f64; 9], displacement_carac: [f64; 9], displacement_dynamic: f64, - ) -> Pump { - Pump { + ) -> Self { + Self { delta_vol_max: Volume::new::(0.), delta_vol_min: Volume::new::(0.), current_displacement: Volume::new::(0.), @@ -568,7 +553,7 @@ impl Pump { self.current_displacement = (1.0 - self.displacement_dynamic) * self.current_displacement + self.displacement_dynamic * theoretical_displacement; - let flow = Pump::calculate_flow(rpm, self.current_displacement) + let flow = Self::calculate_flow(rpm, self.current_displacement) .max(VolumeRate::new::(0.)); self.delta_vol_max = flow * Time::new::(delta_time.as_secs_f64()); @@ -601,7 +586,6 @@ impl PressureSource for Pump { } pub struct ElectricPump { - _id: String, active_id: String, active: bool, @@ -618,16 +602,15 @@ impl ElectricPump { const DISPLACEMENT_MAP: [f64; 9] = [0.263, 0.263, 0.263, 0.263, 0.263, 0.263, 0.163, 0.0, 0.0]; const DISPLACEMENT_DYNAMICS: f64 = 1.0; //1 == No filtering - pub fn new(id: &str) -> ElectricPump { - ElectricPump { - _id: String::from(id).to_uppercase(), + pub fn new(id: &str) -> Self { + Self { active_id: format!("HYD_{}_EPUMP_ACTIVE", id), active: false, rpm: 0., pump: Pump::new( - ElectricPump::DISPLACEMENT_BREAKPTS, - ElectricPump::DISPLACEMENT_MAP, - ElectricPump::DISPLACEMENT_DYNAMICS, + Self::DISPLACEMENT_BREAKPTS, + Self::DISPLACEMENT_MAP, + Self::DISPLACEMENT_DYNAMICS, ), } } @@ -647,16 +630,16 @@ impl ElectricPump { pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { //TODO Simulate speed of pump depending on pump load (flow?/ current?) //Pump startup/shutdown process - if self.active && self.rpm < ElectricPump::NOMINAL_SPEED { - self.rpm += (ElectricPump::NOMINAL_SPEED / ElectricPump::SPOOLUP_TIME) + if self.active && self.rpm < Self::NOMINAL_SPEED { + self.rpm += (Self::NOMINAL_SPEED / Self::SPOOLUP_TIME) * delta_time.as_secs_f64(); } else if !self.active && self.rpm > 0.0 { - self.rpm -= (ElectricPump::NOMINAL_SPEED / ElectricPump::SPOOLDOWN_TIME) + self.rpm -= (Self::NOMINAL_SPEED / Self::SPOOLDOWN_TIME) * delta_time.as_secs_f64(); } //Limiting min and max speed - self.rpm = self.rpm.min(ElectricPump::NOMINAL_SPEED).max(0.0); + self.rpm = self.rpm.min(Self::NOMINAL_SPEED).max(0.0); self.pump.update(delta_time, line, self.rpm); } @@ -678,14 +661,8 @@ impl SimulationElement for ElectricPump { writer.write_bool(&self.active_id, self.is_active()); } } -impl Default for ElectricPump { - fn default() -> Self { - ElectricPump::new("DEFAULT") - } -} pub struct EngineDrivenPump { - _id: String, active_id: String, active: bool, @@ -704,23 +681,22 @@ impl EngineDrivenPump { const DISPLACEMENT_DYNAMICS: f64 = 0.3; //0.1 == 90% filtering on max displacement transient - pub fn new(id: &str) -> EngineDrivenPump { - EngineDrivenPump { - _id: String::from(id).to_uppercase(), + pub fn new(id: &str) -> Self { + Self { active_id: format!("HYD_{}_EDPUMP_ACTIVE", id), active: false, pump: Pump::new( - EngineDrivenPump::DISPLACEMENT_BREAKPTS, - EngineDrivenPump::DISPLACEMENT_MAP, - EngineDrivenPump::DISPLACEMENT_DYNAMICS, + Self::DISPLACEMENT_BREAKPTS, + Self::DISPLACEMENT_MAP, + Self::DISPLACEMENT_DYNAMICS, ), } } pub fn update(&mut self, delta_time: &Duration, line: &HydLoop, engine: &Engine) { let n2_rpm = - engine.corrected_n2().get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; - let pump_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; + engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; + let pump_rpm = n2_rpm * Self::PUMP_N2_GEAR_RATIO; self.pump.update(delta_time, line, pump_rpm); } @@ -752,15 +728,8 @@ impl SimulationElement for EngineDrivenPump { writer.write_bool(&self.active_id, self.is_active()); } } -impl Default for EngineDrivenPump { - fn default() -> Self { - EngineDrivenPump::new("DEFAULT") - } -} pub struct WindTurbine { - rpm_id: String, - position: f64, speed: f64, acceleration: f64, @@ -777,9 +746,8 @@ impl WindTurbine { ]; const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 5., 1., 1.]; - pub fn new(id: &str) -> Self { + pub fn new() -> Self { Self { - rpm_id: format!("HYD_{}RAT_RPM", id), position: Self::STOWED_ANGLE, speed: 0., acceleration: 0., @@ -849,18 +817,16 @@ impl WindTurbine { } impl SimulationElement for WindTurbine { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64(&self.rpm_id, self.get_rpm()); + writer.write_f64("HYD_RAT_RPM", self.get_rpm()); } } impl Default for WindTurbine { fn default() -> Self { - WindTurbine::new("") + Self::new() } } pub struct RamAirTurbine { - position_id: String, - deployment_commanded: bool, pump: Pump, wind_turbine: WindTurbine, @@ -879,7 +845,7 @@ impl RamAirTurbine { //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(id: &str) -> Self { + pub fn new() -> Self { let mut max_disp = 0.; for v in Self::DISPLACEMENT_MAP.iter() { if v > &max_disp { @@ -888,14 +854,13 @@ impl RamAirTurbine { } Self { - position_id: format!("HYD_{}RAT_STOW_POSITION", id), deployment_commanded: false, pump: Pump::new( Self::DISPLACEMENT_BREAKPTS, Self::DISPLACEMENT_MAP, Self::DISPLACEMENT_DYNAMICS, ), - wind_turbine: WindTurbine::new(id), + wind_turbine: WindTurbine::new(), position: 0., max_displacement: max_disp, } @@ -955,7 +920,12 @@ impl SimulationElement for RamAirTurbine { } fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64(&self.position_id, self.position); + writer.write_f64("HYD_RAT_STOW_POSITION", self.position); + } +} +impl Default for RamAirTurbine { + fn default() -> Self { + Self::new() } } @@ -1131,7 +1101,7 @@ mod tests { #[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 = RamAirTurbine::new(); let mut blue_loop = hydraulic_loop("BLUE"); let timestep = 0.05; @@ -1222,7 +1192,7 @@ mod tests { let mut green_loop = hydraulic_loop("GREEN"); - let mut ptu = Ptu::new(""); + let mut ptu = PowerTransferUnit::new(); let context = context(Duration::from_millis(100)); From 3477afc9836a14e8c24a7e23d64d6b83c6d609f0 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Fri, 2 Apr 2021 21:35:19 +0200 Subject: [PATCH 064/122] refactor: pump commands Definitely not where I want it to be yet. --- src/systems/a320_systems/src/hydraulic.rs | 259 +++++++++++++--------- src/systems/systems/src/hydraulic/mod.rs | 72 +++--- 2 files changed, 185 insertions(+), 146 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 07969f46408..8fb4bc9a226 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,14 +1,16 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; -use systems::engine::Engine; -use systems::hydraulic::{ElectricPump, EngineDrivenPump, HydFluid, HydLoop, PowerTransferUnit, RamAirTurbine}; +use systems::hydraulic::{ + ElectricPump, EngineDrivenPump, HydFluid, HydLoop, PowerTransferUnit, RamAirTurbine, +}; use systems::overhead::{ AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, }; use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; +use systems::{engine::Engine, hydraulic::PumpPressurisationCommand}; use systems::{hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate}; pub struct A320Hydraulic { @@ -166,39 +168,26 @@ impl A320Hydraulic { } fn update_hydraulics_logic_outputs(&mut self, context: &UpdateContext) { - self.ptu.enabling(self.hyd_logic.ptu_ena_output()); + self.ptu.set_enabled(self.hyd_logic.ptu_should_be_enabled()); if self.hyd_logic.rat_should_be_deployed(context) { self.ram_air_turbine.deploy(); } - if self.hyd_logic.blue_electric_pump_output() { - self.blue_electric_pump.start(); - } else { - self.blue_electric_pump.stop(); - } - - if self.hyd_logic.yellow_electric_pump_output() { - self.yellow_electric_pump.start(); - } else { - self.yellow_electric_pump.stop(); - } + self.blue_electric_pump + .command(self.hyd_logic.blue_electric_pump_pressurisation_command()); + self.yellow_electric_pump + .command(self.hyd_logic.yellow_electric_pump_pressurisation_command()); self.yellow_loop - .set_fire_shutoff_valve_state(self.hyd_logic.yellow_loop_fire_valve_output()); + .set_fire_shutoff_valve_state(self.hyd_logic.yellow_loop_fire_valve_should_be_open()); self.green_loop - .set_fire_shutoff_valve_state(self.hyd_logic.green_loop_fire_valve_output()); + .set_fire_shutoff_valve_state(self.hyd_logic.green_loop_fire_valve_should_be_open()); - if self.hyd_logic.engine_driven_pump_1_output() { - self.engine_driven_pump_1.start(); - } else { - self.engine_driven_pump_1.stop(); - } - if self.hyd_logic.engine_driven_pump_2_output() { - self.engine_driven_pump_2.start(); - } else { - self.engine_driven_pump_2.stop(); - } + self.engine_driven_pump_1 + .command(self.hyd_logic.engine_driven_pump_1_pressurisation_command()); + self.engine_driven_pump_2 + .command(self.hyd_logic.engine_driven_pump_2_pressurisation_command()); } pub fn is_blue_pressurised(&self) -> bool { @@ -569,13 +558,13 @@ pub struct A320HydraulicLogic { eng_1_master_on: bool, eng_2_master_on: bool, - ptu_ena_output: bool, - blue_electric_pump_output: bool, - yellow_electric_pump_output: bool, - green_loop_fire_valve_output: bool, - yellow_loop_fire_valve_output: bool, - engine_driven_pump_2_output: bool, - engine_driven_pump_1_output: bool, + ptu_should_be_enabled: bool, + blue_electric_pump_should_run: bool, + yellow_electric_pump_should_run: bool, + green_loop_fire_valve_should_be_open: bool, + yellow_loop_fire_valve_should_be_open: bool, + engine_driven_pump_2_should_run: bool, + engine_driven_pump_1_should_run: bool, green_loop_pressurised_feedback: bool, blue_loop_pressurised_feedback: bool, @@ -610,13 +599,13 @@ impl A320HydraulicLogic { eng_1_master_on: false, eng_2_master_on: false, - ptu_ena_output: false, - blue_electric_pump_output: false, - yellow_electric_pump_output: false, - green_loop_fire_valve_output: false, - yellow_loop_fire_valve_output: false, - engine_driven_pump_2_output: true, - engine_driven_pump_1_output: true, + ptu_should_be_enabled: false, + blue_electric_pump_should_run: false, + yellow_electric_pump_should_run: false, + green_loop_fire_valve_should_be_open: false, + yellow_loop_fire_valve_should_be_open: false, + engine_driven_pump_2_should_run: true, + engine_driven_pump_1_should_run: true, green_loop_pressurised_feedback: false, blue_loop_pressurised_feedback: false, @@ -624,8 +613,8 @@ impl A320HydraulicLogic { } } - fn ptu_ena_output(&self) -> bool { - self.ptu_ena_output + fn ptu_should_be_enabled(&self) -> bool { + self.ptu_should_be_enabled } fn rat_should_be_deployed(&self, context: &UpdateContext) -> bool { @@ -637,52 +626,76 @@ impl A320HydraulicLogic { && context.indicated_airspeed() > Velocity::new::(100.) } - fn blue_electric_pump_output(&self) -> bool { - self.blue_electric_pump_output + fn blue_electric_pump_pressurisation_command(&self) -> PumpPressurisationCommand { + if self.blue_electric_pump_should_run { + PumpPressurisationCommand::Pressurise + } else { + PumpPressurisationCommand::Depressurise + } } - fn yellow_electric_pump_output(&self) -> bool { - self.yellow_electric_pump_output + fn yellow_electric_pump_pressurisation_command(&self) -> PumpPressurisationCommand { + if self.yellow_electric_pump_should_run { + PumpPressurisationCommand::Pressurise + } else { + PumpPressurisationCommand::Depressurise + } } - fn green_loop_fire_valve_output(&self) -> bool { - self.green_loop_fire_valve_output + fn green_loop_fire_valve_should_be_open(&self) -> bool { + self.green_loop_fire_valve_should_be_open } - fn yellow_loop_fire_valve_output(&self) -> bool { - self.yellow_loop_fire_valve_output + fn yellow_loop_fire_valve_should_be_open(&self) -> bool { + self.yellow_loop_fire_valve_should_be_open } - fn engine_driven_pump_2_output(&self) -> bool { - self.engine_driven_pump_2_output + fn engine_driven_pump_1_pressurisation_command(&self) -> PumpPressurisationCommand { + if self.engine_driven_pump_1_should_run { + PumpPressurisationCommand::Pressurise + } else { + PumpPressurisationCommand::Depressurise + } } - fn engine_driven_pump_1_output(&self) -> bool { - self.engine_driven_pump_1_output + fn engine_driven_pump_2_pressurisation_command(&self) -> PumpPressurisationCommand { + if self.engine_driven_pump_2_should_run { + PumpPressurisationCommand::Pressurise + } else { + PumpPressurisationCommand::Depressurise + } } fn green_edp_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_1_output && !self.green_loop_pressurised_feedback + self.engine_driven_pump_1_pressurisation_command() + .is_pressurise() + && !self.green_loop_pressurised_feedback } fn yellow_edp_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_2_output && !self.yellow_loop_pressurised_feedback + self.engine_driven_pump_2_pressurisation_command() + .is_pressurise() + && !self.yellow_loop_pressurised_feedback } fn yellow_epump_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.yellow_electric_pump_output && !self.yellow_loop_pressurised_feedback + self.yellow_electric_pump_pressurisation_command() + .is_pressurise() + && !self.yellow_loop_pressurised_feedback } fn blue_epump_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.blue_electric_pump_output && !self.blue_loop_pressurised_feedback + self.blue_electric_pump_pressurisation_command() + .is_pressurise() + && !self.blue_loop_pressurised_feedback } fn set_green_pressurised(&mut self, is_pressurised: bool) { @@ -727,7 +740,7 @@ impl A320HydraulicLogic { self.nose_wheel_steering_pin_inserted .update(context, self.pushback_tug.is_pushing()); - self.update_ed_pump_states(overhead_panel, engine_fire_overhead); + self.update_engine_driven_pump_states(overhead_panel, engine_fire_overhead); self.update_e_pump_states(overhead_panel); @@ -735,80 +748,74 @@ impl A320HydraulicLogic { } fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - let ptu_inhibit = self.should_inhibit_ptu_after_cargo_door_operation() - && overhead_panel.yellow_epump_push_button.is_auto(); - if overhead_panel.ptu_push_button.is_auto() + let ptu_inhibited = self.should_inhibit_ptu_after_cargo_door_operation() + && overhead_panel.yellow_epump_push_button_is_auto(); + + self.ptu_should_be_enabled = 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_is_inserted())) - && !ptu_inhibit - { - self.ptu_ena_output = true; - } else { - self.ptu_ena_output = false; - } + && !ptu_inhibited; } - fn update_ed_pump_states( + fn update_engine_driven_pump_states( &mut self, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, ) { - if overhead_panel.edp1_push_button.is_auto() + if overhead_panel.edp1_push_button_is_auto() && self.eng_1_master_on - && !engine_fire_overhead.eng1_fire_pb.is_released() + && !engine_fire_overhead.engine_1_fire_push_button_is_released() + { + self.engine_driven_pump_1_should_run = true; + } else if overhead_panel.edp1_push_button_is_off() + || engine_fire_overhead.engine_1_fire_push_button_is_released() { - self.engine_driven_pump_1_output = true; - } else if overhead_panel.edp1_push_button.is_off() { - self.engine_driven_pump_1_output = false; + self.engine_driven_pump_1_should_run = false; } - //FIRE valves logic for EDP1 - if engine_fire_overhead.eng1_fire_pb.is_released() { - self.engine_driven_pump_1_output = false; - self.green_loop_fire_valve_output = false; + // FIRE valves logic for EDP1 + if engine_fire_overhead.engine_1_fire_push_button_is_released() { + self.green_loop_fire_valve_should_be_open = false; } else { - self.green_loop_fire_valve_output = true; + self.green_loop_fire_valve_should_be_open = true; } - if overhead_panel.edp2_push_button.is_auto() + if overhead_panel.edp2_push_button_is_auto() && self.eng_2_master_on - && !engine_fire_overhead.eng2_fire_pb.is_released() + && !engine_fire_overhead.engine_2_fire_push_button_is_released() { - self.engine_driven_pump_2_output = true; - } else if overhead_panel.edp2_push_button.is_off() { - self.engine_driven_pump_2_output = false; + self.engine_driven_pump_2_should_run = true; + } else if overhead_panel.edp2_push_button_is_off() + || engine_fire_overhead.engine_2_fire_push_button_is_released() + { + self.engine_driven_pump_2_should_run = false; } - //FIRE valves logic for EDP2 - if engine_fire_overhead.eng2_fire_pb.is_released() { - self.engine_driven_pump_2_output = false; - self.yellow_loop_fire_valve_output = false; + // FIRE valves logic for EDP2 + if engine_fire_overhead.engine_2_fire_push_button_is_released() { + self.yellow_loop_fire_valve_should_be_open = false; } else { - self.yellow_loop_fire_valve_output = true; + self.yellow_loop_fire_valve_should_be_open = true; } } fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - if overhead_panel.yellow_epump_push_button.is_on() - || self.should_activate_yellow_pump_for_cargo_door_operation() - { - self.yellow_electric_pump_output = true; - } else if overhead_panel.yellow_epump_push_button.is_auto() { - self.yellow_electric_pump_output = false; - } + self.yellow_electric_pump_should_run = overhead_panel.yellow_epump_push_button.is_on() + || self.should_activate_yellow_pump_for_cargo_door_operation(); + if overhead_panel.blue_epump_push_button.is_auto() { if self.eng_1_master_on || self.eng_2_master_on || overhead_panel.blue_epump_override_push_button.is_on() { - self.blue_electric_pump_output = true; + self.blue_electric_pump_should_run = true; } else { - self.blue_electric_pump_output = false; + self.blue_electric_pump_should_run = false; } } else if overhead_panel.blue_epump_push_button.is_off() { - self.blue_electric_pump_output = false; + self.blue_electric_pump_should_run = false; } } } @@ -837,13 +844,13 @@ impl SimulationElement for A320HydraulicLogic { } pub struct A320HydraulicOverheadPanel { - pub edp1_push_button: AutoOffFaultPushButton, - pub edp2_push_button: AutoOffFaultPushButton, - pub blue_epump_push_button: AutoOffFaultPushButton, - pub ptu_push_button: AutoOffFaultPushButton, - pub rat_push_button: AutoOffFaultPushButton, - pub yellow_epump_push_button: AutoOnFaultPushButton, - pub blue_epump_override_push_button: OnOffFaultPushButton, + 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 fn new() -> A320HydraulicOverheadPanel { @@ -866,6 +873,30 @@ impl A320HydraulicOverheadPanel { self.yellow_epump_push_button .set_fault(hyd.yellow_epump_has_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 edp1_push_button_is_auto(&self) -> bool { + self.edp1_push_button.is_auto() + } + + fn edp1_push_button_is_off(&self) -> bool { + self.edp1_push_button.is_off() + } + + fn edp2_push_button_is_auto(&self) -> bool { + self.edp2_push_button.is_auto() + } + + fn edp2_push_button_is_off(&self) -> bool { + self.edp2_push_button.is_off() + } } impl SimulationElement for A320HydraulicOverheadPanel { fn accept(&mut self, visitor: &mut T) { @@ -882,16 +913,24 @@ impl SimulationElement for A320HydraulicOverheadPanel { } pub struct A320EngineFireOverheadPanel { - pub eng1_fire_pb: FirePushButton, - pub eng2_fire_pb: FirePushButton, + eng1_fire_pb: FirePushButton, + eng2_fire_pb: FirePushButton, } impl A320EngineFireOverheadPanel { - pub fn new() -> A320EngineFireOverheadPanel { - A320EngineFireOverheadPanel { + pub fn new() -> Self { + Self { eng1_fire_pb: FirePushButton::new("ENG1"), eng2_fire_pb: FirePushButton::new("ENG2"), } } + + fn engine_1_fire_push_button_is_released(&self) -> bool { + self.eng1_fire_pb.is_released() + } + + fn engine_2_fire_push_button_is_released(&self) -> bool { + self.eng2_fire_pb.is_released() + } } impl SimulationElement for A320EngineFireOverheadPanel { fn accept(&mut self, visitor: &mut T) { diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 00490739e35..990957f42fc 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -164,15 +164,18 @@ impl PowerTransferUnit { } } - pub fn enabling(&mut self, enable_flag: bool) { - self.is_enabled = enable_flag; + pub fn set_enabled(&mut self, enable: bool) { + self.is_enabled = enable; } } 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.get_flow().get::()); + writer.write_f64( + "HYD_PTU_MOTOR_FLOW", + self.get_flow().get::(), + ); writer.write_bool("HYD_PTU_VALVE_OPENED", self.is_enabled()); } } @@ -337,8 +340,7 @@ impl HydLoop { self.accumulator_gas_pressure = (Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE) * Volume::new::(Self::ACCUMULATOR_MAX_VOLUME)) - / (Volume::new::(Self::ACCUMULATOR_MAX_VOLUME) - - self.accumulator_fluid_volume); + / (Volume::new::(Self::ACCUMULATOR_MAX_VOLUME) - self.accumulator_fluid_volume); } fn update_ptu_flows( @@ -515,6 +517,16 @@ impl SimulationElement for HydLoop { } } +pub enum PumpPressurisationCommand { + Pressurise, + Depressurise, +} +impl PumpPressurisationCommand { + pub fn is_pressurise(&self) -> bool { + matches!(self, PumpPressurisationCommand::Pressurise) + } +} + pub struct Pump { delta_vol_max: Volume, delta_vol_min: Volume, @@ -542,8 +554,8 @@ impl Pump { } } - pub fn set_pressurised_state(&mut self, is_pressurised: bool) { - self.is_pressurised = is_pressurised; + fn command(&mut self, commanded_state: PumpPressurisationCommand) { + self.is_pressurised = commanded_state.is_pressurise(); } fn update(&mut self, delta_time: &Duration, line: &HydLoop, rpm: f64) { @@ -615,12 +627,8 @@ impl ElectricPump { } } - pub fn start(&mut self) { - self.active = true; - } - - pub fn stop(&mut self) { - self.active = false; + pub fn command(&mut self, commanded_state: PumpPressurisationCommand) { + self.active = commanded_state.is_pressurise(); } pub fn get_rpm(&self) -> f64 { @@ -631,11 +639,9 @@ impl ElectricPump { //TODO Simulate speed of pump depending on pump load (flow?/ current?) //Pump startup/shutdown process if self.active && self.rpm < Self::NOMINAL_SPEED { - self.rpm += (Self::NOMINAL_SPEED / Self::SPOOLUP_TIME) - * delta_time.as_secs_f64(); + self.rpm += (Self::NOMINAL_SPEED / Self::SPOOLUP_TIME) * delta_time.as_secs_f64(); } else if !self.active && self.rpm > 0.0 { - self.rpm -= (Self::NOMINAL_SPEED / Self::SPOOLDOWN_TIME) - * delta_time.as_secs_f64(); + self.rpm -= (Self::NOMINAL_SPEED / Self::SPOOLDOWN_TIME) * delta_time.as_secs_f64(); } //Limiting min and max speed @@ -694,21 +700,15 @@ impl EngineDrivenPump { } pub fn update(&mut self, delta_time: &Duration, line: &HydLoop, engine: &Engine) { - let n2_rpm = - engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; + let n2_rpm = engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; let pump_rpm = n2_rpm * Self::PUMP_N2_GEAR_RATIO; self.pump.update(delta_time, line, pump_rpm); } - pub fn start(&mut self) { - self.active = true; - self.pump.set_pressurised_state(true); - } - - pub fn stop(&mut self) { - self.active = false; - self.pump.set_pressurised_state(false); + pub fn command(&mut self, commanded_state: PumpPressurisationCommand) { + self.active = commanded_state.is_pressurise(); + self.pump.command(commanded_state); } pub fn is_active(&self) -> bool { @@ -961,7 +961,7 @@ mod tests { } if x == 200 { assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); - edp1.stop(); + edp1.command(PumpPressurisationCommand::Depressurise); } if x >= 500 { //Shutdown + 30s @@ -1181,12 +1181,12 @@ mod tests { //shut yellow epump, check drop of pressure in both loops fn yellow_green_ptu_loop_simulation() { let mut epump = electric_pump(); - epump.stop(); + epump.command(PumpPressurisationCommand::Depressurise); let mut yellow_loop = hydraulic_loop("YELLOW"); let mut edp1 = engine_driven_pump(); assert!(!edp1.active); //Is off when created? - edp1.stop(); + edp1.command(PumpPressurisationCommand::Depressurise); let mut engine1 = engine(Ratio::new::(0.0)); @@ -1210,7 +1210,7 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); assert!(green_loop.reservoir_volume == green_res_at_start); - epump.start(); + epump.command(PumpPressurisationCommand::Pressurise); } if x == 110 { @@ -1222,7 +1222,7 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); assert!(green_loop.reservoir_volume == green_res_at_start); - ptu.enabling(true); + ptu.set_enabled(true); } if x == 300 { @@ -1237,7 +1237,7 @@ mod tests { println!("------------GREEN EDP1 ON------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); - edp1.start(); + edp1.command(PumpPressurisationCommand::Pressurise); } if (500..=600).contains(&x) { @@ -1253,8 +1253,8 @@ mod tests { println!("-------------ALL PUMPS OFF------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - edp1.stop(); - epump.stop(); + edp1.command(PumpPressurisationCommand::Depressurise); + epump.command(PumpPressurisationCommand::Depressurise); } if x == 800 { @@ -1415,7 +1415,7 @@ mod tests { let dummy_update = Duration::from_secs(1); let mut line = hydraulic_loop("GREEN"); - edp.start(); + edp.command(PumpPressurisationCommand::Pressurise); line.loop_pressure = pressure; edp.update(&dummy_update, &line, &eng); //Update 10 times to stabilize displacement From a916bf8f550d667ecafd802b385ed93f419a17f7 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Sat, 3 Apr 2021 20:34:52 +0200 Subject: [PATCH 065/122] refactor: remove A320HydraulicLogic --- src/systems/a320_systems/src/hydraulic.rs | 1083 +++++++++-------- src/systems/a320_systems/src/lib.rs | 2 +- src/systems/a320_systems_wasm/src/lib.rs | 12 +- .../systems/src/hydraulic/brakecircuit.rs | 14 +- src/systems/systems/src/hydraulic/mod.rs | 351 ++++-- 5 files changed, 846 insertions(+), 616 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 8fb4bc9a226..1120aba63dd 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,8 +1,11 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; +use systems::engine::Engine; use systems::hydraulic::{ - ElectricPump, EngineDrivenPump, HydFluid, HydLoop, PowerTransferUnit, RamAirTurbine, + ElectricPump, EngineDrivenPump, HydFluid, HydraulicLoop, HydraulicLoopController, + PowerTransferUnit, PowerTransferUnitController, PumpController, RamAirTurbine, + RamAirTurbineController, }; use systems::overhead::{ AutoOffFaultPushButton, AutoOnFaultPushButton, FirePushButton, OnOffFaultPushButton, @@ -10,21 +13,39 @@ use systems::overhead::{ use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; -use systems::{engine::Engine, hydraulic::PumpPressurisationCommand}; use systems::{hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate}; -pub struct A320Hydraulic { - hyd_logic: A320HydraulicLogic, +pub(super) struct A320Hydraulic { hyd_brake_logic: A320HydraulicBrakingLogic, - blue_loop: HydLoop, - green_loop: HydLoop, - yellow_loop: HydLoop, + 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: EngineDrivenPump, + engine_driven_pump_1_controller: A320EngineDrivenPumpController, + 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, - ptu: PowerTransferUnit, + 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, @@ -41,12 +62,11 @@ impl A320Hydraulic { const HYDRAULIC_SIM_TIME_STEP: u64 = 100; //refresh rate of hydraulic simulation in ms const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; //refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update - pub fn new() -> A320Hydraulic { + pub(super) fn new() -> A320Hydraulic { A320Hydraulic { - hyd_logic: A320HydraulicLogic::new(), hyd_brake_logic: A320HydraulicBrakingLogic::new(), - blue_loop: HydLoop::new( + blue_loop: HydraulicLoop::new( "BLUE", false, false, @@ -57,7 +77,8 @@ impl A320Hydraulic { HydFluid::new(Pressure::new::(1450000000.0)), false, ), - green_loop: HydLoop::new( + blue_loop_controller: A320HydraulicLoopController::new(None), + green_loop: HydraulicLoop::new( "GREEN", true, false, @@ -68,7 +89,8 @@ impl A320Hydraulic { HydFluid::new(Pressure::new::(1450000000.0)), true, ), - yellow_loop: HydLoop::new( + green_loop_controller: A320HydraulicLoopController::new(Some(1)), + yellow_loop: HydraulicLoop::new( "YELLOW", false, true, @@ -79,12 +101,29 @@ impl A320Hydraulic { HydFluid::new(Pressure::new::(1450000000.0)), true, ), + yellow_loop_controller: A320HydraulicLoopController::new(Some(2)), + engine_driven_pump_1: EngineDrivenPump::new("GREEN"), + engine_driven_pump_1_controller: A320EngineDrivenPumpController::new(1), + 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(), - ptu: PowerTransferUnit::new(), + ram_air_turbine_controller: A320RamAirTurbineController::new(), + + power_transfer_unit: PowerTransferUnit::new(), + power_transfer_unit_controller: A320PowerTransferUnitController::new(), braking_circuit_norm: BrakeCircuit::new( "NORM", @@ -109,100 +148,7 @@ impl A320Hydraulic { } } - pub fn green_edp_has_fault(&self) -> bool { - self.hyd_logic.green_edp_has_fault() - } - - pub fn yellow_epump_has_fault(&self) -> bool { - self.hyd_logic.yellow_epump_has_fault() - } - - pub fn yellow_edp_has_fault(&self) -> bool { - self.hyd_logic.yellow_edp_has_fault() - } - - pub fn blue_epump_has_fault(&self) -> bool { - self.hyd_logic.blue_epump_has_fault() - } - - // Updates pressure available state based on pressure switches - fn update_hyd_avail_states(&mut self) { - if self.green_loop.get_pressure() - <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) - { - self.is_green_pressurised = false; - } else if self.green_loop.get_pressure() - >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) - { - self.is_green_pressurised = true; - } - - if self.blue_loop.get_pressure() - <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) - { - self.is_blue_pressurised = false; - } else if self.blue_loop.get_pressure() - >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) - { - self.is_blue_pressurised = true; - } - - if self.yellow_loop.get_pressure() - <= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_LO_HYST) - { - self.is_yellow_pressurised = false; - } else if self.yellow_loop.get_pressure() - >= Pressure::new::(A320Hydraulic::MIN_PRESS_PRESSURISED_HI_HYST) - { - self.is_yellow_pressurised = true; - } - } - - fn set_hydraulics_logic_feedbacks(&mut self) { - self.hyd_logic - .set_green_pressurised(self.is_green_pressurised()); - self.hyd_logic - .set_blue_pressurised(self.is_blue_pressurised()); - self.hyd_logic - .set_yellow_pressurised(self.is_yellow_pressurised()); - } - - fn update_hydraulics_logic_outputs(&mut self, context: &UpdateContext) { - self.ptu.set_enabled(self.hyd_logic.ptu_should_be_enabled()); - - if self.hyd_logic.rat_should_be_deployed(context) { - self.ram_air_turbine.deploy(); - } - - self.blue_electric_pump - .command(self.hyd_logic.blue_electric_pump_pressurisation_command()); - self.yellow_electric_pump - .command(self.hyd_logic.yellow_electric_pump_pressurisation_command()); - - self.yellow_loop - .set_fire_shutoff_valve_state(self.hyd_logic.yellow_loop_fire_valve_should_be_open()); - self.green_loop - .set_fire_shutoff_valve_state(self.hyd_logic.green_loop_fire_valve_should_be_open()); - - self.engine_driven_pump_1 - .command(self.hyd_logic.engine_driven_pump_1_pressurisation_command()); - self.engine_driven_pump_2 - .command(self.hyd_logic.engine_driven_pump_2_pressurisation_command()); - } - - pub fn is_blue_pressurised(&self) -> bool { - self.is_blue_pressurised - } - - pub fn is_green_pressurised(&self) -> bool { - self.is_green_pressurised - } - - pub fn is_yellow_pressurised(&self) -> bool { - self.is_yellow_pressurised - } - - pub fn update( + pub(super) fn update( &mut self, context: &UpdateContext, engine1: &Engine, @@ -210,7 +156,7 @@ impl A320Hydraulic { overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, ) { - let min_hyd_loop_timestep = Duration::from_millis(A320Hydraulic::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz + let min_hyd_loop_timestep = Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz self.total_sim_time_elapsed += context.delta(); @@ -259,15 +205,97 @@ impl A320Hydraulic { // 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 * A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; - let delta_time_physics = - min_hyd_loop_timestep / A320Hydraulic::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X + num_of_update_loops * Self::ACTUATORS_SIM_TIME_STEP_MULT; + let delta_time_physics = min_hyd_loop_timestep / Self::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X for _ in 0..num_of_actuators_update_loops { self.update_fast_rate(&context, &delta_time_physics); } } } + fn green_edp_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.engine_driven_pump_1_controller.should_pressurise() && !self.is_green_pressurised + } + + fn yellow_epump_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.yellow_electric_pump_controller.should_pressurise() && !self.is_yellow_pressurised + } + + fn yellow_edp_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.engine_driven_pump_2_controller.should_pressurise() && !self.is_yellow_pressurised + } + + fn blue_epump_has_fault(&self) -> bool { + // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop + // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong + self.blue_electric_pump_controller.should_pressurise() && !self.is_blue_pressurised + } + + #[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() + } + + // Updates pressure available state based on pressure switches + fn update_hyd_avail_states(&mut self) { + if self.green_loop.get_pressure() + <= Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST) + { + self.is_green_pressurised = false; + } else if self.green_loop.get_pressure() + >= Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST) + { + self.is_green_pressurised = true; + } + + if self.blue_loop.get_pressure() + <= Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST) + { + self.is_blue_pressurised = false; + } else if self.blue_loop.get_pressure() + >= Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST) + { + self.is_blue_pressurised = true; + } + + if self.yellow_loop.get_pressure() + <= Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST) + { + self.is_yellow_pressurised = false; + } else if self.yellow_loop.get_pressure() + >= Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST) + { + self.is_yellow_pressurised = true; + } + } + + pub(super) fn is_blue_pressurised(&self) -> bool { + self.is_blue_pressurised + } + + #[cfg(test)] + fn is_green_pressurised(&self) -> bool { + self.is_green_pressurised + } + + #[cfg(test)] + fn is_yellow_pressurised(&self) -> bool { + self.is_yellow_pressurised + } + // All the higher frequency updates like physics fn update_fast_rate(&mut self, context: &UpdateContext, delta_time_physics: &Duration) { self.ram_air_turbine @@ -285,15 +313,6 @@ impl A320Hydraulic { min_hyd_loop_timestep: Duration, ) { self.update_hyd_avail_states(); - self.set_hydraulics_logic_feedbacks(); - - // Base logic update based on overhead Could be done only once (before that loop) but if so delta time should be set accordingly - self.hyd_logic.update( - overhead_panel, - engine_fire_overhead, - &context.with_delta(min_hyd_loop_timestep), - ); - self.update_hydraulics_logic_outputs(&context.with_delta(min_hyd_loop_timestep)); // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic @@ -303,38 +322,92 @@ impl A320Hydraulic { &mut self.braking_circuit_altn, ); - self.ptu.update(&self.green_loop, &self.yellow_loop); - self.engine_driven_pump_1 - .update(&min_hyd_loop_timestep, &self.green_loop, &engine1); - self.engine_driven_pump_2 - .update(&min_hyd_loop_timestep, &self.yellow_loop, &engine2); - self.yellow_electric_pump - .update(&min_hyd_loop_timestep, &self.yellow_loop); - self.blue_electric_pump - .update(&min_hyd_loop_timestep, &self.blue_loop); + 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.ram_air_turbine - .update(&min_hyd_loop_timestep, &self.blue_loop); + self.engine_driven_pump_1_controller + .update(overhead_panel, engine_fire_overhead); + self.engine_driven_pump_1.update( + &min_hyd_loop_timestep, + &self.green_loop, + &engine1, + &self.engine_driven_pump_1_controller, + ); + + self.engine_driven_pump_2_controller + .update(overhead_panel, engine_fire_overhead); + self.engine_driven_pump_2.update( + &min_hyd_loop_timestep, + &self.yellow_loop, + &engine2, + &self.engine_driven_pump_2_controller, + ); + + self.blue_electric_pump_controller.update(overhead_panel); + self.blue_electric_pump.update( + &min_hyd_loop_timestep, + &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_electric_pump.update( + &min_hyd_loop_timestep, + &self.yellow_loop, + &self.yellow_electric_pump_controller, + ); + + self.ram_air_turbine_controller + .update(&context.with_delta(min_hyd_loop_timestep)); + self.ram_air_turbine.update( + &min_hyd_loop_timestep, + &self.blue_loop, + &self.ram_air_turbine_controller, + ); + + self.green_loop_controller.update(engine_fire_overhead); self.green_loop.update( &min_hyd_loop_timestep, Vec::new(), vec![&self.engine_driven_pump_1], Vec::new(), - vec![&self.ptu], + vec![&self.power_transfer_unit], + &self.green_loop_controller, ); + + self.yellow_loop_controller.update(engine_fire_overhead); self.yellow_loop.update( &min_hyd_loop_timestep, vec![&self.yellow_electric_pump], vec![&self.engine_driven_pump_2], Vec::new(), - vec![&self.ptu], + vec![&self.power_transfer_unit], + &self.yellow_loop_controller, ); + + self.blue_loop_controller.update(engine_fire_overhead); self.blue_loop.update( &min_hyd_loop_timestep, vec![&self.blue_electric_pump], Vec::new(), vec![&self.ram_air_turbine], Vec::new(), + &self.blue_loop_controller, ); self.braking_circuit_norm @@ -344,29 +417,328 @@ impl A320Hydraulic { self.braking_circuit_norm.reset_accumulators(); self.braking_circuit_altn.reset_accumulators(); } -} +} +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.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); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.green_edp_has_fault()); + writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.blue_epump_has_fault()); + writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.yellow_edp_has_fault()); + writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.yellow_epump_has_fault()); + } +} + +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, + should_pressurise: bool, +} +impl A320EngineDrivenPumpController { + fn new(engine_number: usize) -> Self { + Self { + engine_number, + engine_master_on_id: format!("GENERAL ENG STARTER ACTIVE:{}", engine_number), + engine_master_on: false, + should_pressurise: false, + } + } + + fn update( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + engine_fire_overhead: &A320EngineFireOverheadPanel, + ) { + if overhead_panel.edp_push_button_is_auto(self.engine_number) + && self.engine_master_on + && !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; + } + } +} +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); + } +} + +struct A320BlueElectricPumpController { + should_pressurise: bool, + engine_1_master_on: bool, + engine_2_master_on: bool, +} +impl A320BlueElectricPumpController { + fn new() -> Self { + Self { + should_pressurise: false, + engine_1_master_on: false, + engine_2_master_on: false, + } + } + + fn update(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + if overhead_panel.blue_epump_push_button.is_auto() { + if self.engine_1_master_on + || self.engine_2_master_on + || 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; + } + } +} +impl PumpController for A320BlueElectricPumpController { + fn should_pressurise(&self) -> bool { + self.should_pressurise + } +} +impl SimulationElement for A320BlueElectricPumpController { + fn read(&mut self, state: &mut SimulatorReader) { + self.engine_1_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:1"); + self.engine_2_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:2"); + } +} +impl Default for A320BlueElectricPumpController { + fn default() -> Self { + Self::new() + } +} + +struct A320YellowElectricPumpController { + should_pressurise: 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, + 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, + ) { + 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(); + } + + #[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 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, + } + } -impl SimulationElement for A320Hydraulic { - fn accept(&mut self, visitor: &mut T) { - self.yellow_electric_pump.accept(visitor); - self.blue_electric_pump.accept(visitor); - self.engine_driven_pump_1.accept(visitor); - self.engine_driven_pump_2.accept(visitor); - self.ram_air_turbine.accept(visitor); + 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_pushing()); - self.ptu.accept(visitor); + let ptu_inhibited = self.should_inhibit_ptu_after_cargo_door_operation.output() + && overhead_panel.yellow_epump_push_button_is_auto(); - self.blue_loop.accept(visitor); - self.green_loop.accept(visitor); - self.yellow_loop.accept(visitor); + 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; + } - self.hyd_logic.accept(visitor); - self.hyd_brake_logic.accept(visitor); + #[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"); + } +} - self.braking_circuit_norm.accept(visitor); - self.braking_circuit_altn.accept(visitor); +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, + } + } - visitor.visit(self); + 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"); } } @@ -405,7 +777,11 @@ impl A320HydraulicBrakingLogic { //Updates final brake demands per hydraulic loop based on pilot pedal demands //TODO: think about where to build those brake demands from autobrake if not from brake pedals - pub fn update_brake_demands(&mut self, delta_time_update: &Duration, green_loop: &HydLoop) { + pub fn update_brake_demands( + &mut self, + delta_time_update: &Duration, + green_loop: &HydraulicLoop, + ) { let green_used_for_brakes = green_loop.get_pressure() //TODO Check this logic > Pressure::new::(A320HydraulicBrakingLogic::MIN_PRESSURE_BRAKE_ALTN ) && self.anti_skid_activated @@ -493,7 +869,7 @@ struct Door { impl Door { fn new(id: usize) -> Self { Self { - exit_id: format!("EXIT OPEN {}", id), + exit_id: format!("EXIT OPEN:{}", id), position: 0., previous_position: 0., } @@ -545,305 +921,7 @@ impl SimulationElement for PushbackTug { } } -pub struct A320HydraulicLogic { - forward_cargo_door: Door, - aft_cargo_door: Door, - pushback_tug: PushbackTug, - should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate, - should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate, - nose_wheel_steering_pin_inserted: DelayedFalseLogicGate, - - parking_brake_lever_pos: bool, - weight_on_wheels: bool, - eng_1_master_on: bool, - eng_2_master_on: bool, - - ptu_should_be_enabled: bool, - blue_electric_pump_should_run: bool, - yellow_electric_pump_should_run: bool, - green_loop_fire_valve_should_be_open: bool, - yellow_loop_fire_valve_should_be_open: bool, - engine_driven_pump_2_should_run: bool, - engine_driven_pump_1_should_run: bool, - - green_loop_pressurised_feedback: bool, - blue_loop_pressurised_feedback: bool, - yellow_loop_pressurised_feedback: bool, -} -// Implements low level logic for all hydraulics commands -// takes inputs from hydraulics and from plane systems, update outputs for pumps and all other hyd systems -impl A320HydraulicLogic { - const DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION: Duration = - Duration::from_secs(20); - 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); - - pub fn new() -> A320HydraulicLogic { - A320HydraulicLogic { - forward_cargo_door: Door::new(5), - aft_cargo_door: Door::new(3), - pushback_tug: PushbackTug::new(), - should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate::new( - A320HydraulicLogic::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, - ), - should_inhibit_ptu_after_cargo_door_operation: DelayedFalseLogicGate::new( - A320HydraulicLogic::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, - ), - nose_wheel_steering_pin_inserted: DelayedFalseLogicGate::new( - A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, - ), - - parking_brake_lever_pos: true, - weight_on_wheels: true, - eng_1_master_on: false, - eng_2_master_on: false, - - ptu_should_be_enabled: false, - blue_electric_pump_should_run: false, - yellow_electric_pump_should_run: false, - green_loop_fire_valve_should_be_open: false, - yellow_loop_fire_valve_should_be_open: false, - engine_driven_pump_2_should_run: true, - engine_driven_pump_1_should_run: true, - - green_loop_pressurised_feedback: false, - blue_loop_pressurised_feedback: false, - yellow_loop_pressurised_feedback: false, - } - } - - fn ptu_should_be_enabled(&self) -> bool { - self.ptu_should_be_enabled - } - - fn rat_should_be_deployed(&self, context: &UpdateContext) -> bool { - // RAT Deployment - // Todo check all other needed conditions this is faked with engine master while it should check elec buses - !self.eng_1_master_on - && !self.eng_2_master_on - //Todo get speed from ADIRS - && context.indicated_airspeed() > Velocity::new::(100.) - } - - fn blue_electric_pump_pressurisation_command(&self) -> PumpPressurisationCommand { - if self.blue_electric_pump_should_run { - PumpPressurisationCommand::Pressurise - } else { - PumpPressurisationCommand::Depressurise - } - } - - fn yellow_electric_pump_pressurisation_command(&self) -> PumpPressurisationCommand { - if self.yellow_electric_pump_should_run { - PumpPressurisationCommand::Pressurise - } else { - PumpPressurisationCommand::Depressurise - } - } - - fn green_loop_fire_valve_should_be_open(&self) -> bool { - self.green_loop_fire_valve_should_be_open - } - - fn yellow_loop_fire_valve_should_be_open(&self) -> bool { - self.yellow_loop_fire_valve_should_be_open - } - - fn engine_driven_pump_1_pressurisation_command(&self) -> PumpPressurisationCommand { - if self.engine_driven_pump_1_should_run { - PumpPressurisationCommand::Pressurise - } else { - PumpPressurisationCommand::Depressurise - } - } - - fn engine_driven_pump_2_pressurisation_command(&self) -> PumpPressurisationCommand { - if self.engine_driven_pump_2_should_run { - PumpPressurisationCommand::Pressurise - } else { - PumpPressurisationCommand::Depressurise - } - } - - fn green_edp_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_1_pressurisation_command() - .is_pressurise() - && !self.green_loop_pressurised_feedback - } - - fn yellow_edp_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_2_pressurisation_command() - .is_pressurise() - && !self.yellow_loop_pressurised_feedback - } - - fn yellow_epump_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.yellow_electric_pump_pressurisation_command() - .is_pressurise() - && !self.yellow_loop_pressurised_feedback - } - - fn blue_epump_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.blue_electric_pump_pressurisation_command() - .is_pressurise() - && !self.blue_loop_pressurised_feedback - } - - fn set_green_pressurised(&mut self, is_pressurised: bool) { - self.green_loop_pressurised_feedback = is_pressurised; - } - - fn set_blue_pressurised(&mut self, is_pressurised: bool) { - self.blue_loop_pressurised_feedback = is_pressurised; - } - - fn set_yellow_pressurised(&mut self, is_pressurised: bool) { - self.yellow_loop_pressurised_feedback = is_pressurised; - } - - fn should_activate_yellow_pump_for_cargo_door_operation(&self) -> bool { - self.should_activate_yellow_pump_for_cargo_door_operation - .output() - } - - fn should_inhibit_ptu_after_cargo_door_operation(&self) -> bool { - self.should_inhibit_ptu_after_cargo_door_operation.output() - } - - fn nose_wheel_steering_pin_is_inserted(&self) -> bool { - self.nose_wheel_steering_pin_inserted.output() - } - - fn any_cargo_door_has_moved(&self) -> bool { - self.forward_cargo_door.has_moved() || self.aft_cargo_door.has_moved() - } - - fn update( - &mut self, - overhead_panel: &A320HydraulicOverheadPanel, - engine_fire_overhead: &A320EngineFireOverheadPanel, - context: &UpdateContext, - ) { - self.should_activate_yellow_pump_for_cargo_door_operation - .update(context, self.any_cargo_door_has_moved()); - self.should_inhibit_ptu_after_cargo_door_operation - .update(context, self.any_cargo_door_has_moved()); - self.nose_wheel_steering_pin_inserted - .update(context, self.pushback_tug.is_pushing()); - - self.update_engine_driven_pump_states(overhead_panel, engine_fire_overhead); - - self.update_e_pump_states(overhead_panel); - - self.update_ptu_logic(overhead_panel); - } - - fn update_ptu_logic(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - let ptu_inhibited = self.should_inhibit_ptu_after_cargo_door_operation() - && overhead_panel.yellow_epump_push_button_is_auto(); - - self.ptu_should_be_enabled = 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_is_inserted())) - && !ptu_inhibited; - } - - fn update_engine_driven_pump_states( - &mut self, - overhead_panel: &A320HydraulicOverheadPanel, - engine_fire_overhead: &A320EngineFireOverheadPanel, - ) { - if overhead_panel.edp1_push_button_is_auto() - && self.eng_1_master_on - && !engine_fire_overhead.engine_1_fire_push_button_is_released() - { - self.engine_driven_pump_1_should_run = true; - } else if overhead_panel.edp1_push_button_is_off() - || engine_fire_overhead.engine_1_fire_push_button_is_released() - { - self.engine_driven_pump_1_should_run = false; - } - - // FIRE valves logic for EDP1 - if engine_fire_overhead.engine_1_fire_push_button_is_released() { - self.green_loop_fire_valve_should_be_open = false; - } else { - self.green_loop_fire_valve_should_be_open = true; - } - - if overhead_panel.edp2_push_button_is_auto() - && self.eng_2_master_on - && !engine_fire_overhead.engine_2_fire_push_button_is_released() - { - self.engine_driven_pump_2_should_run = true; - } else if overhead_panel.edp2_push_button_is_off() - || engine_fire_overhead.engine_2_fire_push_button_is_released() - { - self.engine_driven_pump_2_should_run = false; - } - - // FIRE valves logic for EDP2 - if engine_fire_overhead.engine_2_fire_push_button_is_released() { - self.yellow_loop_fire_valve_should_be_open = false; - } else { - self.yellow_loop_fire_valve_should_be_open = true; - } - } - - fn update_e_pump_states(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { - self.yellow_electric_pump_should_run = overhead_panel.yellow_epump_push_button.is_on() - || self.should_activate_yellow_pump_for_cargo_door_operation(); - - if overhead_panel.blue_epump_push_button.is_auto() { - if self.eng_1_master_on - || self.eng_2_master_on - || overhead_panel.blue_epump_override_push_button.is_on() - { - self.blue_electric_pump_should_run = true; - } else { - self.blue_electric_pump_should_run = false; - } - } else if overhead_panel.blue_epump_push_button.is_off() { - self.blue_electric_pump_should_run = false; - } - } -} -impl SimulationElement for A320HydraulicLogic { - fn accept(&mut self, visitor: &mut T) { - self.forward_cargo_door.accept(visitor); - self.aft_cargo_door.accept(visitor); - self.pushback_tug.accept(visitor); - - visitor.visit(self); - } - - 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 ENG1 STARTER ACTIVE"); - self.eng_2_master_on = state.read_bool("GENERAL ENG2 STARTER ACTIVE"); - self.weight_on_wheels = state.read_bool("SIM ON GROUND"); - } - - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.green_edp_has_fault()); - writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.blue_epump_has_fault()); - writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.yellow_edp_has_fault()); - writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.yellow_epump_has_fault()); - } -} - -pub struct A320HydraulicOverheadPanel { +pub(super) struct A320HydraulicOverheadPanel { edp1_push_button: AutoOffFaultPushButton, edp2_push_button: AutoOffFaultPushButton, blue_epump_push_button: AutoOffFaultPushButton, @@ -853,7 +931,7 @@ pub struct A320HydraulicOverheadPanel { blue_epump_override_push_button: OnOffFaultPushButton, } impl A320HydraulicOverheadPanel { - pub fn new() -> 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"), @@ -865,7 +943,7 @@ impl A320HydraulicOverheadPanel { } } - pub fn update_pb_faults(&mut self, hyd: &A320Hydraulic) { + pub(super) fn update(&mut self, hyd: &A320Hydraulic) { self.edp1_push_button.set_fault(hyd.green_edp_has_fault()); self.edp2_push_button.set_fault(hyd.yellow_edp_has_fault()); self.blue_epump_push_button @@ -882,20 +960,28 @@ impl A320HydraulicOverheadPanel { self.ptu_push_button.is_auto() } - fn edp1_push_button_is_auto(&self) -> bool { - self.edp1_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 edp1_push_button_is_off(&self) -> bool { - self.edp1_push_button.is_off() + 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 edp2_push_button_is_auto(&self) -> bool { - self.edp2_push_button.is_auto() + fn blue_epump_override_push_button_is_on(&self) -> bool { + self.blue_epump_override_push_button.is_on() } - fn edp2_push_button_is_off(&self) -> bool { - self.edp2_push_button.is_off() + fn blue_epump_push_button_is_off(&self) -> bool { + self.blue_epump_push_button.is_off() } } impl SimulationElement for A320HydraulicOverheadPanel { @@ -924,12 +1010,12 @@ impl A320EngineFireOverheadPanel { } } - fn engine_1_fire_push_button_is_released(&self) -> bool { - self.eng1_fire_pb.is_released() - } - - fn engine_2_fire_push_button_is_released(&self) -> bool { - self.eng2_fire_pb.is_released() + 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 { @@ -941,24 +1027,6 @@ impl SimulationElement for A320EngineFireOverheadPanel { } } -#[cfg(test)] -mod a320_hydraulic_simvars { - use super::*; - use systems::simulation::test::SimulationTestBed; - - #[test] - fn writes_its_state() { - let mut hyd_logic = A320HydraulicLogic::new(); - let mut test_bed = SimulationTestBed::new(); - test_bed.run_without_update(&mut hyd_logic); - - assert!(test_bed.contains_key("HYD_GREEN_EDPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("HYD_BLUE_EPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("HYD_YELLOW_EDPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); - } -} - #[cfg(test)] mod tests { use super::*; @@ -988,19 +1056,16 @@ mod tests { } fn is_nws_pin_inserted(&self) -> bool { - self.hydraulics - .hyd_logic - .nose_wheel_steering_pin_is_inserted() + self.hydraulics.nose_wheel_steering_pin_is_inserted() } fn is_cargo_powering_yellow_epump(&self) -> bool { self.hydraulics - .hyd_logic - .should_activate_yellow_pump_for_cargo_door_operation() + .should_pressurise_yellow_pump_for_cargo_door_operation() } - fn is_ptu_ena(&self) -> bool { - self.hydraulics.ptu.is_enabled() + fn is_ptu_enabled(&self) -> bool { + self.hydraulics.power_transfer_unit.is_enabled() } fn is_blue_pressurised(&self) -> bool { @@ -1033,7 +1098,7 @@ mod tests { &self.engine_fire_overhead, ); - self.overhead.update_pb_faults(&self.hydraulics); + self.overhead.update(&self.hydraulics); } } impl SimulationElement for A320HydraulicsTestAircraft { @@ -1072,7 +1137,7 @@ mod tests { } fn is_ptu_enabled(&self) -> bool { - self.aircraft.is_ptu_ena() + self.aircraft.is_ptu_enabled() } fn is_blue_pressurised(&self) -> bool { @@ -1187,7 +1252,7 @@ mod tests { } fn set_cargo_door_state(mut self, position: f64) -> Self { - self.simulation_test_bed.write_f64("EXIT OPEN 5", position); + self.simulation_test_bed.write_f64("EXIT OPEN:5", position); self } @@ -1206,7 +1271,7 @@ mod tests { fn start_eng1(mut self, n2: Ratio) -> Self { self.simulation_test_bed - .write_bool("GENERAL ENG1 STARTER ACTIVE", true); + .write_bool("GENERAL ENG STARTER ACTIVE:1", true); self.aircraft.set_engine_1_n2(n2); self @@ -1214,7 +1279,7 @@ mod tests { fn start_eng2(mut self, n2: Ratio) -> Self { self.simulation_test_bed - .write_bool("GENERAL ENG2 STARTER ACTIVE", true); + .write_bool("GENERAL ENG STARTER ACTIVE:2", true); self.aircraft.set_engine_2_n2(n2); self @@ -1222,7 +1287,7 @@ mod tests { fn stop_eng1(mut self) -> Self { self.simulation_test_bed - .write_bool("GENERAL ENG1 STARTER ACTIVE", false); + .write_bool("GENERAL ENG STARTER ACTIVE:1", false); self.aircraft.set_engine_1_n2(Ratio::new::(0.)); self @@ -1230,7 +1295,7 @@ mod tests { fn stop_eng2(mut self) -> Self { self.simulation_test_bed - .write_bool("GENERAL ENG2 STARTER ACTIVE", false); + .write_bool("GENERAL ENG STARTER ACTIVE:2", false); self.aircraft.set_engine_2_n2(Ratio::new::(0.)); self @@ -1405,7 +1470,7 @@ mod tests { test_bed = test_bed.run_waiting_for(Duration::from_secs(1)); assert!(!test_bed.is_ptu_enabled()); test_bed = test_bed.run_waiting_for( - A320HydraulicLogic::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, + A320PowerTransferUnitController::DURATION_OF_PTU_INHIBIT_AFTER_CARGO_DOOR_OPERATION, ); //Should re enabled after 40s assert!(test_bed.is_ptu_enabled()); } @@ -1429,7 +1494,7 @@ mod tests { assert!(test_bed.aircraft.is_nws_pin_inserted()); test_bed = test_bed.set_pushback_state(false).run_waiting_for( - A320HydraulicLogic::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, + A320PowerTransferUnitController::DURATION_AFTER_WHICH_NWS_PIN_IS_REMOVED_AFTER_PUSHBACK, ); assert!(!test_bed.aircraft.is_nws_pin_inserted()); @@ -1452,7 +1517,7 @@ mod tests { assert!(test_bed.aircraft.is_cargo_powering_yellow_epump()); test_bed = test_bed.run_waiting_for( - A320HydraulicLogic::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, + A320YellowElectricPumpController::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, ); assert!(!test_bed.aircraft.is_cargo_powering_yellow_epump()); @@ -1865,5 +1930,17 @@ mod tests { assert!(test_bed.get_brake_left_yellow_pressure() > Pressure::new::(950.)); assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); } + + #[test] + fn writes_its_state() { + let mut hyd_logic = A320Hydraulic::new(); + let mut test_bed = SimulationTestBed::new(); + test_bed.run_without_update(&mut hyd_logic); + + assert!(test_bed.contains_key("HYD_GREEN_EDPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("HYD_BLUE_EPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("HYD_YELLOW_EDPUMP_LOW_PRESS")); + assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); + } } } diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 6b7ff20cb67..097cd63bb6d 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -112,7 +112,7 @@ impl Aircraft for A320 { &self.engine_fire_overhead, ); - self.hydraulic_overhead.update_pb_faults(&self.hydraulic); + self.hydraulic_overhead.update(&self.hydraulic); self.power_consumption.update(context); } diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index 4d0ee90fff6..61dc96b6f3f 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -88,8 +88,8 @@ impl A320SimulatorReaderWriter { 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)?, - cargo_door_back_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, //TODO It is the catering door for now + cargo_door_forward_pos: AircraftVariable::from("EXIT OPEN", "Percent", 5)?, + cargo_door_aft_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, //TODO It is the catering door for now 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)?, @@ -121,11 +121,11 @@ 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 ENG1 STARTER ACTIVE" => self.master_eng_1.get(), - "GENERAL ENG2 STARTER ACTIVE" => self.master_eng_2.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(), + "EXIT OPEN:5" => self.cargo_door_forward_pos.get(), + "EXIT OPEN:3" => self.cargo_door_aft_pos.get(), "PUSHBACK ANGLE" => self.pushback_angle.get(), "PUSHBACK STATE" => self.pushback_state.get(), "ANTISKID BRAKES ACTIVE" => self.anti_skid_activated.get(), diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 9811b0c93c3..e616dae4fac 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -1,5 +1,5 @@ use crate::{ - hydraulic::HydLoop, + hydraulic::HydraulicLoop, simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext}, }; @@ -137,7 +137,7 @@ impl BrakeCircuit { } } - pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydLoop) { + pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydraulicLoop) { let delta_vol = ((self.demanded_brake_position_left - self.current_brake_position_left) + (self.demanded_brake_position_right - self.current_brake_position_right)) * self.total_displacement; @@ -336,7 +336,7 @@ impl AutoBrakeController { mod tests { use super::*; use crate::{ - hydraulic::{HydFluid, HydLoop}, + hydraulic::{HydFluid, HydraulicLoop}, simulation::UpdateContext, }; use uom::si::{ @@ -487,9 +487,9 @@ mod tests { assert!(controller.get_brake_command() >= 0.0); } - fn hydraulic_loop(loop_color: &str) -> HydLoop { + fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { match loop_color { - "GREEN" => HydLoop::new( + "GREEN" => HydraulicLoop::new( loop_color, false, true, @@ -500,7 +500,7 @@ mod tests { HydFluid::new(Pressure::new::(1450000000.0)), true, ), - "YELLOW" => HydLoop::new( + "YELLOW" => HydraulicLoop::new( loop_color, true, false, @@ -511,7 +511,7 @@ mod tests { HydFluid::new(Pressure::new::(1450000000.0)), true, ), - _ => HydLoop::new( + _ => HydraulicLoop::new( loop_color, false, false, diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 990957f42fc..25fbc63ce83 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -50,6 +50,10 @@ impl HydFluid { } } +pub trait PowerTransferUnitController { + fn should_enable(&self) -> bool; +} + //TODO enhance simulation with RPM and variable displacement on one side? pub struct PowerTransferUnit { is_enabled: bool, @@ -100,7 +104,14 @@ impl PowerTransferUnit { self.is_active_right } - pub fn update(&mut self, loop_left: &HydLoop, loop_right: &HydLoop) { + pub fn update( + &mut self, + loop_left: &HydraulicLoop, + loop_right: &HydraulicLoop, + controller: &T, + ) { + self.is_enabled = controller.should_enable(); + let delta_p = loop_left.get_pressure() - loop_right.get_pressure(); //TODO: use maped characteristics for PTU? @@ -163,10 +174,6 @@ impl PowerTransferUnit { self.last_flow = VolumeRate::new::(0.0); } } - - pub fn set_enabled(&mut self, enable: bool) { - self.is_enabled = enable; - } } impl SimulationElement for PowerTransferUnit { fn write(&self, writer: &mut SimulatorWriter) { @@ -185,7 +192,11 @@ impl Default for PowerTransferUnit { } } -pub struct HydLoop { +pub trait HydraulicLoopController { + fn should_open_fire_shutoff_valve(&self) -> bool; +} + +pub struct HydraulicLoop { pressure_id: String, reservoir_id: String, fire_valve_id: String, @@ -209,7 +220,7 @@ pub struct HydLoop { fire_shutoff_valve_opened: bool, has_fire_valve: bool, } -impl HydLoop { +impl HydraulicLoop { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI const ACCUMULATOR_MAX_VOLUME: f64 = 0.264; // in gallons @@ -303,10 +314,6 @@ impl HydLoop { / self.fluid.get_bulk_mod() } - pub fn set_fire_shutoff_valve_state(&mut self, opened: bool) { - self.fire_shutoff_valve_opened = opened; - } - pub fn is_fire_shutoff_valve_opened(&self) -> bool { self.fire_shutoff_valve_opened } @@ -398,14 +405,17 @@ impl HydLoop { self.ptu_active = ptu_act; } - pub fn update( + pub fn update( &mut self, delta_time: &Duration, 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.); @@ -504,7 +514,7 @@ impl HydLoop { self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); } } -impl SimulationElement for HydLoop { +impl SimulationElement for HydraulicLoop { fn write(&self, writer: &mut SimulatorWriter) { writer.write_f64(&self.pressure_id, self.get_pressure().get::()); writer.write_f64( @@ -517,14 +527,8 @@ impl SimulationElement for HydLoop { } } -pub enum PumpPressurisationCommand { - Pressurise, - Depressurise, -} -impl PumpPressurisationCommand { - pub fn is_pressurise(&self) -> bool { - matches!(self, PumpPressurisationCommand::Pressurise) - } +pub trait PumpController { + fn should_pressurise(&self) -> bool; } pub struct Pump { @@ -535,7 +539,6 @@ pub struct Pump { displacement_carac: [f64; 9], // Displacement low pass filter. [0:1], 0 frozen -> 1 instantaneous dynamic displacement_dynamic: f64, - is_pressurised: bool, } impl Pump { fn new( @@ -550,16 +553,17 @@ impl Pump { press_breakpoints, displacement_carac, displacement_dynamic, - is_pressurised: true, } } - fn command(&mut self, commanded_state: PumpPressurisationCommand) { - self.is_pressurised = commanded_state.is_pressurise(); - } - - fn update(&mut self, delta_time: &Duration, line: &HydLoop, rpm: f64) { - let theoretical_displacement = self.calculate_displacement(line.get_pressure()); + fn update( + &mut self, + delta_time: &Duration, + line: &HydraulicLoop, + rpm: f64, + controller: &T, + ) { + let theoretical_displacement = self.calculate_displacement(line.get_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 @@ -572,8 +576,12 @@ impl Pump { self.delta_vol_min = Volume::new::(0.0); } - fn calculate_displacement(&self, pressure: Pressure) -> Volume { - if self.is_pressurised { + fn calculate_displacement( + &self, + pressure: Pressure, + controller: &T, + ) -> Volume { + if controller.should_pressurise() { return Volume::new::(interpolation( &self.press_breakpoints, &self.displacement_carac, @@ -600,7 +608,7 @@ impl PressureSource for Pump { pub struct ElectricPump { active_id: String, - active: bool, + is_active: bool, rpm: f64, pump: Pump, } @@ -617,7 +625,7 @@ impl ElectricPump { pub fn new(id: &str) -> Self { Self { active_id: format!("HYD_{}_EPUMP_ACTIVE", id), - active: false, + is_active: false, rpm: 0., pump: Pump::new( Self::DISPLACEMENT_BREAKPTS, @@ -627,31 +635,33 @@ impl ElectricPump { } } - pub fn command(&mut self, commanded_state: PumpPressurisationCommand) { - self.active = commanded_state.is_pressurise(); - } - pub fn get_rpm(&self) -> f64 { self.rpm } - pub fn update(&mut self, delta_time: &Duration, line: &HydLoop) { + pub fn update( + &mut self, + delta_time: &Duration, + line: &HydraulicLoop, + controller: &T, + ) { //TODO Simulate speed of pump depending on pump load (flow?/ current?) //Pump startup/shutdown process - if self.active && self.rpm < Self::NOMINAL_SPEED { + if self.is_active && self.rpm < Self::NOMINAL_SPEED { self.rpm += (Self::NOMINAL_SPEED / Self::SPOOLUP_TIME) * delta_time.as_secs_f64(); - } else if !self.active && self.rpm > 0.0 { + } else if !self.is_active && self.rpm > 0.0 { self.rpm -= (Self::NOMINAL_SPEED / Self::SPOOLDOWN_TIME) * delta_time.as_secs_f64(); } //Limiting min and max speed self.rpm = self.rpm.min(Self::NOMINAL_SPEED).max(0.0); - self.pump.update(delta_time, line, self.rpm); + self.pump.update(delta_time, line, self.rpm, controller); + self.is_active = controller.should_pressurise(); } pub fn is_active(&self) -> bool { - self.active + self.is_active } } impl PressureSource for ElectricPump { @@ -671,7 +681,7 @@ impl SimulationElement for ElectricPump { pub struct EngineDrivenPump { active_id: String, - active: bool, + is_active: bool, pump: Pump, } impl EngineDrivenPump { @@ -690,7 +700,7 @@ impl EngineDrivenPump { pub fn new(id: &str) -> Self { Self { active_id: format!("HYD_{}_EDPUMP_ACTIVE", id), - active: false, + is_active: false, pump: Pump::new( Self::DISPLACEMENT_BREAKPTS, Self::DISPLACEMENT_MAP, @@ -699,20 +709,22 @@ impl EngineDrivenPump { } } - pub fn update(&mut self, delta_time: &Duration, line: &HydLoop, engine: &Engine) { + pub fn update( + &mut self, + delta_time: &Duration, + line: &HydraulicLoop, + engine: &Engine, + controller: &T, + ) { let n2_rpm = engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; let pump_rpm = n2_rpm * Self::PUMP_N2_GEAR_RATIO; - self.pump.update(delta_time, line, pump_rpm); - } - - pub fn command(&mut self, commanded_state: PumpPressurisationCommand) { - self.active = commanded_state.is_pressurise(); - self.pump.command(commanded_state); + self.pump.update(delta_time, line, pump_rpm, controller); + self.is_active = controller.should_pressurise(); } - pub fn is_active(&self) -> bool { - self.active + fn is_active(&self) -> bool { + self.is_active } } impl PressureSource for EngineDrivenPump { @@ -826,9 +838,31 @@ impl Default for WindTurbine { } } +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, @@ -860,15 +894,27 @@ impl RamAirTurbine { 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, delta_time: &Duration, line: &HydLoop) { - self.pump - .update(delta_time, line, self.wind_turbine.get_rpm()); + pub fn update( + &mut self, + delta_time: &Duration, + line: &HydraulicLoop, + controller: &T, + ) { + self.deployment_commanded = controller.should_deploy(); + + self.pump.update( + delta_time, + line, + self.wind_turbine.get_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 @@ -898,10 +944,6 @@ impl RamAirTurbine { } } } - - pub fn deploy(&mut self) { - self.deployment_commanded = true; - } } impl PressureSource for RamAirTurbine { fn get_delta_vol_max(&self) -> Volume { @@ -942,17 +984,105 @@ mod tests { volume::{gallon, liter}, }; + 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 command_enable(&mut self) { + self.should_enable = true; + } + } + impl PowerTransferUnitController for TestPowerTransferUnitController { + fn should_enable(&self) -> bool { + self.should_enable + } + } + + 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 engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn green_loop_edp_simulation() { let mut edp1 = engine_driven_pump(); let mut green_loop = hydraulic_loop("GREEN"); - edp1.active = true; + let green_loop_controller = + TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); let init_n2 = Ratio::new::(55.0); let engine1 = engine(init_n2); let context = context(Duration::from_millis(100)); + let mut pump_controller = TestPumpController::commanding_pressurise(); for x in 0..600 { if x == 50 { @@ -961,20 +1091,21 @@ mod tests { } if x == 200 { assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); - edp1.command(PumpPressurisationCommand::Depressurise); + pump_controller.command_depressurise(); } if x >= 500 { //Shutdown + 30s assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&context.delta(), &green_loop, &engine1); + edp1.update(&context.delta(), &green_loop, &engine1, &pump_controller); green_loop.update( &context.delta(), Vec::new(), vec![&edp1], Vec::new(), Vec::new(), + &green_loop_controller, ); if x % 20 == 0 { println!("Iteration {}", x); @@ -1009,26 +1140,29 @@ mod tests { fn yellow_loop_epump_simulation() { let mut epump = electric_pump(); let mut yellow_loop = hydraulic_loop("YELLOW"); - epump.active = true; + let yellow_loop_controller = + TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); + let mut pump_controller = TestPumpController::commanding_pressurise(); let context = context(Duration::from_millis(100)); for x in 0..800 { if x == 400 { assert!(yellow_loop.loop_pressure >= Pressure::new::(2800.0)); - epump.active = false; + pump_controller.command_depressurise(); } if x >= 600 { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } - epump.update(&context.delta(), &yellow_loop); + epump.update(&context.delta(), &yellow_loop, &pump_controller); yellow_loop.update( &context.delta(), vec![&epump], Vec::new(), Vec::new(), Vec::new(), + &yellow_loop_controller, ); if x % 20 == 0 { println!("Iteration {}", x); @@ -1056,26 +1190,29 @@ mod tests { fn blue_loop_epump_simulation() { let mut epump = electric_pump(); let mut blue_loop = hydraulic_loop("BLUE"); - epump.active = true; + let blue_loop_controller = + TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); + let mut pump_controller = TestPumpController::commanding_pressurise(); let context = context(Duration::from_millis(100)); for x in 0..800 { if x == 400 { assert!(blue_loop.loop_pressure >= Pressure::new::(2800.0)); - epump.active = false; + pump_controller.command_depressurise(); } if x >= 600 { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } - epump.update(&context.delta(), &blue_loop); + epump.update(&context.delta(), &blue_loop, &pump_controller); blue_loop.update( &context.delta(), vec![&epump], Vec::new(), Vec::new(), Vec::new(), + &blue_loop_controller, ); if x % 20 == 0 { println!("Iteration {}", x); @@ -1102,11 +1239,14 @@ mod tests { //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_airpseed = context.indicated_airspeed(); + let mut indicated_airspeed = context.indicated_airspeed(); let mut time = 0.0; for x in 0..1500 { @@ -1121,7 +1261,7 @@ mod tests { if time >= 20. && time < 20. + timestep { println!("ASSERT RAT STOWED STILL NO PRESS"); assert!(blue_loop.loop_pressure <= Pressure::new::(50.0)); - rat.deploy(); + rat_controller.command_deployment(); } if time >= 30. && time < 30. + timestep { @@ -1138,7 +1278,7 @@ mod tests { if time >= 70. && time < 70. + timestep { println!("STOPING THE PLANE"); - indicated_airpseed = Velocity::new::(0.); + indicated_airspeed = Velocity::new::(0.); } if time >= 120. && time < 120. + timestep { @@ -1146,14 +1286,15 @@ mod tests { assert!(rat.wind_turbine.rpm <= 2500.); } - rat.update_physics(&context.delta(), &indicated_airpseed); - rat.update(&context.delta(), &blue_loop); + rat.update_physics(&context.delta(), &indicated_airspeed); + rat.update(&context.delta(), &blue_loop, &rat_controller); blue_loop.update( &context.delta(), Vec::new(), Vec::new(), vec![&rat], Vec::new(), + &blue_loop_controller, ); if x % 20 == 0 { println!("Iteration {} Time {}", x, time); @@ -1180,19 +1321,24 @@ mod tests { //shut green edp off, check drop of pressure and ptu effect //shut yellow epump, check drop of pressure in both loops fn yellow_green_ptu_loop_simulation() { - let mut epump = electric_pump(); - epump.command(PumpPressurisationCommand::Depressurise); let mut yellow_loop = hydraulic_loop("YELLOW"); + let yellow_loop_controller = + TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - let mut edp1 = engine_driven_pump(); - assert!(!edp1.active); //Is off when created? - edp1.command(PumpPressurisationCommand::Depressurise); + let mut electric_pump = electric_pump(); + let mut electric_pump_controller = TestPumpController::commanding_depressurise(); + + let mut engine_driven_pump = engine_driven_pump(); + let mut engine_driven_pump_controller = TestPumpController::commanding_depressurise(); let mut engine1 = engine(Ratio::new::(0.0)); let mut green_loop = hydraulic_loop("GREEN"); + let green_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)); @@ -1210,7 +1356,7 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); assert!(green_loop.reservoir_volume == green_res_at_start); - epump.command(PumpPressurisationCommand::Pressurise); + electric_pump_controller.command_pressurise(); } if x == 110 { @@ -1222,7 +1368,7 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); assert!(green_loop.reservoir_volume == green_res_at_start); - ptu.set_enabled(true); + ptu_controller.command_enable(); } if x == 300 { @@ -1237,7 +1383,7 @@ mod tests { println!("------------GREEN EDP1 ON------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); - edp1.command(PumpPressurisationCommand::Pressurise); + engine_driven_pump_controller.command_pressurise(); } if (500..=600).contains(&x) { @@ -1253,12 +1399,12 @@ mod tests { println!("-------------ALL PUMPS OFF------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - edp1.command(PumpPressurisationCommand::Depressurise); - epump.command(PumpPressurisationCommand::Depressurise); + engine_driven_pump_controller.command_depressurise(); + electric_pump_controller.command_depressurise(); } if x == 800 { - //@80s diabling edp and epump + //@80s disabling edp and epump println!("-----------IS PRESSURE OFF?-----------"); assert!(yellow_loop.loop_pressure < Pressure::new::(50.0)); assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); @@ -1273,30 +1419,37 @@ mod tests { ); } - ptu.update(&green_loop, &yellow_loop); - edp1.update(&context.delta(), &green_loop, &engine1); - epump.update(&context.delta(), &yellow_loop); + ptu.update(&green_loop, &yellow_loop, &ptu_controller); + engine_driven_pump.update( + &context.delta(), + &green_loop, + &engine1, + &engine_driven_pump_controller, + ); + electric_pump.update(&context.delta(), &yellow_loop, &electric_pump_controller); yellow_loop.update( &context.delta(), - vec![&epump], + vec![&electric_pump], Vec::new(), Vec::new(), vec![&ptu], + &yellow_loop_controller, ); green_loop.update( &context.delta(), Vec::new(), - vec![&edp1], + vec![&engine_driven_pump], Vec::new(), vec![&ptu], + &green_loop_controller, ); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); println!("---PSI YELLOW: {}", yellow_loop.loop_pressure.get::()); - println!("---RPM YELLOW: {}", epump.rpm); + println!("---RPM YELLOW: {}", electric_pump.rpm); println!( "---Priming State: {}/{}", yellow_loop.loop_volume.get::(), @@ -1313,9 +1466,9 @@ mod tests { } } - fn hydraulic_loop(loop_color: &str) -> HydLoop { + fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { match loop_color { - "GREEN" => HydLoop::new( + "GREEN" => HydraulicLoop::new( loop_color, true, false, @@ -1326,7 +1479,7 @@ mod tests { HydFluid::new(Pressure::new::(1450000000.0)), true, ), - "YELLOW" => HydLoop::new( + "YELLOW" => HydraulicLoop::new( loop_color, false, true, @@ -1337,7 +1490,7 @@ mod tests { HydFluid::new(Pressure::new::(1450000000.0)), true, ), - _ => HydLoop::new( + _ => HydraulicLoop::new( loop_color, false, false, @@ -1384,7 +1537,7 @@ mod tests { #[test] fn starts_inactive() { - assert!(!engine_driven_pump().active); + assert!(!engine_driven_pump().is_active); } #[test] @@ -1415,11 +1568,11 @@ mod tests { let dummy_update = Duration::from_secs(1); let mut line = hydraulic_loop("GREEN"); - edp.command(PumpPressurisationCommand::Pressurise); + let engine_driven_pump_controller = TestPumpController::commanding_pressurise(); line.loop_pressure = pressure; - edp.update(&dummy_update, &line, &eng); //Update 10 times to stabilize displacement + edp.update(&dummy_update, &line, &eng, &engine_driven_pump_controller); //Update 10 times to stabilize displacement - edp.update(&time, &line, &eng); + edp.update(&time, &line, &eng, &engine_driven_pump_controller); edp.get_delta_vol_max() } From 60d437bbf853e70198f14198c549fe0d7642a3ff Mon Sep 17 00:00:00 2001 From: David Walschots Date: Sun, 4 Apr 2021 11:52:23 +0200 Subject: [PATCH 066/122] refactor: is_pressurised in HydraulicLoop --- src/systems/a320_systems/src/hydraulic.rs | 70 +++++-------------- .../systems/src/hydraulic/brakecircuit.rs | 6 ++ src/systems/systems/src/hydraulic/mod.rs | 24 +++++++ 3 files changed, 48 insertions(+), 52 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 1120aba63dd..42956e38f43 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -50,17 +50,13 @@ pub(super) struct A320Hydraulic { braking_circuit_altn: BrakeCircuit, total_sim_time_elapsed: Duration, lag_time_accumulator: Duration, - - is_green_pressurised: bool, - is_blue_pressurised: bool, - is_yellow_pressurised: bool, } impl A320Hydraulic { const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1450.0; const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 1750.0; - const HYDRAULIC_SIM_TIME_STEP: u64 = 100; //refresh rate of hydraulic simulation in ms - const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; //refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update + const HYDRAULIC_SIM_TIME_STEP: u64 = 100; // Refresh rate of hydraulic simulation in ms + const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; // Refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update pub(super) fn new() -> A320Hydraulic { A320Hydraulic { @@ -76,6 +72,8 @@ impl A320Hydraulic { Volume::new::(1.56), HydFluid::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( @@ -88,6 +86,8 @@ impl A320Hydraulic { Volume::new::(3.6), HydFluid::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( @@ -100,6 +100,8 @@ impl A320Hydraulic { Volume::new::(3.15), HydFluid::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)), @@ -141,10 +143,6 @@ impl A320Hydraulic { total_sim_time_elapsed: Duration::new(0, 0), lag_time_accumulator: Duration::new(0, 0), - - is_green_pressurised: false, - is_blue_pressurised: false, - is_yellow_pressurised: false, } } @@ -216,25 +214,28 @@ impl A320Hydraulic { fn green_edp_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_1_controller.should_pressurise() && !self.is_green_pressurised + self.engine_driven_pump_1_controller.should_pressurise() + && !self.green_loop.is_pressurised() } fn yellow_epump_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.yellow_electric_pump_controller.should_pressurise() && !self.is_yellow_pressurised + self.yellow_electric_pump_controller.should_pressurise() + && !self.yellow_loop.is_pressurised() } fn yellow_edp_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_2_controller.should_pressurise() && !self.is_yellow_pressurised + self.engine_driven_pump_2_controller.should_pressurise() + && !self.yellow_loop.is_pressurised() } fn blue_epump_has_fault(&self) -> bool { // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.blue_electric_pump_controller.should_pressurise() && !self.is_blue_pressurised + self.blue_electric_pump_controller.should_pressurise() && !self.blue_loop.is_pressurised() } #[cfg(test)] @@ -249,51 +250,18 @@ impl A320Hydraulic { .nose_wheel_steering_pin_is_inserted() } - // Updates pressure available state based on pressure switches - fn update_hyd_avail_states(&mut self) { - if self.green_loop.get_pressure() - <= Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST) - { - self.is_green_pressurised = false; - } else if self.green_loop.get_pressure() - >= Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST) - { - self.is_green_pressurised = true; - } - - if self.blue_loop.get_pressure() - <= Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST) - { - self.is_blue_pressurised = false; - } else if self.blue_loop.get_pressure() - >= Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST) - { - self.is_blue_pressurised = true; - } - - if self.yellow_loop.get_pressure() - <= Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST) - { - self.is_yellow_pressurised = false; - } else if self.yellow_loop.get_pressure() - >= Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST) - { - self.is_yellow_pressurised = true; - } - } - pub(super) fn is_blue_pressurised(&self) -> bool { - self.is_blue_pressurised + self.blue_loop.is_pressurised() } #[cfg(test)] fn is_green_pressurised(&self) -> bool { - self.is_green_pressurised + self.green_loop.is_pressurised() } #[cfg(test)] fn is_yellow_pressurised(&self) -> bool { - self.is_yellow_pressurised + self.yellow_loop.is_pressurised() } // All the higher frequency updates like physics @@ -312,8 +280,6 @@ impl A320Hydraulic { engine_fire_overhead: &A320EngineFireOverheadPanel, min_hyd_loop_timestep: Duration, ) { - self.update_hyd_avail_states(); - // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic .update_brake_demands(&min_hyd_loop_timestep, &self.green_loop); diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index e616dae4fac..c78965ded4f 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -499,6 +499,8 @@ mod tests { Volume::new::(3.83), HydFluid::new(Pressure::new::(1450000000.0)), true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), ), "YELLOW" => HydraulicLoop::new( loop_color, @@ -510,6 +512,8 @@ mod tests { Volume::new::(3.3), HydFluid::new(Pressure::new::(1450000000.0)), true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), ), _ => HydraulicLoop::new( loop_color, @@ -521,6 +525,8 @@ mod tests { Volume::new::(1.5), HydFluid::new(Pressure::new::(1450000000.0)), false, + Pressure::new::(1450.0), + Pressure::new::(1750.0), ), } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 25fbc63ce83..61af9502f30 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -219,6 +219,9 @@ pub struct HydraulicLoop { current_max_flow: VolumeRate, //Current total max flow available from pressure sources fire_shutoff_valve_opened: bool, has_fire_valve: bool, + min_pressure_pressurised_lo_hyst: Pressure, + min_pressure_pressurised_hi_hyst: Pressure, + is_pressurised: bool, } impl HydraulicLoop { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI @@ -243,6 +246,8 @@ impl HydraulicLoop { reservoir_volume: Volume, fluid: HydFluid, has_fire_valve: bool, + min_pressure_pressurised_lo_hyst: Pressure, + min_pressure_pressurised_hi_hyst: Pressure, ) -> Self { Self { pressure_id: format!("HYD_{}_PRESSURE", id), @@ -268,6 +273,9 @@ impl HydraulicLoop { 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, } } @@ -512,6 +520,16 @@ impl HydraulicLoop { self.current_delta_vol = delta_vol; self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); + + 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 { @@ -1478,6 +1496,8 @@ mod tests { Volume::new::(3.83), HydFluid::new(Pressure::new::(1450000000.0)), true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), ), "YELLOW" => HydraulicLoop::new( loop_color, @@ -1489,6 +1509,8 @@ mod tests { Volume::new::(3.3), HydFluid::new(Pressure::new::(1450000000.0)), true, + Pressure::new::(1450.0), + Pressure::new::(1750.0), ), _ => HydraulicLoop::new( loop_color, @@ -1500,6 +1522,8 @@ mod tests { Volume::new::(1.5), HydFluid::new(Pressure::new::(1450000000.0)), false, + Pressure::new::(1450.0), + Pressure::new::(1750.0), ), } } From 9aac050c0eefd3ee0b09c2189650d35361c29132 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 11 Apr 2021 17:57:39 +0200 Subject: [PATCH 067/122] Added accumulator base hydraulic bloc --- .../systems/src/hydraulic/brakecircuit.rs | 115 ++++------- src/systems/systems/src/hydraulic/mod.rs | 185 +++++++++++++----- 2 files changed, 180 insertions(+), 120 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index c78965ded4f..a07d7834493 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -7,10 +7,9 @@ use std::f64::consts::E; use std::string::String; use std::time::Duration; -use uom::si::{ - acceleration::foot_per_second_squared, f64::*, pressure::psi, time::second, volume::gallon, - volume_rate::gallon_per_second, -}; +use uom::si::{acceleration::foot_per_second_squared, f64::*, pressure::psi, volume::gallon}; + +use super::HydraulicAccumulator; pub trait ActuatorHydInterface { fn get_used_volume(&self) -> Volume; @@ -39,12 +38,13 @@ pub struct BrakeCircuit { //Brake accumulator variables. Accumulator can have 0 volume if no accumulator has_accumulator: bool, - accumulator_total_volume: Volume, - accumulator_gas_pressure: Pressure, - accumulator_gas_volume: Volume, - accumulator_fluid_volume: Volume, - accumulator_press_breakpoints: [f64; 9], - accumulator_flow_carac: [f64; 9], + accumulator: HydraulicAccumulator, + // accumulator_total_volume: Volume, + // accumulator_gas_pressure: Pressure, + // accumulator_gas_volume: Volume, + // accumulator_fluid_volume: Volume, + // accumulator_press_breakpoints: [f64; 9], + // accumulator_flow_carac: [f64; 9], //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 @@ -94,23 +94,6 @@ impl BrakeCircuit { has_accu = false; } - //taking care init volume is maxed at accumulator capacity: we can't exceed max_volume minus a margin for gas to compress - let limited_volume = accumulator_fluid_volume_at_init.min(accumulator_volume * 0.9); - - //If we don't start with empty accumulator we need to init pressure too - let mut gas_press_at_init = Pressure::new::(0.); - if has_accu { - gas_press_at_init = (Pressure::new::(BrakeCircuit::ACCUMULATOR_GAS_PRE_CHARGE) - * accumulator_volume) - / (accumulator_volume - limited_volume); - } - - //Fluid pressure measure is equal to gas volume in accumulator only if there is fluid in the accumulator - let mut acc_fluid_press_init = Pressure::new::(0.); - if limited_volume > Volume::new::(0.) { - acc_fluid_press_init = gas_press_at_init; - } - BrakeCircuit { _id: String::from(id).to_uppercase(), id_left_press: format!("HYD_BRAKE_{}_LEFT_PRESS", id), @@ -125,15 +108,17 @@ impl BrakeCircuit { demanded_brake_position_right: 0.0, pressure_applied_right: Pressure::new::(0.0), has_accumulator: has_accu, - accumulator_total_volume: accumulator_volume, - accumulator_gas_pressure: gas_press_at_init, - accumulator_gas_volume: accumulator_volume - limited_volume, - accumulator_fluid_volume: limited_volume, - accumulator_press_breakpoints: BrakeCircuit::ACCUMULATOR_PRESS_BREAKPTS, - accumulator_flow_carac: BrakeCircuit::ACCUMULATOR_FLOW_CARAC, + accumulator: HydraulicAccumulator::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.), - accumulator_fluid_pressure_sensor_filtered: acc_fluid_press_init, //Pressure measured after accumulator in brake circuit + accumulator_fluid_pressure_sensor_filtered: Pressure::new::(0.0), //Pressure measured after accumulator in brake circuit } } @@ -142,40 +127,23 @@ impl BrakeCircuit { + (self.demanded_brake_position_right - self.current_brake_position_right)) * self.total_displacement; - //TODO this is a duplicate of an accumulator: refactor an accumulator object usable in all hydraulics code if self.has_accumulator { - let accumulator_delta_press = self.accumulator_gas_pressure - hyd_loop.get_pressure(); - let flow_variation = VolumeRate::new::(super::interpolation( - &self.accumulator_press_breakpoints, - &self.accumulator_flow_carac, - accumulator_delta_press.get::().abs(), - )); - - if accumulator_delta_press.get::() < 0.0 { - let volume_to_acc = flow_variation * Time::new::(delta_time.as_secs_f64()); - self.accumulator_fluid_volume += volume_to_acc; - self.accumulator_gas_volume -= volume_to_acc; - self.volume_to_actuator_accumulator += volume_to_acc; - } + self.accumulator.update( + delta_time, + &mut self.volume_to_actuator_accumulator, + hyd_loop.loop_pressure, + ); if delta_vol > Volume::new::(0.0) { - let volume_from_acc = self.accumulator_fluid_volume.min(delta_vol); - if volume_from_acc != Volume::new::(0.0) { - self.accumulator_fluid_volume -= volume_from_acc; - self.accumulator_gas_volume += volume_from_acc; - } else { + let volume_from_acc = self.accumulator.get_delta_vol(delta_vol); + if volume_from_acc == Volume::new::(0.0) { self.demanded_brake_position_left = self.current_brake_position_left; self.demanded_brake_position_right = self.current_brake_position_right; } } else { self.volume_to_res_accumulator += - delta_vol.abs().min(self.accumulator_fluid_volume); + delta_vol.abs().min(self.accumulator.get_fluid_volume()); } - - self.accumulator_gas_pressure = - (Pressure::new::(BrakeCircuit::ACCUMULATOR_GAS_PRE_CHARGE) - * self.accumulator_total_volume) - / (self.accumulator_total_volume - self.accumulator_fluid_volume); } else { //Else case if no accumulator: we just take deltavol needed or return it back to res if delta_vol > Volume::new::(0.0) @@ -187,15 +155,16 @@ impl BrakeCircuit { } } - if self.accumulator_fluid_volume > Volume::new::(0.0) { + if self.accumulator.get_fluid_volume() > Volume::new::(0.0) { self.pressure_applied_left = - self.accumulator_gas_pressure * self.demanded_brake_position_left; + self.accumulator.get_raw_gas_press() * self.demanded_brake_position_left; self.pressure_applied_right = - self.accumulator_gas_pressure * self.demanded_brake_position_right; + self.accumulator.get_raw_gas_press() * self.demanded_brake_position_right; self.accumulator_fluid_pressure_sensor_filtered = self .accumulator_fluid_pressure_sensor_filtered - + (self.accumulator_gas_pressure - self.accumulator_fluid_pressure_sensor_filtered) + + (self.accumulator.get_raw_gas_press() + - self.accumulator_fluid_pressure_sensor_filtered) * (1. - E.powf( -delta_time.as_secs_f64() @@ -364,9 +333,11 @@ mod tests { + brake_circuit_unprimed.get_brake_pressure_right() < 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); + assert!(brake_circuit_unprimed.accumulator.total_volume == init_max_vol); + assert!( + brake_circuit_unprimed.accumulator.get_fluid_volume() == Volume::new::(0.0) + ); + assert!(brake_circuit_unprimed.accumulator.gas_volume == init_max_vol); let brake_circuit_primed = BrakeCircuit::new( "altn", @@ -380,9 +351,9 @@ mod tests { + brake_circuit_unprimed.get_brake_pressure_right() < 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); + assert!(brake_circuit_primed.accumulator.total_volume == init_max_vol); + assert!(brake_circuit_primed.accumulator.get_fluid_volume() == init_max_vol / 2.0); + assert!(brake_circuit_primed.accumulator.gas_volume < init_max_vol); } #[test] @@ -417,14 +388,14 @@ mod tests { assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= Pressure::new::(50.)); - assert!(brake_circuit_primed.accumulator_fluid_volume >= Volume::new::(0.1)); + assert!(brake_circuit_primed.accumulator.get_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(&Duration::from_secs_f64(0.1), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); - assert!(brake_circuit_primed.accumulator_fluid_volume >= Volume::new::(0.1)); + assert!(brake_circuit_primed.accumulator.get_fluid_volume() >= Volume::new::(0.1)); } #[test] @@ -465,7 +436,7 @@ mod tests { brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); - assert!(brake_circuit_primed.accumulator_fluid_volume == Volume::new::(0.0)); + assert!(brake_circuit_primed.accumulator.get_fluid_volume() == Volume::new::(0.0)); } #[test] diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 61af9502f30..5b505ef391a 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -50,6 +50,34 @@ impl HydFluid { } } +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; } @@ -196,16 +224,105 @@ pub trait HydraulicLoopController { fn should_open_fire_shutoff_valve(&self) -> bool; } +struct HydraulicAccumulator { + total_volume: Volume, + gas_init_precharge: Pressure, + gas_pressure: Pressure, + gas_volume: Volume, + fluid_volume: Volume, + press_breakpoints: [f64; 9], + flow_carac: [f64; 9], + has_control_valve: bool, +} + +impl HydraulicAccumulator { + pub fn new( + gas_precharge: Pressure, + total_volume: Volume, + fluid_vol_at_init: Volume, + press_breakpoints: [f64; 9], + flow_carac: [f64; 9], + 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, + press_breakpoints, + flow_carac, + has_control_valve, + } + } + + fn update(&mut self, delta_time: &Duration, delta_vol: &mut Volume, loop_pressure: Pressure) { + let accumulator_delta_press = self.gas_pressure - loop_pressure; + let flow_variation = VolumeRate::new::(interpolation( + &self.press_breakpoints, + &self.flow_carac, + accumulator_delta_press.get::().abs(), + )); + + // 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 * Time::new::(delta_time.as_secs_f64())); + self.fluid_volume -= volume_from_acc; + self.gas_volume += 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 * Time::new::(delta_time.as_secs_f64())); + self.fluid_volume += volume_to_acc; + self.gas_volume -= volume_to_acc; + *delta_vol -= volume_to_acc; + } + + self.gas_pressure = + (self.gas_init_precharge * self.total_volume) / (self.total_volume - self.fluid_volume); + } + + pub 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 + } + + pub fn get_fluid_volume(&self) -> Volume { + self.fluid_volume + } + + pub fn get_raw_gas_press(&self) -> Pressure { + self.gas_pressure + } +} pub struct HydraulicLoop { pressure_id: String, reservoir_id: String, fire_valve_id: String, fluid: HydFluid, - accumulator_gas_pressure: Pressure, - accumulator_gas_volume: Volume, - accumulator_fluid_volume: Volume, - accumulator_press_breakpoints: [f64; 9], - accumulator_flow_carac: [f64; 9], + accumulator: HydraulicAccumulator, connected_to_ptu_left_side: bool, connected_to_ptu_right_side: bool, loop_pressure: Pressure, @@ -254,9 +371,6 @@ impl HydraulicLoop { reservoir_id: format!("HYD_{}_RESERVOIR", id), fire_valve_id: format!("HYD_{}_FIRE_VALVE_OPENED", id), - accumulator_gas_pressure: Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), - accumulator_gas_volume: Volume::new::(Self::ACCUMULATOR_MAX_VOLUME), - accumulator_fluid_volume: Volume::new::(0.), connected_to_ptu_left_side, connected_to_ptu_right_side, loop_pressure: Pressure::new::(14.7), @@ -266,10 +380,16 @@ impl HydraulicLoop { ptu_active: false, reservoir_volume, fluid, + accumulator: HydraulicAccumulator::new( + Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), + Volume::new::(Self::ACCUMULATOR_MAX_VOLUME), + Volume::new::(0.), + Self::ACCUMULATOR_PRESS_BREAKPTS, + Self::ACCUMULATOR_FLOW_CARAC, + false, + ), current_delta_vol: Volume::new::(0.), current_flow: VolumeRate::new::(0.), - accumulator_press_breakpoints: Self::ACCUMULATOR_PRESS_BREAKPTS, - accumulator_flow_carac: Self::ACCUMULATOR_FLOW_CARAC, current_max_flow: VolumeRate::new::(0.), fire_shutoff_valve_opened: true, has_fire_valve, @@ -326,38 +446,6 @@ impl HydraulicLoop { self.fire_shutoff_valve_opened } - fn update_accumulator(&mut self, delta_time: &Duration, delta_vol: &mut Volume) { - let accumulator_delta_press = self.accumulator_gas_pressure - self.loop_pressure; - let flow_variation = VolumeRate::new::(interpolation( - &self.accumulator_press_breakpoints, - &self.accumulator_flow_carac, - accumulator_delta_press.get::().abs(), - )); - - // 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 { - let volume_from_acc = self - .accumulator_fluid_volume - .min(flow_variation * Time::new::(delta_time.as_secs_f64())); - self.accumulator_fluid_volume -= volume_from_acc; - self.accumulator_gas_volume += volume_from_acc; - *delta_vol += volume_from_acc; - } else { - let volume_to_acc = delta_vol - .max(Volume::new::(0.0)) - .max(flow_variation * Time::new::(delta_time.as_secs_f64())); - self.accumulator_fluid_volume += volume_to_acc; - self.accumulator_gas_volume -= volume_to_acc; - *delta_vol -= volume_to_acc; - } - - self.accumulator_gas_pressure = (Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE) - * Volume::new::(Self::ACCUMULATOR_MAX_VOLUME)) - / (Volume::new::(Self::ACCUMULATOR_MAX_VOLUME) - self.accumulator_fluid_volume); - } - fn update_ptu_flows( &mut self, delta_time: &Duration, @@ -465,7 +553,8 @@ impl HydraulicLoop { self.update_ptu_flows(delta_time, ptus, &mut delta_vol, &mut reservoir_return); // Updates current accumulator state and updates loop delta_vol - self.update_accumulator(delta_time, &mut delta_vol); + self.accumulator + .update(delta_time, &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 @@ -1139,15 +1228,15 @@ mod tests { ); println!( "--------Acc Fluid Volume (L): {}", - green_loop.accumulator_fluid_volume.get::() + green_loop.accumulator.get_fluid_volume().get::() ); println!( "--------Acc Gas Volume (L): {}", - green_loop.accumulator_gas_volume.get::() + green_loop.accumulator.gas_volume.get::() ); println!( "--------Acc Gas Pressure (psi): {}", - green_loop.accumulator_gas_pressure.get::() + green_loop.accumulator.get_raw_gas_press().get::() ); } } @@ -1197,7 +1286,7 @@ mod tests { ); println!( "--------Acc Volume (g): {}", - yellow_loop.accumulator_gas_volume.get::() + yellow_loop.accumulator.gas_volume.get::() ); } } @@ -1247,7 +1336,7 @@ mod tests { ); println!( "--------Acc Volume (g): {}", - blue_loop.accumulator_gas_volume.get::() + blue_loop.accumulator.gas_volume.get::() ); } } From 7b64afb0f87ed0a44b9575ddbe8ae41641c6aab7 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 12 Apr 2021 20:02:47 +0200 Subject: [PATCH 068/122] Prepared actuator interaction mechanism --- src/systems/a320_systems/src/hydraulic.rs | 59 +++++++++++++++---- src/systems/a320_systems_wasm/src/lib.rs | 8 +-- .../systems/src/hydraulic/brakecircuit.rs | 19 +++--- src/systems/systems/src/hydraulic/mod.rs | 27 +++++---- 4 files changed, 78 insertions(+), 35 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 42956e38f43..5b0ea2134f4 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -188,8 +188,12 @@ impl A320Hydraulic { * min_hyd_loop_timestep.as_secs_f64(), ); // Keep track of time left after all fixed loop are done - // This is main update loop at base fixed step + // 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, engine1, @@ -270,6 +274,29 @@ impl A320Hydraulic { .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) { + // Actuator interaction given as example here needs extensive tests in another PR + // self.green_loop + // .update_actuator_volumes(&self.braking_circuit_norm); + // self.braking_circuit_norm.reset_accumulators(); + } + + fn update_yellow_actuators_volume(&mut self) { + // Actuator interaction given as example here needs extensive tests in another PR + // 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, @@ -380,8 +407,6 @@ impl A320Hydraulic { .update(&min_hyd_loop_timestep, &self.green_loop); self.braking_circuit_altn .update(&min_hyd_loop_timestep, &self.yellow_loop); - self.braking_circuit_norm.reset_accumulators(); - self.braking_circuit_altn.reset_accumulators(); } } impl SimulationElement for A320Hydraulic { @@ -1500,20 +1525,25 @@ mod tests { //Enabled on cold start assert!(test_bed.is_ptu_enabled()); - //Yellow epump ON / Waiting 10s + //Yellow epump ON / Waiting 25s test_bed = test_bed .set_yellow_e_pump(false) - .run_waiting_for(Duration::from_secs(10)); + .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::(3500.)); + 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::(3500.)); //Ptu push button disables PTU / green press should fall test_bed = test_bed @@ -1676,13 +1706,13 @@ mod tests { assert!(!test_bed.is_fire_valve_eng1_closed()); assert!(!test_bed.is_fire_valve_eng2_closed()); - //Starting eng 1 + // Starting eng 1 test_bed = test_bed .start_eng2(Ratio::new::(50.)) .start_eng1(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(5)); - //Waiting for 5s pressure hsould be at 3000 psi + // 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()); @@ -1693,7 +1723,7 @@ mod tests { assert!(!test_bed.is_fire_valve_eng1_closed()); assert!(!test_bed.is_fire_valve_eng2_closed()); - //Green shutoff valve + // Green shutoff valve test_bed = test_bed .set_eng1_fire_button(true) .run_waiting_for(Duration::from_secs(20)); @@ -1708,7 +1738,7 @@ mod tests { assert!(test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); - //Yellow shutoff valve + // Yellow shutoff valve test_bed = test_bed .set_eng2_fire_button(true) .run_waiting_for(Duration::from_secs(20)); @@ -1758,9 +1788,12 @@ mod tests { .set_right_brake(Ratio::new::(0.)) .set_park_brake(false) .set_yellow_e_pump(false) - .run_waiting_for(Duration::from_secs(15)); + .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.)); @@ -1771,7 +1804,7 @@ mod tests { //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(2)); + .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.)); @@ -1821,7 +1854,9 @@ mod tests { .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.)); @@ -1833,7 +1868,9 @@ mod tests { 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] diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index 61dc96b6f3f..089bba6c8a3 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -88,8 +88,8 @@ impl A320SimulatorReaderWriter { 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_forward_pos: AircraftVariable::from("EXIT OPEN", "Percent", 5)?, - cargo_door_aft_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, //TODO It is the catering door for now + cargo_door_front_pos: AircraftVariable::from("EXIT OPEN", "Percent", 5)?, + cargo_door_back_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, //TODO It is the catering door for now 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)?, @@ -124,8 +124,8 @@ impl SimulatorReaderWriter for A320SimulatorReaderWriter { "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_forward_pos.get(), - "EXIT OPEN:3" => self.cargo_door_aft_pos.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(), diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index a07d7834493..c122ab7db21 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -128,34 +128,35 @@ impl BrakeCircuit { * self.total_displacement; if self.has_accumulator { + let mut volume_into_accumulator = Volume::new::(0.); self.accumulator.update( delta_time, - &mut self.volume_to_actuator_accumulator, + &mut volume_into_accumulator, hyd_loop.loop_pressure, ); - if delta_vol > Volume::new::(0.0) { + // Volume that just came into accumulator is taken from hydraulic loop through olume_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); - if volume_from_acc == Volume::new::(0.0) { + if volume_from_acc <= Volume::new::(0.0000001) { self.demanded_brake_position_left = self.current_brake_position_left; self.demanded_brake_position_right = self.current_brake_position_right; } } else { - self.volume_to_res_accumulator += - delta_vol.abs().min(self.accumulator.get_fluid_volume()); + self.volume_to_res_accumulator += delta_vol.abs(); } } else { //Else case if no accumulator: we just take deltavol needed or return it back to res - if delta_vol > Volume::new::(0.0) - && hyd_loop.get_pressure() >= Pressure::new::(100.) - { + if delta_vol > Volume::new::(0.) { self.volume_to_actuator_accumulator += delta_vol; } else { self.volume_to_res_accumulator += delta_vol.abs(); } } - if self.accumulator.get_fluid_volume() > Volume::new::(0.0) { + if self.accumulator.get_fluid_volume() > Volume::new::(0.) { self.pressure_applied_left = self.accumulator.get_raw_gas_press() * self.demanded_brake_position_left; self.pressure_applied_right = diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 5b505ef391a..c0f28c4928b 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -14,7 +14,9 @@ use uom::si::{ use crate::engine::Engine; use crate::shared::interpolation; use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; + pub mod brakecircuit; +use crate::hydraulic::brakecircuit::ActuatorHydInterface; // Trait common to all hydraulic pumps // Max gives maximum available volume at that time as if it is a variable displacement @@ -339,6 +341,8 @@ pub struct HydraulicLoop { 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 { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI @@ -396,6 +400,8 @@ impl HydraulicLoop { 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.), } } @@ -419,6 +425,11 @@ impl HydraulicLoop { drawn } + pub fn update_actuator_volumes(&mut self, actuator: &T) { + self.total_actuators_consumed_volume += actuator.get_used_volume(); + self.total_actuators_returned_volume += actuator.get_reservoir_return(); + } + // Returns the max flow that can be output from reservoir in dt time pub fn get_usable_reservoir_flow(&self, amount: VolumeRate, delta_time: Time) -> VolumeRate { let mut drawn = amount; @@ -567,17 +578,11 @@ impl HydraulicLoop { self.reservoir_volume -= delta_loop_vol; } - //Actuators to update here, we get their accumulated consumptions and returns, then reset them for next iteration - let used_fluid_qty = Volume::new::(0.); - //foreach actuator pseudocode - //used_fluidQty =used_fluidQty+aileron.volumeToActuatorAccumulated - //reservoirReturn=reservoirReturn+aileron.volumeToResAccumulated - //actuator.resetVolumes() - //actuator.set_available_pressure(self.loop_pressure) - //end foreach - //end actuator - - delta_vol -= used_fluid_qty; + //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 = From 34775437c0954e8d35fbd69de13f2459c9aa70c0 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 13 Apr 2021 09:05:16 +0200 Subject: [PATCH 069/122] update 3D pressure gauges --- .../FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 09ad4fa726b..e21b0857af5 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 @@ -3347,7 +3347,7 @@ Press_Arc_L 3 1 - (A:BRAKE PARKING POSITION, Bool) 2 * + (L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS, number) 1000 / @@ -3355,7 +3355,7 @@ Press_Arc_R 3 1 - (A:BRAKE PARKING POSITION, Bool) 2 * + (L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS, number) 1000 / @@ -3363,7 +3363,7 @@ Accu_Press 1 1 - 0.8 + (L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS, number) 4000 / From 4ea772e18dbc5c9c83def38080545016dfb9489f Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 13 Apr 2021 10:10:01 +0200 Subject: [PATCH 070/122] Added reservoir stability test --- src/systems/a320_systems/src/hydraulic.rs | 100 +++++++++++++++++++--- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 5b0ea2134f4..bc1dc579c36 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1155,6 +1155,10 @@ mod tests { 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 get_brake_left_yellow_pressure(&mut self) -> Pressure { Pressure::new::( self.simulation_test_bed @@ -1691,6 +1695,78 @@ mod tests { 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_test() { + let mut test_bed = test_bed_with() + .engines_off() + .on_the_ground() + .set_cold_dark_inputs() + .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(); + + // Now doing cycles of pressurisation on EDP and ePump + for _ in 1..51 { + test_bed = test_bed + .start_eng2(Ratio::new::(50.)) + .run_waiting_for(Duration::from_secs(50)); + + assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); + 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.)); + + current_res_level = test_bed.get_yellow_reservoir_volume(); + let mut reservoir_difference = reservoir_level_after_priming - current_res_level; + //println!("---Reservoir deviation: {}", reservoir_difference.get::()); + + // Make sure no more deviation than 0.001 gallon is lost after full pressure and unpressurized states + assert!(reservoir_difference.get::().abs() < 0.001); + + 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.)); + + 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.)); + + current_res_level = test_bed.get_yellow_reservoir_volume(); + reservoir_difference = reservoir_level_after_priming - current_res_level; + //println!("---Reservoir deviation: {}", reservoir_difference.get::()); + assert!(reservoir_difference.get::().abs() < 0.001); + } + } + #[test] fn yellow_green_edp_firevalve_test() { let mut test_bed = test_bed_with() @@ -1762,15 +1838,15 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //Accumulator empty on cold start + // Accumulator empty on cold start assert!(test_bed.get_brake_yellow_accumulator_pressure() < Pressure::new::(50.)); - //No brakes + // 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() < Pressure::new::(50.)); assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); - //No brakes even if we brake + // No brakes even if we brake test_bed = test_bed .set_left_brake(Ratio::new::(100.)) .set_right_brake(Ratio::new::(100.)) @@ -1782,7 +1858,7 @@ mod tests { assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); assert!(test_bed.get_brake_yellow_accumulator_pressure() < Pressure::new::(50.)); - //Park brake off, loading accumulator, we expect no brake pressure but accumulator loaded + // 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.)) @@ -1801,7 +1877,7 @@ mod tests { assert!(test_bed.get_brake_yellow_accumulator_pressure() > Pressure::new::(2500.)); - //Park brake on, loaded accumulator, we expect brakes on yellow side only + // 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)); @@ -1822,7 +1898,7 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //No brakes + // 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() < Pressure::new::(50.)); @@ -1836,7 +1912,7 @@ mod tests { assert!(test_bed.is_green_pressurised()); assert!(test_bed.is_yellow_pressurised()); - //No brakes if we don't brake + // No brakes if we don't brake test_bed = test_bed .set_left_brake(Ratio::new::(0.)) .set_right_brake(Ratio::new::(0.)) @@ -1847,7 +1923,7 @@ mod tests { 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 + // Braking cause green braking system to rise test_bed = test_bed .set_left_brake(Ratio::new::(100.)) .set_right_brake(Ratio::new::(100.)) @@ -1860,7 +1936,7 @@ mod tests { 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 + // 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)); @@ -1889,7 +1965,7 @@ mod tests { assert!(test_bed.is_green_pressurised()); assert!(test_bed.is_yellow_pressurised()); - //Braking left + // Braking left test_bed = test_bed .set_left_brake(Ratio::new::(100.)) .set_right_brake(Ratio::new::(0.)) @@ -1900,7 +1976,7 @@ mod tests { assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); - //Braking right + // Braking right test_bed = test_bed .set_left_brake(Ratio::new::(0.)) .set_right_brake(Ratio::new::(100.)) @@ -1911,7 +1987,7 @@ mod tests { 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 + // 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.)) From 5bbf3d5b631a25089c7411e62035f87f353dc609 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:08:44 +0200 Subject: [PATCH 071/122] Fix: update delay wrong in timed controllers --- .github/CHANGELOG.md | 2 +- src/systems/a320_systems/src/hydraulic.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 725ad65d836..e8da2503f21 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.7.0 +1. [HYD] First hydraulics systems integration - @crocket63 (crocket) 1. [MCDU] Fixed input and display issues on PERF/W&B and INIT pages - @felixharnstrom (Felix Härnström) 1. [MCDU] Progress page only shows GPS Primary when it should - @Username23-23 (NONAmr2433 #8777) 1. [ND] Add VOR/ADF needles to ILS arc display - @tracernz (Mike) @@ -17,7 +18,6 @@ 1. [MCDU] Show max distance of 9999 NM on duplicate waypoint selection - @tracernz (Mike) ## 0.6.0 -1. [HYD] First hydraulics systems integration - @crocket63 (crocket) 1. [CDU] Added WIND page - @tyler58546 (tyler58546) 1. [SOUND] Improved engine startup and fly by sound - @hotshotp (Boris#9134) 1. [SOUND] Added new vent test and ext pwr relay random sounds - @hotshotp (Boris#9134) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index bc1dc579c36..4f5352a502e 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -316,7 +316,7 @@ impl A320Hydraulic { ); self.power_transfer_unit_controller.update( - context, + &context.with_delta(min_hyd_loop_timestep), overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, @@ -354,7 +354,7 @@ impl A320Hydraulic { ); self.yellow_electric_pump_controller.update( - context, + &context.with_delta(min_hyd_loop_timestep), overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, From c9f45781da754f2f732b1ad71ca38db2314a51ea Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:47:28 +0200 Subject: [PATCH 072/122] Comments update --- .../systems/src/hydraulic/brakecircuit.rs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index c122ab7db21..31f4c5ed748 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -16,19 +16,20 @@ pub trait ActuatorHydInterface { fn get_reservoir_return(&self) -> Volume; } -//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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). +// 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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). // So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position pub struct BrakeCircuit { _id: String, id_left_press: String, id_right_press: String, id_acc_press: String, - //Total volume used when at max braking position. - //Simple model for now we assume at max braking we have max brake displacement + + // Total volume used when at max braking position. + // Simple model for now we assume at max braking we have max brake displacement total_displacement: Volume, - //actuator position //TODO make this more dynamic with a per wheel basis instead of left/right? + // Actuator position //TODO make this more dynamic with a per wheel basis instead of left/right? current_brake_position_left: f64, demanded_brake_position_left: f64, pressure_applied_left: Pressure, @@ -36,22 +37,17 @@ pub struct BrakeCircuit { demanded_brake_position_right: f64, pressure_applied_right: Pressure, - //Brake accumulator variables. Accumulator can have 0 volume if no accumulator + // Brake accumulator variables. Accumulator can have 0 volume if no accumulator has_accumulator: bool, accumulator: HydraulicAccumulator, - // accumulator_total_volume: Volume, - // accumulator_gas_pressure: Pressure, - // accumulator_gas_volume: Volume, - // accumulator_fluid_volume: Volume, - // accumulator_press_breakpoints: [f64; 9], - // accumulator_flow_carac: [f64; 9], - - //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 + + // 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, - accumulator_fluid_pressure_sensor_filtered: Pressure, //Fluid pressure in brake circuit filtered for cockpit gauges + // Fluid pressure in brake circuit filtered for cockpit gauges + accumulator_fluid_pressure_sensor_filtered: Pressure, } impl SimulationElement for BrakeCircuit { @@ -80,7 +76,7 @@ impl BrakeCircuit { [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.01, 0.016, 0.02, 0.04, 0.1, 0.15, 0.35, 0.5]; - //Filtered using time constant low pass: new_val = old_val + (new_val - old_val)* (1 - e^(-dt/TCONST)) + // Filtered using time constant low pass: new_val = old_val + (new_val - old_val)* (1 - e^(-dt/TCONST)) const ACC_PRESSURE_SENSOR_FILTER_TIMECONST: f64 = 0.1; //Time constant of the filter used to measure brake circuit pressure pub fn new( @@ -118,7 +114,9 @@ impl BrakeCircuit { ), volume_to_actuator_accumulator: Volume::new::(0.), volume_to_res_accumulator: Volume::new::(0.), - accumulator_fluid_pressure_sensor_filtered: Pressure::new::(0.0), //Pressure measured after accumulator in brake circuit + + // Pressure measured after accumulator in brake circuit + accumulator_fluid_pressure_sensor_filtered: Pressure::new::(0.0), } } @@ -148,7 +146,7 @@ impl BrakeCircuit { self.volume_to_res_accumulator += delta_vol.abs(); } } else { - //Else case if no accumulator: we just take deltavol needed or return it back to res + // Else case if no accumulator: we just take deltavol needed or return it back to res if delta_vol > Volume::new::(0.) { self.volume_to_actuator_accumulator += delta_vol; } else { @@ -233,7 +231,9 @@ pub struct AutoBrakeController { pub current_accel_error: Acceleration, accel_error_prev: Acceleration, current_integral_term: f64, - current_brake_dmnd: f64, //Controller brake demand to satisfy autobrake mode [0:1] + + // Controller brake demand to satisfy autobrake mode [0:1] + current_brake_dmnd: f64, is_enabled: bool, } @@ -319,7 +319,7 @@ mod tests { }; #[test] - //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s + // Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn brake_state_at_init() { let init_max_vol = Volume::new::(1.5); let brake_circuit_unprimed = BrakeCircuit::new( From a09bae737f1c6127c015a70b18d8729de6241df9 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 14 Apr 2021 11:40:50 +0200 Subject: [PATCH 073/122] New braking actuator --- .../systems/src/hydraulic/brakecircuit.rs | 255 ++++++++++++++---- 1 file changed, 203 insertions(+), 52 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 31f4c5ed748..0efe22bf556 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -16,6 +16,94 @@ pub trait ActuatorHydInterface { fn get_reservoir_return(&self) -> Volume; } +pub struct BrakeActuator { + total_displacement: Volume, + base_speed: f64, + + current_position: f64, + + required_position: f64, + + volume_to_actuator_accumulator: Volume, + volume_to_res_accumulator: Volume, + + pressure_received: Pressure, +} + +impl BrakeActuator { + const ACTUATOR_BASE_SPEED: f64 = 1.; // movement in percent/100 per second. 1 means 0 to 1 in 1s + const MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI: f64 = 100.; + const NOMINAL_PRESSURE_FOR_NORMAL_BRAKE_OPERATION_PSI: f64 = 2800.; + + pub 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.), + pressure_received: Pressure::new::(0.), + } + } + + pub fn set_position_demand(&mut self, required_position: f64) { + self.required_position = required_position; + } + + pub fn get_applied_brake_pressure(&self) -> Pressure { + self.pressure_received * self.current_position + } + + pub fn update(&mut self, delta_time: &Duration, received_pressure: Pressure) { + self.pressure_received = received_pressure; + let final_delta_position = self.update_position(delta_time, 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; + } + } + + pub 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, delta_time: &Duration, loop_pressure: Pressure) -> f64 { + let delta_position_required = self.required_position - self.current_position; + + let mut new_position = self.current_position; + if delta_position_required > 0.001 { + let mut speed_modifier = (loop_pressure.get::() + - Self::MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI) + / Self::NOMINAL_PRESSURE_FOR_NORMAL_BRAKE_OPERATION_PSI; + speed_modifier = speed_modifier.min(1.).max(0.); + new_position = + self.current_position + delta_time.as_secs_f64() * self.base_speed * speed_modifier; + new_position = new_position.min(self.current_position + delta_position_required); + } else if delta_position_required < -0.001 { + new_position = self.current_position - delta_time.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 ActuatorHydInterface for BrakeActuator { + fn get_used_volume(&self) -> Volume { + self.volume_to_actuator_accumulator + } + fn get_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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). // So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position @@ -25,15 +113,11 @@ pub struct BrakeCircuit { id_right_press: String, id_acc_press: String, - // Total volume used when at max braking position. - // Simple model for now we assume at max braking we have max brake displacement - total_displacement: Volume, + left_brake_actuator: BrakeActuator, + right_brake_actuator: BrakeActuator, - // Actuator position //TODO make this more dynamic with a per wheel basis instead of left/right? - current_brake_position_left: f64, demanded_brake_position_left: f64, pressure_applied_left: Pressure, - current_brake_position_right: f64, demanded_brake_position_right: f64, pressure_applied_right: Pressure, @@ -96,11 +180,12 @@ impl BrakeCircuit { id_right_press: format!("HYD_BRAKE_{}_RIGHT_PRESS", id), id_acc_press: format!("HYD_BRAKE_{}_ACC_PRESS", id), - total_displacement, - current_brake_position_left: 0.0, + // 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), - current_brake_position_right: 0.0, demanded_brake_position_right: 0.0, pressure_applied_right: Pressure::new::(0.0), has_accumulator: has_accu, @@ -120,10 +205,31 @@ impl BrakeCircuit { } } + fn update_brake_actuators(&mut self, delta_time: &Duration, 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); + + self.left_brake_actuator.update(delta_time, hyd_pressure); + self.right_brake_actuator.update(delta_time, hyd_pressure); + } + pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydraulicLoop) { - let delta_vol = ((self.demanded_brake_position_left - self.current_brake_position_left) - + (self.demanded_brake_position_right - self.current_brake_position_right)) - * self.total_displacement; + + // The pressure available in brakes is the one of accumulator only if accumulator has fluid + let mut actual_pressure_available = Pressure::new::(0.); + if self.accumulator.get_fluid_volume() > Volume::new::(0.) { + actual_pressure_available = self.accumulator.get_raw_gas_press(); + } else { + actual_pressure_available = hyd_loop.get_pressure(); + } + + // Moving brake actuators from pressure available + self.update_brake_actuators(delta_time, actual_pressure_available); + + let delta_vol = self.left_brake_actuator.get_used_volume() + + self.right_brake_actuator.get_used_volume(); if self.has_accumulator { let mut volume_into_accumulator = Volume::new::(0.); @@ -133,59 +239,40 @@ impl BrakeCircuit { hyd_loop.loop_pressure, ); - // Volume that just came into accumulator is taken from hydraulic loop through olume_to_actuator interface + // 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); - if volume_from_acc <= Volume::new::(0.0000001) { - self.demanded_brake_position_left = self.current_brake_position_left; - self.demanded_brake_position_right = self.current_brake_position_right; - } - } else { - self.volume_to_res_accumulator += delta_vol.abs(); + 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 if delta_vol > Volume::new::(0.) { self.volume_to_actuator_accumulator += delta_vol; } else { - self.volume_to_res_accumulator += delta_vol.abs(); + self.volume_to_res_accumulator += + self.left_brake_actuator.get_reservoir_return(); + self.volume_to_res_accumulator += + self.right_brake_actuator.get_reservoir_return(); } } - if self.accumulator.get_fluid_volume() > Volume::new::(0.) { - self.pressure_applied_left = - self.accumulator.get_raw_gas_press() * self.demanded_brake_position_left; - self.pressure_applied_right = - self.accumulator.get_raw_gas_press() * self.demanded_brake_position_right; - - self.accumulator_fluid_pressure_sensor_filtered = self - .accumulator_fluid_pressure_sensor_filtered - + (self.accumulator.get_raw_gas_press() - - self.accumulator_fluid_pressure_sensor_filtered) - * (1. - - E.powf( - -delta_time.as_secs_f64() - / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, - )); - } else { - self.pressure_applied_left = - hyd_loop.get_pressure() * self.demanded_brake_position_left; - self.pressure_applied_right = - hyd_loop.get_pressure() * self.demanded_brake_position_right; - - self.accumulator_fluid_pressure_sensor_filtered = self - .accumulator_fluid_pressure_sensor_filtered - + (hyd_loop.get_pressure() - self.accumulator_fluid_pressure_sensor_filtered) - * (1. - - E.powf( - -delta_time.as_secs_f64() - / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, - )); - } - self.current_brake_position_left = self.demanded_brake_position_left; - self.current_brake_position_right = self.demanded_brake_position_right; + 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( + -delta_time.as_secs_f64() + / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, + )); } pub fn set_brake_demand_left(&mut self, brake_ratio: f64) { @@ -318,6 +405,70 @@ mod tests { volume::gallon, }; + #[test] + // Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s + 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(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); + 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 loop_idx in 0..15 { + brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); + println!( + "Loop {}, position: {}", + loop_idx, brake_actuator.current_position + ); + } + + 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 loop_idx in 0..15 { + brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); + println!( + "Loop {}, position: {}", + loop_idx, brake_actuator.current_position + ); + } + + // 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] // Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn brake_state_at_init() { From 42537152b10f068b1ea5565c952cbb35071d4a16 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 14 Apr 2021 12:57:06 +0200 Subject: [PATCH 074/122] Corrected reservoir return --- .../systems/src/hydraulic/brakecircuit.rs | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 0efe22bf556..47d20fe6935 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -249,16 +249,14 @@ impl BrakeCircuit { } } else { // Else case if no accumulator: we just take deltavol needed or return it back to res - if delta_vol > Volume::new::(0.) { - self.volume_to_actuator_accumulator += delta_vol; - } else { - self.volume_to_res_accumulator += - self.left_brake_actuator.get_reservoir_return(); - self.volume_to_res_accumulator += - self.right_brake_actuator.get_reservoir_return(); - } + self.volume_to_actuator_accumulator += delta_vol; } + self.volume_to_res_accumulator += + self.left_brake_actuator.get_reservoir_return(); + self.volume_to_res_accumulator += + self.right_brake_actuator.get_reservoir_return(); + self.left_brake_actuator.reset_accumulators(); self.right_brake_actuator.reset_accumulators(); @@ -437,10 +435,6 @@ mod tests { brake_actuator.set_position_demand(-2.); for loop_idx in 0..15 { brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); - println!( - "Loop {}, position: {}", - loop_idx, brake_actuator.current_position - ); } assert!(brake_actuator.current_position <= 0.01); @@ -456,10 +450,6 @@ mod tests { for loop_idx in 0..15 { brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); - println!( - "Loop {}, position: {}", - loop_idx, brake_actuator.current_position - ); } // We should not be able to move actuator @@ -536,7 +526,7 @@ mod tests { ); brake_circuit_primed.set_brake_demand_left(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= Pressure::new::(50.)); @@ -544,7 +534,7 @@ mod tests { brake_circuit_primed.set_brake_demand_left(0.0); brake_circuit_primed.set_brake_demand_right(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); assert!(brake_circuit_primed.accumulator.get_fluid_volume() >= Volume::new::(0.1)); @@ -578,14 +568,14 @@ mod tests { ); brake_circuit_primed.set_brake_demand_left(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= 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(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); assert!(brake_circuit_primed.accumulator.get_fluid_volume() == Volume::new::(0.0)); From 3c231c5a0c202da140eedc12849ee02e4b4593de Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 14 Apr 2021 13:29:24 +0200 Subject: [PATCH 075/122] Clippy errors --- src/systems/a320_systems/src/hydraulic.rs | 14 ++++++-------- src/systems/systems/src/hydraulic/brakecircuit.rs | 14 +++++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 4f5352a502e..eed2a2429e7 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -282,17 +282,15 @@ impl A320Hydraulic { } fn update_green_actuators_volume(&mut self) { - // Actuator interaction given as example here needs extensive tests in another PR - // self.green_loop - // .update_actuator_volumes(&self.braking_circuit_norm); - // self.braking_circuit_norm.reset_accumulators(); + self.green_loop + .update_actuator_volumes(&self.braking_circuit_norm); + self.braking_circuit_norm.reset_accumulators(); } fn update_yellow_actuators_volume(&mut self) { - // Actuator interaction given as example here needs extensive tests in another PR - // self.yellow_loop - // .update_actuator_volumes(&self.braking_circuit_altn); - // self.braking_circuit_altn.reset_accumulators(); + self.yellow_loop + .update_actuator_volumes(&self.braking_circuit_altn); + self.braking_circuit_altn.reset_accumulators(); } fn update_blue_actuators_volume(&mut self) {} diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 47d20fe6935..e3278e0d2ba 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -216,16 +216,14 @@ impl BrakeCircuit { } pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydraulicLoop) { - // The pressure available in brakes is the one of accumulator only if accumulator has fluid - let mut actual_pressure_available = Pressure::new::(0.); + let actual_pressure_available: Pressure; if self.accumulator.get_fluid_volume() > Volume::new::(0.) { actual_pressure_available = self.accumulator.get_raw_gas_press(); } else { actual_pressure_available = hyd_loop.get_pressure(); } - // Moving brake actuators from pressure available self.update_brake_actuators(delta_time, actual_pressure_available); let delta_vol = self.left_brake_actuator.get_used_volume() @@ -252,10 +250,8 @@ impl BrakeCircuit { self.volume_to_actuator_accumulator += delta_vol; } - self.volume_to_res_accumulator += - self.left_brake_actuator.get_reservoir_return(); - self.volume_to_res_accumulator += - self.right_brake_actuator.get_reservoir_return(); + self.volume_to_res_accumulator += self.left_brake_actuator.get_reservoir_return(); + self.volume_to_res_accumulator += self.right_brake_actuator.get_reservoir_return(); self.left_brake_actuator.reset_accumulators(); self.right_brake_actuator.reset_accumulators(); @@ -433,7 +429,7 @@ mod tests { brake_actuator.reset_accumulators(); brake_actuator.set_position_demand(-2.); - for loop_idx in 0..15 { + for _ in 0..15 { brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); } @@ -448,7 +444,7 @@ mod tests { brake_actuator.reset_accumulators(); brake_actuator.set_position_demand(1.2); - for loop_idx in 0..15 { + for _ in 0..15 { brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); } From ffda4da975bdd6a8ec41ffc1b5d5411a140cf4ab Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 14 Apr 2021 14:36:12 +0200 Subject: [PATCH 076/122] test updates for new brakes --- src/systems/a320_systems/src/hydraulic.rs | 167 ++++++++++++++++-- .../systems/src/hydraulic/brakecircuit.rs | 5 + 2 files changed, 161 insertions(+), 11 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index eed2a2429e7..ddc4bda5eb9 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1044,6 +1044,10 @@ mod tests { } } + fn get_yellow_brake_accumulator_fluid_volume(&self) -> Volume { + self.hydraulics.braking_circuit_altn.get_acc_fluid_volume() + } + fn is_nws_pin_inserted(&self) -> bool { self.hydraulics.nose_wheel_steering_pin_is_inserted() } @@ -1171,6 +1175,14 @@ mod tests { ) } + 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 @@ -1192,6 +1204,10 @@ mod tests { ) } + fn get_brake_yellow_accumulator_fluid_volume(&mut self) -> Volume { + self.aircraft.get_yellow_brake_accumulator_fluid_volume() + } + fn is_fire_valve_eng1_closed(&mut self) -> bool { !self .simulation_test_bed @@ -1700,6 +1716,8 @@ mod tests { .engines_off() .on_the_ground() .set_cold_dark_inputs() + .set_ptu_state(false) + .set_park_brake(false) // Park brake off to not use fluid in brakes .run_one_tick(); // Starting epump wait for pressure rise to make sure system is primed including brake accumulator @@ -1720,6 +1738,9 @@ mod tests { 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..51 { test_bed = test_bed @@ -1738,13 +1759,6 @@ mod tests { assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); assert!(test_bed.yellow_pressure() > Pressure::new::(-50.)); - current_res_level = test_bed.get_yellow_reservoir_volume(); - let mut reservoir_difference = reservoir_level_after_priming - current_res_level; - //println!("---Reservoir deviation: {}", reservoir_difference.get::()); - - // Make sure no more deviation than 0.001 gallon is lost after full pressure and unpressurized states - assert!(reservoir_difference.get::().abs() < 0.001); - test_bed = test_bed .set_yellow_e_pump(false) .run_waiting_for(Duration::from_secs(50)); @@ -1752,17 +1766,148 @@ mod tests { 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(); - current_res_level = test_bed.get_yellow_reservoir_volume(); - reservoir_difference = reservoir_level_after_priming - current_res_level; - //println!("---Reservoir deviation: {}", reservoir_difference.get::()); - assert!(reservoir_difference.get::().abs() < 0.001); + 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_test() { + 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::(50.)) + .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..101 { + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .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_test() { + 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..51 { + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .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::(50.)) + .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] diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index e3278e0d2ba..2543e16212e 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -282,10 +282,15 @@ impl BrakeCircuit { pub fn get_brake_pressure_right(&self) -> Pressure { self.pressure_applied_right } + pub fn get_acc_pressure(&self) -> Pressure { self.accumulator_fluid_pressure_sensor_filtered } + pub fn get_acc_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.); From d3c1ad25c44f89dd42a366598d7482d251203a61 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 14 Apr 2021 15:13:25 +0200 Subject: [PATCH 077/122] Updated brake actuator response --- src/systems/a320_systems/src/hydraulic.rs | 6 +++--- src/systems/systems/src/hydraulic/brakecircuit.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index ddc4bda5eb9..9fbf7b08679 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -97,7 +97,7 @@ impl A320Hydraulic { Volume::new::(19.75), Volume::new::(19.81), Volume::new::(10.0), - Volume::new::(3.15), + Volume::new::(3.25), HydFluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), @@ -131,14 +131,14 @@ impl A320Hydraulic { "NORM", Volume::new::(0.), Volume::new::(0.), - Volume::new::(0.08), + Volume::new::(0.13), ), braking_circuit_altn: BrakeCircuit::new( "ALTN", Volume::new::(1.5), Volume::new::(0.0), - Volume::new::(0.08), + Volume::new::(0.13), ), total_sim_time_elapsed: Duration::new(0, 0), diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 2543e16212e..fd7bd5b89f4 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -33,7 +33,7 @@ pub struct BrakeActuator { impl BrakeActuator { const ACTUATOR_BASE_SPEED: f64 = 1.; // movement in percent/100 per second. 1 means 0 to 1 in 1s const MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI: f64 = 100.; - const NOMINAL_PRESSURE_FOR_NORMAL_BRAKE_OPERATION_PSI: f64 = 2800.; + const NOMINAL_PRESSURE_FOR_NORMAL_BRAKE_OPERATION_PSI: f64 = 1000.; pub fn new(total_displacement: Volume) -> Self { Self { From 80612b79496c468a2ba1fc15841d9d2ecaed8584 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 14 Apr 2021 19:32:33 +0200 Subject: [PATCH 078/122] Improved brake actuator pressure handling --- .../systems/src/hydraulic/brakecircuit.rs | 77 +++++++++++++++---- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index fd7bd5b89f4..7eb9cfae50c 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -26,14 +26,12 @@ pub struct BrakeActuator { volume_to_actuator_accumulator: Volume, volume_to_res_accumulator: Volume, - - pressure_received: Pressure, } impl BrakeActuator { const ACTUATOR_BASE_SPEED: f64 = 1.; // movement in percent/100 per second. 1 means 0 to 1 in 1s - const MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI: f64 = 100.; - const NOMINAL_PRESSURE_FOR_NORMAL_BRAKE_OPERATION_PSI: f64 = 1000.; + const MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI: f64 = 50.; + const PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI: f64 = 3100.; pub fn new(total_displacement: Volume) -> Self { Self { @@ -43,7 +41,6 @@ impl BrakeActuator { required_position: 0., volume_to_actuator_accumulator: Volume::new::(0.), volume_to_res_accumulator: Volume::new::(0.), - pressure_received: Pressure::new::(0.), } } @@ -51,12 +48,21 @@ impl BrakeActuator { 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. + } + } + pub fn get_applied_brake_pressure(&self) -> Pressure { - self.pressure_received * self.current_position + Pressure::new::(Self::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI) * self.current_position } pub fn update(&mut self, delta_time: &Duration, received_pressure: Pressure) { - self.pressure_received = received_pressure; let final_delta_position = self.update_position(delta_time, received_pressure); if final_delta_position > 0. { @@ -72,16 +78,15 @@ impl BrakeActuator { } fn update_position(&mut self, delta_time: &Duration, loop_pressure: Pressure) -> f64 { - let delta_position_required = self.required_position - self.current_position; + // 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 { - let mut speed_modifier = (loop_pressure.get::() - - Self::MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI) - / Self::NOMINAL_PRESSURE_FOR_NORMAL_BRAKE_OPERATION_PSI; - speed_modifier = speed_modifier.min(1.).max(0.); - new_position = - self.current_position + delta_time.as_secs_f64() * self.base_speed * speed_modifier; + new_position = self.current_position + delta_time.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 - delta_time.as_secs_f64() * self.base_speed; @@ -405,7 +410,6 @@ mod tests { }; #[test] - // Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn brake_actuator_movement() { let mut brake_actuator = BrakeActuator::new(Volume::new::(0.04)); @@ -415,7 +419,10 @@ mod tests { brake_actuator.set_position_demand(1.2); for loop_idx in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); + brake_actuator.update( + &Duration::from_secs_f64(0.1), + Pressure::new::(BrakeActuator::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI), + ); println!( "Loop {}, position: {}", loop_idx, brake_actuator.current_position @@ -461,7 +468,43 @@ mod tests { } #[test] - // Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s + 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(&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(&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( From f07e1e4e06396b9c50d6bf27b05c96650daf91a7 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 15 Apr 2021 15:47:41 +0200 Subject: [PATCH 079/122] Braking logic simplified/ Pressure limitation added --- src/systems/a320_systems/src/hydraulic.rs | 109 +++++++++--------- .../systems/src/hydraulic/brakecircuit.rs | 74 +++++++++++- 2 files changed, 129 insertions(+), 54 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 9fbf7b08679..3a1e2671214 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -306,8 +306,11 @@ impl A320Hydraulic { min_hyd_loop_timestep: Duration, ) { // Process brake logic (which circuit brakes) and send brake demands (how much) - self.hyd_brake_logic - .update_brake_demands(&min_hyd_loop_timestep, &self.green_loop); + self.hyd_brake_logic.update_brake_demands(&self.green_loop); + 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, @@ -745,80 +748,81 @@ pub struct A320HydraulicBrakingLogic { } //Implements brakes computers logic impl A320HydraulicBrakingLogic { - const PARK_BRAKE_DEMAND_DYNAMIC: f64 = 0.8; //Dynamic of the parking brake application/removal in (percent/100) per s - const LOW_PASS_FILTER_BRAKE_COMMAND: f64 = 0.85; //Low pass filter on all brake commands - const MIN_PRESSURE_BRAKE_ALTN: f64 = 1500.; //Minimum pressure until main switched on ALTN brakes + const MIN_PRESSURE_BRAKE_ALTN: f64 = 2000.; //Minimum pressure until main switched on ALTN brakes pub fn new() -> A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { parking_brake_demand: true, //Position of parking brake lever weight_on_wheels: true, - left_brake_pilot_input: 1.0, // read from brake pedals - right_brake_pilot_input: 1.0, // read from brake pedals - left_brake_green_output: 0.0, //Actual command sent to left green circuit - left_brake_yellow_output: 1.0, //Actual command sent to left yellow circuit. Init 1 as considering park brake on on init - right_brake_green_output: 0.0, //Actual command sent to right green circuit - right_brake_yellow_output: 1.0, //Actual command sent to right yellow circuit. Init 1 as considering park brake on on init + left_brake_pilot_input: 0.0, + right_brake_pilot_input: 0.0, + left_brake_green_output: 0.0, // Actual command sent to left green circuit + left_brake_yellow_output: 1.0, // Actual command sent to left yellow circuit. Init 1 as considering park brake on on init + right_brake_green_output: 0.0, // Actual command sent to right green circuit + right_brake_yellow_output: 1.0, // Actual command sent to right yellow circuit. Init 1 as considering park brake on on init anti_skid_activated: true, autobrakes_setting: 0, } } - //Updates final brake demands per hydraulic loop based on pilot pedal demands - //TODO: think about where to build those brake demands from autobrake if not from brake pedals - pub fn update_brake_demands( + pub fn update_brake_pressure_limitation( &mut self, - delta_time_update: &Duration, - green_loop: &HydraulicLoop, + 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 e + norm_brk.set_brake_limit_ena(true); + norm_brk.set_brake_press_limit(Pressure::new::(2538.)); + + if self.parking_brake_demand { + altn_brk.set_brake_limit_ena(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_ena(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_ena(true); + } + } + + // Updates final brake demands per hydraulic loop based on pilot pedal demands + //TODO: think about where to build those brake demands from autobrake if not from brake pedals + pub fn update_brake_demands(&mut self, green_loop: &HydraulicLoop) { let green_used_for_brakes = green_loop.get_pressure() //TODO Check this logic > Pressure::new::(A320HydraulicBrakingLogic::MIN_PRESSURE_BRAKE_ALTN ) && self.anti_skid_activated && !self.parking_brake_demand; - let dynamic_increment = - A320HydraulicBrakingLogic::PARK_BRAKE_DEMAND_DYNAMIC * delta_time_update.as_secs_f64(); - if green_used_for_brakes { - self.left_brake_green_output = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.left_brake_pilot_input - + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.left_brake_green_output; - self.right_brake_green_output = A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.right_brake_pilot_input - + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.right_brake_green_output; - - self.left_brake_yellow_output -= dynamic_increment; - self.right_brake_yellow_output -= dynamic_increment; + 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 = - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.left_brake_pilot_input - + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.left_brake_yellow_output; - self.right_brake_yellow_output = - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND - * self.right_brake_pilot_input - + (1.0 - A320HydraulicBrakingLogic::LOW_PASS_FILTER_BRAKE_COMMAND) - * self.right_brake_yellow_output; - if !self.anti_skid_activated { - self.left_brake_yellow_output = self.left_brake_yellow_output.min(0.5); //0.5 is temporary implementation of pressure limitation around 1000psi - self.right_brake_yellow_output = self.right_brake_yellow_output.min(0.5); - //0.5 is temporary implementation of pressure limitation around 1000psi - } + 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 += dynamic_increment; - self.left_brake_yellow_output = self.left_brake_yellow_output.min(0.7); //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes - self.right_brake_yellow_output += dynamic_increment; - self.right_brake_yellow_output = self.right_brake_yellow_output.min(0.7); - //0.7 is temporary implementation of pressure limitation around 2000psi for parking brakes + self.left_brake_yellow_output = 1.; + self.right_brake_yellow_output = 1.; } - self.left_brake_green_output -= dynamic_increment; - self.right_brake_green_output -= dynamic_increment; } //limiting final values @@ -835,6 +839,7 @@ impl A320HydraulicBrakingLogic { altn.set_brake_demand_right(self.right_brake_yellow_output); } } + impl SimulationElement for A320HydraulicBrakingLogic { fn accept(&mut self, visitor: &mut T) { visitor.visit(self); diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 7eb9cfae50c..fff6ff58a72 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -126,6 +126,9 @@ pub struct BrakeCircuit { 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: HydraulicAccumulator, @@ -193,6 +196,8 @@ impl BrakeCircuit { 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: HydraulicAccumulator::new( Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), @@ -210,14 +215,31 @@ impl BrakeCircuit { } } + pub fn set_brake_press_limit(&mut self, pressure_limit: Pressure) { + self.pressure_limitation = pressure_limit; + } + + pub fn set_brake_limit_ena(&mut self, is_pressure_limit_active: bool) { + self.pressure_limitation_active = is_pressure_limit_active; + } + fn update_brake_actuators(&mut self, delta_time: &Duration, 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); - self.left_brake_actuator.update(delta_time, hyd_pressure); - self.right_brake_actuator.update(delta_time, hyd_pressure); + 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(delta_time, actual_max_allowed_pressure); + self.right_brake_actuator + .update(delta_time, actual_max_allowed_pressure); } pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydraulicLoop) { @@ -625,6 +647,54 @@ mod tests { assert!(brake_circuit_primed.accumulator.get_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(&Duration::from_secs_f64(5.), &hyd_loop); + + assert!( + brake_circuit_primed.get_brake_pressure_left() + + brake_circuit_primed.get_brake_pressure_right() + < 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(&Duration::from_secs_f64(1.5), &hyd_loop); + + assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2900.)); + assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); + + let pressure_limit = Pressure::new::(1200.); + brake_circuit_primed.set_brake_press_limit(pressure_limit); + brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2900.)); + assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); + + brake_circuit_primed.set_brake_limit_ena(true); + brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + + // Now we limit to 1200 but pressure shouldn't drop instantly + assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2500.)); + assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); + + brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + + // After one second it should have reached the lower limit + assert!(brake_circuit_primed.get_brake_pressure_left() <= pressure_limit); + assert!(brake_circuit_primed.get_brake_pressure_right() <= pressure_limit); + } + #[test] fn auto_brake_controller() { let mut controller = AutoBrakeController::new(vec![ From 9758444208299782accfe952f7718ca3cdb64d01 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 15 Apr 2021 20:46:06 +0200 Subject: [PATCH 080/122] Updated cold start state with non empty accumulator --- src/systems/a320_systems/src/hydraulic.rs | 57 +++++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 3a1e2671214..6b2f1adafa8 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -94,10 +94,10 @@ impl A320Hydraulic { "YELLOW", false, true, - Volume::new::(19.75), + Volume::new::(19.81), Volume::new::(19.81), Volume::new::(10.0), - Volume::new::(3.25), + Volume::new::(3.6), HydFluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), @@ -137,7 +137,7 @@ impl A320Hydraulic { braking_circuit_altn: BrakeCircuit::new( "ALTN", Volume::new::(1.5), - Volume::new::(0.0), + Volume::new::(0.5), Volume::new::(0.13), ), @@ -1986,25 +1986,43 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - // Accumulator empty on cold start - assert!(test_bed.get_brake_yellow_accumulator_pressure() < Pressure::new::(50.)); - // No brakes + // 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() < Pressure::new::(50.)); - assert!(test_bed.get_brake_right_yellow_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 + // 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(1)); + .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() < Pressure::new::(50.)); - assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); - assert!(test_bed.get_brake_yellow_accumulator_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 @@ -2046,11 +2064,20 @@ mod tests { .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() < Pressure::new::(50.)); - assert!(test_bed.get_brake_right_yellow_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.)) From c448e0bc4d750024c8ab22a1e066c8982dc1063f Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 20 Apr 2021 20:44:35 +0200 Subject: [PATCH 081/122] Added flow filtering on accumulator --- .../systems/src/hydraulic/brakecircuit.rs | 8 +++-- src/systems/systems/src/hydraulic/mod.rs | 31 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index fff6ff58a72..04db8c34f0b 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -164,9 +164,11 @@ impl SimulationElement for BrakeCircuit { impl BrakeCircuit { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1000.0; // Nitrogen PSI - const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = - [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; - const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.01, 0.016, 0.02, 0.04, 0.1, 0.15, 0.35, 0.5]; + 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)) const ACC_PRESSURE_SENSOR_FILTER_TIMECONST: f64 = 0.1; //Time constant of the filter used to measure brake circuit pressure diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index c0f28c4928b..524d897561d 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -232,18 +232,22 @@ struct HydraulicAccumulator { gas_pressure: Pressure, gas_volume: Volume, fluid_volume: Volume, - press_breakpoints: [f64; 9], - flow_carac: [f64; 9], + current_flow: VolumeRate, + current_delta_vol: Volume, + press_breakpoints: [f64; 10], + flow_carac: [f64; 10], has_control_valve: bool, } impl HydraulicAccumulator { + const FLOW_DYNAMIC_LOW_PASS: f64 = 0.7; + pub fn new( gas_precharge: Pressure, total_volume: Volume, fluid_vol_at_init: Volume, - press_breakpoints: [f64; 9], - flow_carac: [f64; 9], + 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 @@ -258,6 +262,8 @@ impl HydraulicAccumulator { 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, @@ -266,11 +272,13 @@ impl HydraulicAccumulator { fn update(&mut self, delta_time: &Duration, delta_vol: &mut Volume, loop_pressure: Pressure) { let accumulator_delta_press = self.gas_pressure - loop_pressure; - let flow_variation = VolumeRate::new::(interpolation( + 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 @@ -281,6 +289,8 @@ impl HydraulicAccumulator { .min(flow_variation * Time::new::(delta_time.as_secs_f64())); 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 @@ -288,9 +298,12 @@ impl HydraulicAccumulator { .max(flow_variation * Time::new::(delta_time.as_secs_f64())); 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 / Time::new::(delta_time.as_secs_f64()); self.gas_pressure = (self.gas_init_precharge * self.total_volume) / (self.total_volume - self.fluid_volume); } @@ -352,9 +365,11 @@ impl HydraulicLoop { const DELTA_VOL_LOW_PASS_FILTER: f64 = 0.4; - const ACCUMULATOR_PRESS_BREAKPTS: [f64; 9] = - [0.0, 5.0, 10.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 10000.0]; - const ACCUMULATOR_FLOW_CARAC: [f64; 9] = [0.0, 0.005, 0.008, 0.01, 0.02, 0.08, 0.15, 0.35, 0.5]; + 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( From a5fbd046a14b826d30e2d2cf85cb5b410c146a2b Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 21 Apr 2021 19:44:53 +0200 Subject: [PATCH 082/122] Simplified PTU deactivation logic --- src/systems/a320_systems/src/hydraulic.rs | 28 +++++++------ src/systems/systems/src/hydraulic/mod.rs | 49 +++++++++++------------ 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 6b2f1adafa8..3cde3b988f2 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1595,15 +1595,16 @@ mod tests { test_bed = test_bed .start_eng1(Ratio::new::(50.)) .run_one_tick(); - //ALMOST No pressure + + // ALMOST No pressure 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::(500.)); //Blue is auto run assert!(!test_bed.is_yellow_pressurised()); - assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); - //Waiting for 5s pressure hsould be at 3000 psi + // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed .start_eng1(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(5)); @@ -1615,7 +1616,7 @@ mod tests { assert!(!test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); - //Stoping engine, pressure should fall in 20s + // Stoping engine, pressure should fall in 20s test_bed = test_bed .stop_eng1() .run_waiting_for(Duration::from_secs(20)); @@ -1637,22 +1638,23 @@ mod tests { .set_ptu_state(false) .run_one_tick(); - //Starting eng 1 + // Starting eng 1 test_bed = test_bed .start_eng1(Ratio::new::(50.)) .start_eng2(Ratio::new::(50.)) .run_one_tick(); - //ALMOST No pressure + + // 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 + // 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 + // Stoping edp1, pressure should fall in 20s test_bed = test_bed .set_green_ed_pump(false) .run_waiting_for(Duration::from_secs(20)); @@ -1660,7 +1662,7 @@ mod tests { assert!(test_bed.green_pressure() < Pressure::new::(500.)); assert!(test_bed.yellow_pressure() > Pressure::new::(2900.)); - //Stoping edp2, pressure should fall in 20s + // Stoping edp2, pressure should fall in 20s test_bed = test_bed .set_yellow_ed_pump(false) .run_waiting_for(Duration::from_secs(20)); @@ -1677,11 +1679,11 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //Starting eng 1 + // Starting eng 1 test_bed = test_bed .start_eng2(Ratio::new::(50.)) .run_one_tick(); - //ALMOST No pressure + // ALMOST No pressure assert!(!test_bed.is_green_pressurised()); assert!(test_bed.green_pressure() < Pressure::new::(50.)); assert!(!test_bed.is_blue_pressurised()); @@ -1689,7 +1691,7 @@ mod tests { assert!(!test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); - //Waiting for 5s pressure hsould be at 3000 psi + // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed .start_eng2(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(5)); @@ -1701,7 +1703,7 @@ mod tests { assert!(test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() > Pressure::new::(2800.)); - //Stoping engine, pressure should fall in 20s + // Stoping engine, pressure should fall in 20s test_bed = test_bed .stop_eng2() .run_waiting_for(Duration::from_secs(20)); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 524d897561d..62f99f63f1f 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -96,12 +96,15 @@ pub struct PowerTransferUnit { 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.2; - const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.2; + const FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE: f64 = 0.3; + const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.3; + + const EFFICIENCY_LEFT_TO_RIGHT: f64 = 0.81; + const EFFICIENCY_RIGHT_TO_LEFT: f64 = 0.70; //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.8; + const AGRESSIVENESS_FACTOR: f64 = 0.72; pub fn new() -> Self { Self { @@ -144,11 +147,17 @@ impl PowerTransferUnit { let delta_p = loop_left.get_pressure() - loop_right.get_pressure(); - //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 using PressureSource trait? - if self.is_active_left || (!self.is_active_right && delta_p.get::() > 500.0) { + 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; @@ -164,11 +173,14 @@ impl PowerTransferUnit { * self.last_flow.get::(); self.flow_to_left = VolumeRate::new::(-vr); - self.flow_to_right = VolumeRate::new::(vr * 0.81); + 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 self.is_active_right || (!self.is_active_left && delta_p.get::() < -500.0) { + } 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; @@ -183,26 +195,13 @@ impl PowerTransferUnit { + (1.0 - Self::FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE) * self.last_flow.get::(); - self.flow_to_left = VolumeRate::new::(vr * 0.70); + 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; } - - //TODO REVIEW DEACTICATION LOGIC - if !self.is_enabled - || self.is_active_right && loop_left.loop_pressure.get::() > 2950.0 - || self.is_active_left && loop_right.loop_pressure.get::() > 2950.0 - || self.is_active_right && loop_right.loop_pressure.get::() < 500.0 - || self.is_active_left && loop_left.loop_pressure.get::() < 500.0 - { - 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); - } } } impl SimulationElement for PowerTransferUnit { From 552b6319f3b22167f5796298f2258da5d6562a4e Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 23 Apr 2021 09:17:35 +0200 Subject: [PATCH 083/122] Knowledge database to source control --- .../study/Docs/Knowledge_Data_Base.xlsx | Bin 0 -> 394235 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/systems/systems/src/hydraulic/study/Docs/Knowledge_Data_Base.xlsx 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 0000000000000000000000000000000000000000..7087c0f4b6cbd0adeae8376c0d9e0cc40a4f2bad GIT binary patch literal 394235 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%0DA?I<3KYOa-AvZX;n z)xwQu^K$dvY_4L>ll@T@K)4f5-0&{ZM0oj%vx(H4#nOVVm$%Bo@kZ_R5yPiTk|5iI zWx_rnpTRy&cK&^I^uG3vnCl?n?>pP#N?!Yv@{s@Q|46vMwA^$A&RP%&X7X8 zd^F-J#^}4Dty?B_)t0ird6&M_BASDL#@{Ca4`IT@U%x>5g*i#L zNbpI!4!4!Kp{s4gyKjbTJH&Ql2Ag8ZqM|?<(&$LBd-1z72UX(RcCa4sP2xt0eWi@voH zCgjAGu9^MF$svQT8?6+9OhcqrfFR;WmcqPEK&CwFJi6!rL7hfb=~~w{>qu7Vh4Nqrg%&kVh2-;R%eD582UM+6 zuzPQ#cwZhTMhXfwldr#-0anZVUr?Xc=vp4pS{0^S^ftOfzdfU5`IF4PXK^<2a<7d? zWsj#XgXUed8gpwMzF{vutRrVAvcN;QA2t&TTc5o~`aU9c^NE%OhddTpIO%Uk8IMOn zITyIyV`e2`fIaMFg}|o)Pq7U#`#iKre(xN{Y>ePLnK{3NC+b~t-oxosD=SQq^jnq% z#O_0qnHB1`3EUZFN6_q-r;?UuYqNl=z*<>}K2JS9MlBbpK?M9lq14=A$m^`KLZjD< zNb}
!B<`Et4rMwH2he4I38dZT$LS-$Dc%HsAj{j{G?k4tM5*rXUo@QWNJCtozdjb3Fio2Cz*LZg zxSV&o>cY0RyVd&qn@2Tww!GgeXTi`^1TFSZf2;Vb6VWUHF#t4fbQR^TZHbhf;`*d# zb{ZNKW7G?%U}!g!s*c@kf0Vc*f&Kj)RMiL3N0c2;l4nB%lgSUdk4_%B22pp4k;B7~ zW|Xi=k;6x;w|^xcKM=XyGVnZElVMq~`zX`2x83?pD2Pqs4FOY)ukH4ubP+Wr&{>K= zA?UXjwzhFc*m_i4fQL=zCgQGyLtoT_J;^kTZ)GU7sujc}Trw)lmBp0P9dz zCf%G7Uw9A}oz?`P(b)Gim5Xu2TsZU^DDZrX6;b={%W&2}R`WaZ)NTmV|GDx>qZ8cr z7ZT&uZeQLFWh%v?D80=n_@&S$8H+q*eM##0048*xTCgG<~l4heBSZ>q%opX7C z)Y9i01~+V`8pZKA)B;Q)Xr${)N&p*U&ImyfB^Ij{QYt7mR z*$_LW$78;3P8;{&oTHD^^jKyLGG;#Foik3T#l5{EN3cwF-9VN>Fal&Ajh{D4;8=IQ zTt9SPL^ES}UszCZPqKUr7;U8}OTuM1>7*P!GaI#{m2)v!YM;<4HyPlCk$$*!rbMiT z$%5M({Meq@Rs6>xK8XF3T8P2dFkpB5ghSEp3Dn|X7@1Q+P8;LG+sN&J?wb*oj!=dD zjo58F>AkgO``Nq5z@;Ke@NYZjYAY^iA*GN-%dpm z1l@j*u-qds44c%oUDd)dlx&fVbXaGyE#`#@*4gDBqQ<|gq%!7h#_zjl$@@7po-pxS z(uNX!E`iBk=1C26EP-VD3+HMj03;(`vYal}B&*qWW(+(p zslya0aUgs#-C|a?>%*bU(cB$o0S3e4Q|R5%wfFedebaSlJA(|~X{=_63_yEF{43J0nMZxlYL9yG^1uUOy za=R$m{VrBUE;_D9Mn-mqlS170Pjz4s+KMgt;82%m z11yu{Lgkx=5GF(QrPD8&Jl#oqYwEiGw(a@?05sCx`wERDyHCemEZ`Yos=jA2K7DSd zH?6X3;z&uUlQ(s1;Hu}oBch&jL&Y&(RyBWzRP>1Oy`j2e1Adr)g!JbG*dygodh@Lc zdA~%7qMjNv5a|0M;(pvqo}hqPRugyvxBnq+Ve%%5RdeM#1Sx@DMaCJ?t z4;pu=`B@NyQWvfgYC^L~?1pnCjr|encp@hpOUI?CLylUw-PDk)%I}avguR@wq8m}L zq?%^z&x(LR_o!C+Vbs=27u0nb&SI_w$_I_{_}u2i$sBe=dF zki@O&S<&2>ngg0m3yaEc6b^{>P{uxp-ZBHDub|NRX5X{5{@l6Ya5AbjUp4ibEd9SO zNxIb&2Y^a?4R6@0xC{O=B+TVhZQB07;mF~#*Z{bnz@?vC+HUhz=Bp5d9^Y|wOFp5x zgi*|-hi}~WVPjVz{DA5RYvev~o|O>N+$SUzH>utH=#$|b&mFC1gg=R5s z;8fezg9O&CR?dmV{!>{s9Y8swhw-cK4?iFO}lc1G3~IhI$PIt#j$;IZmhXYiPq zc0>d>PsHiZs^0F-reopS0@8hCh2f_664cT!+NJynfp;H~f zK5rA6s3bWW*3Eb2l0FzIiOw+`0EclKs zpj6dbkml9(QUZzhO0`*%c*EfB9Qo7!pRo4k3MtCL;W_dqMM0XB@%E06;20&U11Xoo zJjrRE2l;}y^T}~oysExp{WN2Ja8*xgx8BUb93c(W?L6U;`Wfa5kjJCIks(lTMOAio zT6Pn0FFPG}RF?C0>hx%(r3G7<*=0*3c6|1{fKh5_n;Uq6^$SJ+v7^YM+n|%xHg8DM z1tyUVWeoRd9c~%`3pq&L%L2dt2MS#=hc(1)S#9QlJ-W}xQytsMd8&-UOLLcUCh1*p zd)&GVH{Ck)dkJ4SP#=ymjV^CxAwyiguty8J?RmdQl?<8xI+%ocJYj}(YzfRK48`9W zcvQmE1dIKdl5Rdan8i=olA)y113tO8DoPoasSQwW05IND(6T)M&lLu5Gy38+VtG0L zQR2EI;X<~ErXmkL8l7am4N7aw5|vPcB!xTKV>}!QOwQcOz^r9KKQJY zCgp6bjk?ted&7M1S08BB{qDoyi5pY*A<|%ijli?#j5DiH%s$OoYcP7LsT8y9O=F-M zQM@lZ-lB$`!?wuNa0vYD!+>$&oIL_;qA8|WTX&rEYRpic=XXw-JaR+K zeKx-EG-ZN$xQdb!OuQVBKLTCV;UjsXf^mRGX18wJcC^Uwd+Qr9!{X8w_KcPun;~<7 znERS^rxdZ;yaVCe?{0l9r?@5L94@Dd!Hl#W`cU|?%^(Bkxx3vV<;H50|3%wdhDF&$ z|H2j^(kNXD0!qWsjg*qoT@pigx02FGBO#sA-JL^sN_ThrZg`&Ood0>R^Ywf|xtO_U z?!EWD_xi4b-brgR`ak`fq@hIjaifR`zJav}94ap; zDethOeg2`9-()r{i+4?8GRVWe`ekc$a_gz+t?Z%T-x}P8@`aRV(+~$65}GQ^D($Ia zgoD<$A;*Jp<80+~x|4km9OLtZugV;d7{8avb|SX|f#`z$*5eqi)8(whT1kZLGkZ!s zaU4Z%xO<~r`#&B(oHxJto`Ta^QSfa(B*q1Ex%Q-lsP&{9eAa zGF>BkUTcA!%3@VlusD3Te|VXVFjQi!B5yK=L+@Vm3}Klirw;vVG$ki}>6-RTujG*K zk<$vOR=CtuyE|y)`s~Ti=L%ox5;Z59jw6(=*5B?HgfGLVX+P>L}Y*E@fOxfRo|^}ztw!tj!QC#`&Z)<*em zi71;G^BdSU+kceC?<`aG?3asAR^C8@c4kmpm7*qpH3RIIi{UI4NM*Z*>M{73h?hN~ zaZMwVX|6H+jlUz{SeCUh0MX*v1rN9Q}VlKl3CQ?g=@>T*a5UWBF`leLu zsSol>9U}H&bAM0q^z`)KP*W3Kca)1>TV03kT_ySCn{dE|B;DCE0F(qM8QH(^D8>S# zQm?7#n2_$D6nH!5;P~*XVgHYhz@)Ej1_7#mI%Twif&y|qT5(#WnyARw7NVQkxm0?U^PGaq@e&FBsz<;{i=n;gx=xP zCD;4QQ-;}@vShm7vNYUXnMfqnBd*&{7Ix{X%MWOc5&(qa7;QRdD`O_JR8r8F^U1&$ z0u@@&zOw3dX-~0T8>LBEN#ERu=@T`dxSz@#uE|WsZVKJ(XueZBH|HMS*Rjo(L!4{i zHQPwyz(BDSe*2#nKtViFxg)ML)Bwa}Zno`ju}=M7FD~_+Fzndhyj2&WFU7}TuSy56 zNJGJ=E$#15c#4yG%5OL!Z8=zewItM%z*}W>fIl&8yfytMdu9b?ht<$tYUS~iVJl-Q z@OKK&eh-jbVoHS?PWX<-*sJ4%1N=eBG4hjbjNhTDbk z&n&`=L=L^%pym_}#+-vW;|3(+kDp|Mh4s|y$bI-vETT-f>wk1oF~L*$byeWLqf~5= z(sRSB51sS}g5MX8A9TC<(p`lozf;l@16)^@Vag{arWfpEF&_j?{sQAQzW|dk7syI20_7iuw5y3*RO+3}Evudn zf_i=QZws)f6`xnbY8>`8)vGLm>xK#2I?4MdyxY1#Q8xDjq|dk24^ zdcNb647-}WdC9%vfx#SAac(XsU%{(}*52yY|40DR#JE6^8`9JC+mX=>hCPJM!(#*$=D8q?@V_Fx0f!2^4ZsbDqV4gKW8lm3 zpm&rC17h6hiPX9nSfl-;7xMM}{j)4WuGic(e;|+kCe*#SPJ=AU2du(cR>=q$uM;VU zE<+Fwht8;lkE8Ti*m1^`0VCQ^Nc-O3G3|{Uw$){<<#$6r%oupD!e)iff0=Z51|Non zmSp6CM+VEa$SIFCs}vIR$Dlt$BldVLt?IXkk#}U_*Q-9Y6oq5oY=vT)T;}bsDKFhy zy30t-lX8oiDVV_4_$h({PAyTtKw3hgEQm(+#fPUQt&6Q?1<}FypK(ySQaNpyxhNkH zvVxWYhmAAfZv8%_ht?qkJ4+0-@4fD-QJkeQ{pk(q4X!#(_p4hi9XLhkOMYr}^q2hXBPzdtwT_~^ zE(TtVe{Xo1Elo*3``o~Ix2)EgEXihIWu=7lTjt&z=z=yrnp4F`V0htSG*PHb)Sn3O z(%uU1S7+E!SyA13MoL#IJGcO9A*g!m@3#2|1hiN*-ezj}VCQ^~RuOjFjml=j$75E$ z?oR`k0{?Mw`!1ViyY_4_J^N=V+~;8Ldx1TY0saK|5Yi&tEhjHU{Sx)pH`}^w?;*6Q zODWl&`-)Q?mlKCOm(^I2l0!~-SCCI;%gD&cSimGxu;q7L%x{+8FCKXVcA(L`qXYH& z)1Jzg7IG5L16X)twKF^__U>c<(b3Tl&ceI8K9B5pho<6eNNJB8ZWxvY>#p?Z%kq>E zqqgS0BKXw>e-f`)Qf^^MT*UgF6y$T;7tfMsm>JxNK_PR`voPx!r=qH3#?-@OhM($> zPo{vIYH54$Pvjd%0ovV2fn)b|4A{51;G914csy#PN$_^?<@2p280A;?^H^r{n%~i9 z6Lf%vo@*>EG3w6Q=0LW$L?oGV=vfr=e;Q;q+z-S96~1k}RikGgeardcxF6YejqoGa zUj#*u<2Rb_H@syzI04RP&Wky0F^g3K&1*s|*#Dieux%pFatSnkf5Si!pGd){>6YvH z8T4syKJOiC1f|+eO~0vCCVlnVeEfWVQUJ_rK@oOOQR$B2FBvGb|GW2XAmd_r=g@Cq z6#1MPRJSCjms&Kl-Uq!ZzRUkUWwW4&k+-Yd6CZE@GtJ)xZ&;G<3M(9uTfLtuy5l&d zhkS@Yhk#+HkYJ|EC9)+c0>pNIN#17b1~ZsaQ&U3XfUw-%gHi8tcF=q{FV33&%h*Op zwn_!f);t?(qvq)yCj&TrMhSv#*Mf*qzgbAjt3_6Qt3)9)VHk-Uf!uRV)Ybj!s3Foj zA&q^3wdP{)#Rva($kI_p@9k&gatD))xy@})Ja^74g&^pO?6z)M@HTb*-S(zn8xuNq zfnSOWU0+&%iW5%kZ4py*^?vpWhUVY%v3U`3ZqRzieZ^Vk$Gt5FlkxrsOMilY#k&}l zdSrfV7zI~kZ~iou@5Ly!rDeB zPf27ys(wwSjm4!rC3P#W3fE3VM(rOV70yD`a#u~2@o1($F*X)v@3ucjD`dKi#FgZi zgzyjCR`(~Dh;eKtEFNW{di2y(b^?%C=c-@Br$1%4S9O!qnp~34bCvF;(aRfURA-~F zqK~J~$+dn;A6-~CKf}E!OGdL-N341{^}mGN zA>(nq4FPhs6WVue#f~~nw}unhysER;81`_-%qHef?1Q%h`d>;l{j}*10{OkWbo!O@-pP6!^uPg03a4OyPTc}1rDlr8RH_8>&~91l@KQ8MV*ck z2S#M(i~dG!!8Ua%YlKal89KF2oSp4Vr3L)NeE+-@lOUALSQ@2LHJ)fDEmqN3`CafX zlI&5sV)U7H2_BaPfYc6KqwkY#zu3CyQ8z4cIUc-@831g1@3?a_tD6v4IT#&)m~pV36s%+!+CpYZRM1Fb<4Lhc~<)ChxB7 z%)jEVQjzYQAsoQ?_&O^aZ@y=1kE?uV6MbdChZ|sxwb$X|B3H-M%~>|_jg^|eN=U;1 zi_wq)WrK8@)W+C!+|3nJNyrVuvPHPx-wOFu+T%+-)W`_2{!k8_s*s5BlwUjG=u!f?{eRpC7KUEHQFCh z+gfnr&(y=po!PM62^9NNw4{#ig|N3ze>^r}_ysmp;tw^AQJ`cCIDySF_urNi za~dC(=`Eo8;l_L4y9mx0g*hNl5)H;rIibLr3RMKuv0`CUGx_=B=U>JGA?yAfCz>b> zU(#Y!hzxGNxoz#kI106AYKdg3CEiW*oFI$TpK)rs={%}oeHR+AtJyxSJb)U@`A14f zaz8{(^zS$ExHx}RwimQ&A+0*}fvdo11s%!FNvC&yrB&v2_Bg;aFm*HV;G^4=cwRbv z=@JY>&KVBulK-5|L}lQH+5RzZ`K7F9xuC~{2fgg|OOqT~RNi;%j3U}NP!pL;tWHl( z7ECO99P80S`_b6^l_K9CWR`1sVb|Ap>T5DEw@e~)Dxz-$7I@y9e)aS$V3Zp#OWWWu zW5`DrV$fxw?nC;{Iyv+s3JWoh47N=`y;!==0Ai-WtAz)Odz@)QCfQ+XE`j5b3dY~o zz;kAyC~K_GrG1ZFi@6ZLR=spyV62e>(~F$%S_kjkUzaXrIc$xbeTj~JfqtNb^d+`< z@P{5yxUdyeQ&GX8#6<1VGK48)@>WPfA_#B@#3*`R`JsnBlPnr~>cD~qIz7%a+l)|K z9^ALBbPF;aHMhqnBHA6snEExR$JSTHj5(S_G9Iwh}C7fn>&8jNnM*MxB96>%!8osZ`jG(2NH10XS5NPWMw*>{+-&gC>rL3&$Y9M{@vMQT=s9$ z*6>_yb;aL5hH|%bL38K|Z^dk+cI;g(_azOlrY*V--lq6ErQ#zk{;*nOW`32J2mZCC zQ1;srCgy_KJKKJaG3oAyfk<$3-en&S4t4zK-IUc@nVxK2jjh9)Ct-rd9jSTYt8Xt( zRhRb)<_1?oIVu}DwgL~xFIpT-ms4`&}mHg3|(U?Z$4FC%t)YfAq zdq~fWiaq4Q+G6Apl&yqW{8x<^rR(emlnB5G#wV7RrsN$d zg#n+8hJpS*_KEi&bEh35ANz=zpLJ!05-YrGD2+tNpR~Y=h`)VXNBq@miYs8uG}zW? z{SFG98avUkRsC+e25OALpM5MF^4=awijy|m{3<&B{y|6I>G#z9>g7~7ITV-1raJh> z`n6qBLzxsk=v4=~03Du8`ZA18F(@#II9v433Qc$-sem!PIk&;?2W3(R4=usMZ{PnF+R+u!VRG>P(VHK?5y$BTNM`>xZ{C?c*_3D#Tq=oC6dsS6(oF^5 z6!E1r^j^C5QUL4);8>1{$O+U$Cpu9w#M7A9>C?Z>LU8im_)jAO(ftY=*l)2+Em+T- zbLej`GV!E&{uP?JZ$BF=m4W~}#HYz*cq;BG)AGO~Y;&Ywl}VL}`pkh9d-KR>yop75 z0XO!Rn)%(MguomdK*3pC@BSntATZe$i0Q1$Qh7ShV*d%}R7CK8q%#~t3`kV(1>}QQ zo5TI~#?Yz03W%{5*jQh@jzdnx?*(NKywwdPKYbD~Frbi2W=IB5V9O-Z(CE`YHyIHT zA%IpB=VnO6&Q48@7fF)swgJt$={K34$|_++sK+Y#1w zUUljZw&joTWd{8BBaS!a78PT0Y73P6tT(`w-W2YFGEq{(A1a@Br_tN(!A{Cf$g4q{ zTb!?6E!#Et_QXUHKy;V$z0+=kU3vc{7%(9BOOamFY=g?)ZgeJD-}r7%BEldx2x!FK zSWB>>lI&;zC>C>ow@YrU4Y)|e#J_(Uor^2Qi~wjD0RCUi|Iih`N7u`L2!jfc6u~)u zC;C2MV^ai9T;?||4-5catFz_rNtLsOFcTZQ`=?#A{>9ZQMb8^!N}$6}C1y{)rE>|U zlGVD}7T*RpcCYA5dqjRMFESdTnD9wT7;Q49b5e?8hlb$YobAr|46*89+8Ok}C{8V7 zjf{=Ook6K-uzU?HKuF@_RD!yG*;Z@)$$S=wCFRJ35{EKQs06i@Do@HnKQI8{GBl?9 z3l%a5Iaal2sA&fs49aw_m*g3+DGoUWbRPdq8hq*f&xynH=KXZ|&!2(+^T+|rdExhf zK;jprJ+ayP_bAb2p>Gd0Jo_A)1~2r0gYacY=Cafy`x9jFqjvuZfVw^PaRO9CEm+7y0J%#tZ5EV>p;OeW5JNE$=^OSbr!0f)EdhwUW#wb z=R#*$eq2UW{31NZh`rL7UA5AK#KUPQ%|Z823DqPrrYb)L!6QA4^VpB}xL3?AuJEbH z+oB&0W&gojk19xBK6DCV>3SDoh%5V|9&nKVH?a;;&$^3d5Wm9269R2wGU-PRGP-1s z)^{}M0ELPg#(SzUW1`B_=zfmIandVFMqQQU8jP-%{ErW^i_VYgiJ80iunLT5?k|?= z&<LaIJ z8G{$8@M+uydBjTdf29kKTsl-cO!gDIS*!C9{|kv8)qV)hg?exSbOUh<+l75m8=C0@6*(UKM9`*8qwHCmGG2v z8a@gQ?A1cGv zc0_xC)woN|rN_N(T5eo;9Y%a&Xwl-0NT(h|eboXYg3!Z^wyZLQXlc758*<(Z_sHAm z@~k>3r-Gfz`Dxu-`~C!u0_{C{HX`RrQL}@C_m^Ml?tjXHT6HO5IG{kMrH<^D=#)i$ zdDi_qAcN-}$Cn2uayH#~=!Uux zT9Dc}u^ZLB(>d~Xyq>27u>k=3|AmR{=fG68c%SVuF3R8Wp!OR|?2i-vy`l~kqPFq_ zrwiTPjWff%dg6i|pIQ@aJ+bxM-S0t*J*Pr66$V|X%lmtn$W-LUM-$Au`Pn_oW^2}X zGg2fZ!NsY(WHh>Px4omodWI~kdE#tz<+J$lC^64tX#1HVD36gEE%LNll`~_Ae;J8) zXeP~p@|ZnUU=}Cj=;(;wqapHiR1<{2(ssCW#Bk+jSR~gNIUPQQ7s}{n<(#u4+~uMb z+nSn^6C#!CM(TDk6I9FWhilvHdx*(}=O~&kdpCm)(#utayBMr2Gxr~yM1PK=BslA_ z*FUQhRGWcBq4mykEzI+>-l8LRZa8jP7FWWTjE#VU+nh+r()#avbrjp+nK*BvecNUt z`g{?Zkt_tA?kZvK=Qy{rFL(X@{of@#euYTD@rM-dw?WMtg4q&%q-(0ttZZF`K21QS zwSn3f0uW(Ej_4|df=x&J?YHhR+1~#tJ023EI&`#M6#!)^xGxPqWxXu> zMx}Jt?c!+DV>xE!$v&;ydw6*dLcWXJN6+3b;)h7OJaa&f4UFLRB|SE$Ig_Vb!~}LN zb&ahun%VG1e9qo4e%Y|Zl zQuWVk?Pe?%`!?x&wm{QQM+fbTl=?xx`uM!n#^j~O&Zl#YPxHa;=Qa8AvP&gDSY0E8 zw_t2Z`LbeLy;SiBlBju`;@|}0HU$CPaWIRGxKqp=SXQL&i)7EYFR@T5DV8Q-qUiR2 zXoL`IhM{yfztk#oATd66t;Q*~Uk_`7s#?;NV|U`v_B)m?o%7Z(??XvIv;7uxX|f(_ zz{D^MH~lQ6o^BX!^}Pxvc9rUu_%N&zXwrwK;rF>Ni_H;)^ZE`BYmZivT90t{`A@^p z!T$R$I^p!_RXXKk(QJa@EB&l=jpH zk?%>VuMWeib~46RR#sXl2Y$bXG+f`5(=L0;Qy+Jdz))40ZDXhmgu{ zlS-5nEw&RBMfzA&oye z5MDas4-qa+@2APd4N;cWBK$%VZ_45p-YNkyC;Q^Dm~|hf8>vR-DHUKo;Rlz?5$6&) zLNVDP{~{f?2c$#%8}=hk78)&&r1UO_o{l9NR+QQh&usJ!&|LI}4UUudCP3RXczJE; z6w0qolb36pHk;x@jnls#EyJAJ74tps)sPh?6Gl5V)~}k96d;jEU$HlyP>G>Luo~8r zAv%Ig`vf7G+vx|FeHh&?cfq8Jy(9{GQ%^bs=e3n$jYM1Xq}Yv^$~`yI&0mCU2yh#v zAJ8UYkwGAuL;MBZY6B#%+pG7>uqja`30WB^3eZ9L;1Oe@0Ei(n!eE~qX_fPydWig| z*mc&RAl)vu*#b0ek(g&O`^m7n7Jx$&{$R`<{VKjRTxPPZ8P|1HV^90*TxhG~5>!X~ z3~Ffp+B!cOrQ94gztUqW9r)Gtei3K;-m9{`aaEOKYZSS5<0H@DK0pYk@#_7G&m3n_ zp_`Pvy0@0RDP~OJxLL5ZtX9XSK;FM0+W>pyU3nR3k~RTHx?g!38r~!huJ$ygHKq)ugD25#25>iWnPx7nI8{o%^*Nh`O>}=c?1Ks=u*Dzr|z% zh=q)fvrMG_18Ykke$X82p%e+ynR@2{;OFI zTH3xR(P_V*m&s$2kQAC_VCT?MQT34iFr@Y!JJT}q0e7HOpk6$SjRGb~ZuCh@ru|2# zRB+Uf2*4}H1WS@_rCmn`4#!b*u#$(1JrG#ko&-;spXMqoXjkTHXk`F9wOIEjNK+h= zpr40X$Krvdcit^gy(dUI#E)#3p#-VS-%a4>yqA)blX?L9~#91ZWY|{sga#GJ_6-eqnOkcM?ou{;K9qXo}IsbK!D0tn>BRSopv8|`;h&ElAgS!KAsW>DYy+7!XyTeEPX%RHGW{#raL$pgffIUlqH2M)F7Z z?rwx7kf>xvkrwEwT7#+wnHI@?igP zq-{69m%u?*D`{y4NWSJnXvf%tJq4yh3o>@iioSY*;e|Li@=P-~#}Rlj z7KU^b37n^>oN{h((LR}iw<16Cq}kf80|G8(2`&C;B8r*FR`2H;FY}-i{dT3t@C59! z6(a`^o3+48V}3aM=($JEb*w3+koD?Y*zMmAA|1gZia8sO-`2t`*x$vekr)ezb08qu zDwRBko>rN;05$KQDKU*4I;Kea&kNAs3kkVwQY$Il`IQFi}Ecl7t;JBC|C+=@xLoZM*7ZdQRmyV&+paPE%$Q|kK?mklt2{lyXQ946)#Wf$Cr_u7M8nbBbYGh#Jp8KkE8{xeo)+Akqm1i7?pYcro=;zT6N5`*_StB`n{p!O511VCIk6?V^3Xy%Yz|q-&XMCm=aKV4W zA zp4i$14}*Mim=ODdKV2G6TvBBJ9P?3 zcbBGa8cpv})WrJ%#FyP-ewAm>xuUWJ2hN`3w8tH1@BlDZHC2t7EXzleTvTM!lD-m{ z2u!5gzj=oFkXCl)iN^#dgNz3<-l1K@65fMP<%S<*=gGpx{Hr3qokkATVr{*#mf*F`~=D!c};l7OLtI!=Pt7uK9T{yw8 z=VsDuGo^EtyKzjshNr4acrtE_U5P35iP};W&ry5>x9F|YX_`IZ-40j3K6{(2@*QWU ztc}l|jqZgAh6>Hi>Qz@>VA#~;g{#@@-4J88nUbxLWT#t-*MXqwLNFL{xcMJ;g)8!M2P3s`J2+I|a=rV)rr3 z#Mxg0v)n&E#RRFp5|Vqr&xN4v)gwx3U8CR$pwlK+yLNiIe-E_(yq-BR;cs6=&N5^< zB$00MwB$A{FeuPK|30r((Y;sf7x3o?6Ws@HTR(TI*>9RPHdt0u4Jinii9w|L8UY&9 z=s!d4^Xo(XcvY%QQlz?TPNj3SGYLvs)Pwm z`+pFBY1){`%A%TuJ&i-QWObMt%Z4UnrBQ92VOjefYnO4bskv;|d-vF;6J(LbI9xSu zj305(@I-uDc^P;hkw+Jco>DQs-v6=t%G?tYBb^Po<-SusCTfUdf`ee+_-m|>jFAK! zM0jk%EqP?LIK2_2XPCk1I)eA}@kct18oV$+!P}1Dj ztidL5pKc?*TE1s%y>ckG6MtEdCK{@$)bxlv!fFlVXSMYT5pz9k@!NG>K~GV@R<;a_ zn>;idwFRNA;pa@gv$x^Fw(VF7D+DMwWqpwJ{+$f|9C~mN-)W$dVqx9b@s ztBz|7Z&3k_;C-z7_WpHcyX z{|l=C>kk;Ke?eDinT1!)0;O%F#P?~X)&?O4Mft>clwnQHub_LQIdcEoj0WsEpIPj_ z)Px7v-@8j;L%bL*6w?BQQ)%v>~Eo-`n5ki_$~+mQI?Z`aQY3!oXr z6*;LH*W!cS?Z&vRspZ~;qdn5XqHv!jB@jNZPX%H-!-2rD7_nHMOE4M4YM1rlciJ-1 zL;F4YX&m6}o`7M+pu6Ox?~Nl+dL6%uZO4y|cOUw9z&L+E9B|ApJEc}W4|Mf5 zW$?%ycm(4wO_99hZt&m+({*I0IlGwE1_fZ`(M$A85i(C_`8OXEbB4c63FXlD=87mV zET62d!K%}(NcU1}MWk^@PpoNDt$LlJP%U+z2@v#F>WL>JR}#K{Ka$l%(U>^A?1{R+^vNavOA0vfV`OXQExkbz(`YoXm|;KXUg1RIb3 zPn}ls?E6tCg-YQ;3rIhxoit^`i7x2ZCgd&V$YrI19=Gm@%@4*iFmO2S6*M%79BR40 zvj<1N+E}a0gajB&-ViLN>cb6%Q&m5 z;ch?17A;?slD1@(*i*m#a$r0R;G|*UZ=woALUzlhRZkE@U5}DKmji8Yj8!c96I>Fi z!%7@no}b7VCH|QwWI3^hpJg=o>-ZxdCbFMqe)-E|M*_G z^8ROwU^bGqgsE&8x4iw5z|lg!1ZxImqN;n}U^7uS3Wr;*-DJW}kg`Joa59E%*L{%^HzLv5~)k*qRib;x!_gq+$H{ayA$Gu?g7dP=5O`@VV_MmYkuZ}KW5sE*pXt$P(w%b6Y*YlP8nEEU zfHGd*fvg7vR=D*r~!`OoFS z-5Q^-{wgAd7{J~sMh?#=yYkqqA#Z%rIv4y+%K|dc;~1()fHFhN%GF)^bSW%>Pz#l% zdUU6Wi+kAx0_+p_%d*JQh= z3HJ@ScA&mwGH<=|v@Z$){|yz|mChw-PeT=tg7`8-HW2RzdBCw(BK?AiVS@NY=>hf{W&h2c8BFC zyBOaD6s_LeZeg=L4QbRQYxH0Ch!LtPn(^0t(OU~qcAf1J;=5O%>0t792Kk3`b{l4w zOhx*P>Vf=l!5-@E5P{zbl&G^>$!noRVS>5XErOEpVn`jlJUW)^ua&RU57<}oYe|f& zPRc+h+`zlZ#{;~Z2CMGqbmx`G2VZt&RL&0sY=7gk)iIY>8P%2SSsz|xt_qy1kZTeO zX{zl>-uO|g!N4HK{f-R4FO77}_czoXlB10_7 zIx1s;*X`;Rr={_PTtc;6mK4_TM{8D#`B^)jsH{dRh1>ZRa^PN$v+}1;?`GS)%zBfzPpR@czCJg*^lJWydK1+<*L||??9l_?BhVX7 z8_2FBk9G?<^`v;E-O4?wHBk~bZ_u+6C&!Pl?dwy7T+M+zw$I3P0vsjAL)c^CmFLNZ zcRS82e|Cx3Z!hN@CujPS5EZ}inr*>2EFINNxDS`hT@^S&7QO7B9(cu^XBG96@*N|f zbbGA`Ona6+HhMY?pa12laYM}V0ehMIlIX*g&IGuS59~{o@;)9g|KzmcZ>ek8I2z*s z3XaDD8okTfvxy9E%73F=Rvpi?o@y=`;4_Bb?LIxd81T4TR?Sry7qr?=a^8Bcl~?3m z?d`NNvr3&Hw-a~mS2u+-^3yDlqjT!h#^{=Y>37Mt;+zJaK2_v;iT}KX-JXcZ`I`09 z1*!%5Ye_0Cu`YMY;!r+q(2y%`fg)d1-nGS$}lgh1gP(Ld|4NoY@(GCSCc zR(?zTlPcL@)o`GqpD--MON-aDX!uRG{d%7AzF-0eDgu+rzwzMPr@jz=X_OI~>H72e z!27~BduWQ6r06^u;t*R7=zmM!2UgU3RwJjjmgP4IWMx2 z2e$vdEb@H%oeMQt;6 zBnd4?5?r-jmHBPO7NqJ(NH@vRxL7Wt|4N6fCv{|r1zR+^Z$sGQkkrwVhw&@6&{F zyV_FFYJaVdA{AWthS+E=&s*<37VTY3`V{U-OZX&}1;xa7RIpZQd_i88LY8b|xi2JT z<3#0|C$cG#3>vM-T~J`oC-w^MM_c8N2naS`gMO2JE^PmJlJ61#MAuHUhsREtl<_KB%@F&-ZtbkhA@Z z5SZgLJ1*%KBR{_esEpgC_e6_NIs~e-Iw@NQ6TB!AmzJns2spq!C^XoHdA>4y!$!>2 zaW2vqHoT#_0K*y!efddEQ~0NvsJ6zp5n@yUnMZJc68z()kjCKY5K1b)I2y9S{O#A) zIYO2rDkK@s_$ew-(S$4UhPno<7A`2whV*tG>sb(m44OkMZ+}P+R&*;9776iNz_B{) z^y9*I6i}YVKwm>UamzM1;Bev)aU#o^E3{VzRyL=eh{-L~6qMNRT!>OLVa%v%VLvlLEok#WOWPVT2`A!jTsjxFDN>*CnNeB#z^#62lyCR<%`@RYrQaK7 z!&L6_Y}X{@WQJ6TJxlBOkDOvxi-$*D%x_SG_BCF<$*9$ISbK7bWsre~9^cf9uGfRYAk2rbc*mMw|4Ehxc4KU(WzP~6u&77=Z=X6mr7 z68Rfik8h)s+Jg327?zyjnUqdMecZ({AZc92L6i1w6m&LlS4tax`Z<|)W!os!MHQZ{0yB%5e_{&4WF$)K^ zTC}C;*yl+$x3v&1)LOn>pN>$vmW*$IJbbDH8lY}&SYl>*FV)y>;{14pVv^)!*r%S} zH9Xl1LUjoJhIl3WbM>&Ou1VwUe$;?b&vX#eA>^5GP1{*Gx%Rp<~cZbxw*tvRCHn^~5gzhLMUR7_XaTG51{^D%|EYMH+E73S^ryD^a^Rf(=wrYhsQBb zfh_1E3RCTJ@B@3bA80;7+Pq)>K7qFW=z5 zUp2xB8>(-&Tr96s-z&?jIFK8N{%(Sx5G<3_PI2b>HFV~7UkrV_`qDDBJ~Dh?9xdE( z@M!=E!L4(!6&S1g|5fyz0g?y0W)l2*h z#%=4xq-uZZ>C~ylJE61ab(yqI8uiANmt|&W^fyj!QLu%faIfI>E^gncr_;ZdO-z1y z@6u`a9Cczi55$bOW=7q_ar694XGW{CK5z6IXKVDA+%Yn7&>uRFTy^9=A$E0gec9LvKt!my@uZC2hM`anP zbW#89raP%}Fp|TjAGZ;VfsOQYXtW=4X4laZi>Y-q6RCP&W@+E~vt3vuZ_@8s<(a)n zgLD1IDEOz=Y$VBeD(=F~vtG_;mamK+DvRdIf2t$CMj`O?Nons&w&Z0W`o zbw|6uRH**iY?;Bk9QS*2e-`QcFOT6z8{ziXpxVmHNI^H7+_Dj*McVvsvqyVc6VR>5 zv2FLg$3MyNdy{J59O}zK19+Z@_BhY>r^ZX;IMK^OvJ^k03tI9V#sxyVvk=SvJ)MHE zTX~(T)Ss|hZT_6kz8d6y`)aS-npEfT(P-T%91UN%H*6(B`E)BWdqkH>YCSHH`ZOy` zCU0(g(vtBhN8HfGZrU~awEL3~t_y3cYpjb2+)RRUUs;cqk@D%qQxu%=@QCjle|THm zQh=q)kq)X}T+lGOF!jD~xSx=W$)fPyns?~Z7{YSyoy`IN$gvkG2>Au>qUzaYTfE%< zd;$lFyN`;6ymS62P z=e~xLn0^a*qyIT=P8-IfiVp2}!s6m}N0+4X{Us4>kIQ{-D3HKGS+IA3i^iJcAI8Ko zXnvqxB#s;C>*m}Pech>MDAU1{FX6;pw?K82I==YF!^BG!t2gySpQ5AQEO1Ox@aKC; znp{>pvr4F3qByk(yKtU2%)JQKocATnPoVnGmK4ZeN-YOABeuJDTU zhD%XyL%>4GADnBj>J0B!owc<8^p8ZpRk#eJVijU>sd^m1q~AOocI1b~oV{~_m1-<4 zy)yx`nx>2Buf9?={>QqR;T0uA{2m@(Gim=`^;rWaFy=`HPWkMonr(4UpIl=ZB;ujZ zoSD9_`Y&F^g*V-R!Ifxr23(^AaM~nA<6QLeDkeJ9Gv10miTXFur`vNAd2%@Jse{&I z4-vmw(PofZ{P_Qfd!O}t(k4_lEp}7?=MH!XhyD9NJli&2DD)pzxQ}%g0EH%H2`BFO zKLX$XcP|tk)L&{lD&BBVBZ@?O?-O7*vp7^7X!SPgdyF^vE!AT4enHW*i7bdcsHGD&zR@zh@ zNBIP|94m(rGL(m|^*)kH169GoJoGV%1)PkKpQvl~^`UVFI`UM~-#am?WcwYLx^sE9 zvTQjxtR5u+<)6H`9Ls3xwAu7deKs41cRG6ON`*&!NY@=_P1(PVJjvh*<~@*iTd301 zFO3an&0b*>5A-xrth3dPf4mHtF4;J_>`py(Dt%0qi1$@k_bQd8p0dmpg?X2kaO9jE ztFDp_=UVHxci5Ymz;W@y=bH7CY8DvC{7JkTWN zkm6f&!Nj9C?-YvZrg=EA0}Ij6KVDR@eW2p=)rWthJ@9wS&CH(X-6mD$mgb({#ervEfB7#`I zDt(EnJ?vF3^VjF}qXUPH!k~$PL~}5KrCx`2Cnb-}@fyZtn1IE$vZr6(Nt1jM1IIN( zg_Gcon*e)(!TKAvRc6It<6za>n3{x?1UKEGy2U@muWzVIj;+J3ov}p93w-XxkM)oG z;*exJNYfv6@%WLuZh$Z42C}eRRP8r$3trK5AayW?(RHh88FnGRI z?gD2bMg9iDg1P-?$A3^N`3)(WrDjra1B!@tT1)OYIEZFK}IEoR9L4iO~`yFXqFGOY!FE?HLk+!T8ZZRDu9AEHdq-I0bvyyB=q(#oR zBoErkh^oTky>>>CCOK>v;+5)OE*~R2kAW!VIup&_Es3PR`Xa+=<9Xcj|B?62-<2*+ zqtV2+or%pIPi#zVO^k_c?bznTwrz7_TNB&3Irn?ddC&JB+_mnr*8X94cRk(pbai!g zRdtoxk>jE1R*KNJ%C!g!G3j3icf~T*UPmVYVc8mDHhvrb>JBR70~dcs2blXrbkJ&bgYPv8)FP(=5Q3 zhk}+#w(bDOa!NRm$5?$c2#lt457DROe3(t6j_EAGZ`{=ex^rRsbH!Ic~OBdInE~xqWMh?%EUWUUI;Npch`!Msx z`@$jX!Fb1=LMRj2VWCjvl>3oni{ZekQRmGg%-VAWwM!1V(s0tbr$02;^&Gb;F5IL- z@Z~B%yxDRIOpc4)0ge*0A34cr_Z;0DHdbz+CFx-nVNJ~u^^7wxbe8cW&UeqS3|WoE z0W`WIf3Id!!FDN@XD$q|FTUM2Lh|a}^de!)%>X6TEe;csa`U^}8KcQ2i9g67#2wXE z-Q?hOhwGKnx^UZ+`asjHe0hp?x9>>63rVbLJR|7f+2J&cGdRY3s`ka1#o@q0mEwV% zjs%$7(8lH5smt1vTBa7e#u^crLAr3xJ^18)cDv}t{D5ZV7&!I(z)e(mB0*PDm(>Cj zm?UO#-_`qA{Vua<6o~jb8`6^Qk+M<6IOS<5yKdp`#M4si@4CL5&}uSY=_Ap~fYRp1 z5UugJO|*R5<^wvUMW?NsDRfy~LyquA^cP8--eLQIos*cl+luUXO9be+a+Oq}sX7=y z)Naz^H@WqlkEO{mM%#U&-xq1&84WoT=9Sc!O_cQjRJ9aZlkl+5HEQ<*6;c;{4`&7nz5;3lh2pO#p8`|pJ0LWncqJsSy@`Zf1IEdxVTR`G&&sXQ zx^j{GA1`V19VX|9hHzbT&;_d=6Dyi%MT;*e)0QAG8sUIn=(a8ME*p7j4GRs{oO$OiZ`Q~1Wt$LlMx|Eqs~sHEm0|ZD?O1u&3+>YbKzZXkyGob1 z$*n?RGEI%SiO;zpDWPV+-%q@fm#HE2i1UeQf8jJ!tIttDHZkjWtPU^F7>|!i5Rd*6 z$Kr}fXwj+(Dh6j>L~LucIKVd=sxXl&8e*i1i2tFr8@#C)PQGF|2nD3t z_2-0GKcwC7+a7j5*`U2l)ZwL+_ZQ}mQpag&H4*5197*>6urK{y`rU5A&S`6*!%@>$tFE?>?NvIs;!Y8D=5*tGS+wjl`$^Cf z*V@mbyhEn4i%Y5N;Y-PK+?GddsQ_!W^IdgVkg#2@i^Z4K-LOYV^O3`nuN| z`3It?Q@DcLKZzDSnlq;5js3&2Q9wRjZPu+&uxwp5M8)P=t12Osf^O@;;Uwb+!h zRS9=nniXV?=I^|O!Uw8p?Gy*)s+?B?y^4YT<2$oNYqL8|m>XYd4x)dVTGX(|pMRZ} z(PO^IQYf@iAIs|@)39$$Yy};^R2_N`p0k67xf=-LN8cY;8M2 zW4%%!hUtt?m*`M^!otTw5Apr&aF+$W-Y$WaWGetdWc*2Ap)E;zB14yZurnI%~8d&HGy8_>Z)WB$mT}I3w&DU1pA-+& zkQJxKN9o^6HBJ*{9W`E5Pff41_Z+iA^+xF<5~!oEQG{dDiZ_w_Fy;!5$%2UTiVvrW zEN*G4v*YhbU>>zt*oP`MR~S6w#MjJ}ttW{Jx@Uw|V=ju=ze7SQl%KM>uaGR)wR?)> zx=qclPqlOU7274$y$JounHoZzC4C~4ZK%n~DjEIU`9Qb(Qn`u(!`$bs1j&XLWhkk+ z0|~ha-a9TRjPFfK=c?2(HdoLc)^+Z4PQ>?fQSIbf@KQqr3D17IT zS`|N!5j2DwHMg?N2IzqNaW;xbOdpoVlXKPQyyX)Lk;@pp?rR(;#4cG&kYBm91#A2B z2L)AjXKo)~)mRrdy2`-)I~xg~FQ^Uk_iasAbuJrIw0JeX`?n^m&;i+e2EKv=fwx8KM!39#-m z_@ix50*ic5Z&Sqr)V_N$yvB98_%SZ!OLasAVY*^+k)jhnw-m zkMOWfAB(APGtg`vyyJ(urumgnXd5lZYyRRbd+tArcC}lAdLy`=A&-AU1+seX;7idQ zXL>b<0tyuJo_|JM&6f4AJ(SCy4(zc!#OibEy+ea$QxF?YnhMiK!GJO%Gi4g~cp(+v zbKVGLA2W>4w__4b?Hn(tc^{7zD#0~h_$6{R=_%{!@SqCe;1% z$tT>hTW+cTYVld{SMz9;&;FScW@7#>_=Nxe@`YPIz2+btafN=8AfJzX9!o6`z6GAI;{$LFn=fsP6^ z(Y3HTn#wS|riG4%28K1X21Jn)LbCx`l{HoL0x*B#VHxtLu8*mfkqJ^i2r%;7-J5=L z&W9|I$&-w=_QwpnE|51eFcDv9WKdE;h!7AIVzBREddM(;|M{PqZZu-Rp5!#^XYGHR zN+6b?{1@u8d>A?ge^g-_?tj6?30r{w7v}Q?EMVk77W7;HfAuKhyBD+aSJ(eE^o;-+ z$$|gU@voxKdO$vh;Qx&Ae;N{#3EEQ$e`EZ=!~Prgb65!f4-V^^61W`OTup|o#qm?s z8v@esB4!CcVdPz3wI4Y1ZINOMiwB1Dn;X)_m(hJJ z%1z-*%&cYAjF89tcJPkbG(igy=*Fuv#!II2{_qBV)m4J<9sHGpp!>h)Q$ZQxv)5G0 zO9xrYC5`?2>LAN9#U<65J6+mB6dMb?Cl4RuR1M>4i|1*2& zFrXd7IkVAqcGn98YaLDm$^K3HCTeO+Hv(&_h)13LwGyyOY?LTu??)1IO$QWHxa^uy zeVraK`tEU8_o%A(<#`I;9w$ZbZq;^%aw2oA4;yrw5}iU+b-u@3?6G< z5iWn8ioJT+$IX9GzL5wS~`EiuMFb$0^!O3a%#L*qFKB2OB9!ayl!XZ1n zATLb7bth2QRU4jA^F9?Ddn(;xuDSz8^9md3nHXi3Wgs{g^nH`D+Igf2-_!#A?wAe} z`Zy&*xcs_BS(%)UwaZsL(YNh*v4TuULd5(3bf|Z^aumv^g;0auRNK!IHQ!&y3n{Bc zL`O4haSc6C`O9zic`y@)_ypsbPVHQ90g%7Ff@iGPAxfJ<>5m4Eq%gYxz>wa8hS8ni zYhxexcoD610}ASj3`h4;)ms^$)!r4kT%HA*Q9aRt$@f8cj1!IYTSTsbWhd!B_YcKZ zXafn>e2q`n&?t=-{2P`$&Z+rnPQmtcFu%=0e(H`hz_+useSKGntvcrI~ zdiPv!B5fVhc4p07Az3;AlX?~h#59wg&GhZH@dn33Q@v2U*I++(fE1Xr`){N;| zN4Q_2r43Y=m*9sJ{pJwjokyj}5|pmo4TJPf=eJ^#-L0U_EXlUS*$V@uk7pLdYX|kd z$r21Ox%L1F^q>j;YmTD&=tOo4c%E^|^g#zEMo$N@I_`Nt@NB3c)1lV&8eX<bJW-Z^|Wc+)Dpl3+Cd1) zF{j<3D&iA(VHolYm>(eWuon4u-XyY9WH5@&W zZe}G^T!^wvwhOrBsmp4TliDxm-RZpE#dX=`->`y++zefDy6aygsLX zS@b&F#l%=n!T2$S2~iGcCnJGNB`pGoYn~xmTEH9w-t-B+z3+SKM3Ki5 z6fwztw+<=aqp^fN>hCwBwUB-qzR2Gh$0@aa*qFOPxHcEUSzJBUT7qn*T2`BsA{}47 zR-E(*kb`D7#P;poqzt-judw$VS^I4N?z3ECFD+r4YBW3bfepnUYah$iT@!JwD?qZ4 zx;b6q1|fMM+?m;_thGjrG}+N~2i;dd_S5Ug_;f1WTgIwWs~U97Zmn&tt$JXU`x-*x zUWf69U$6SR z3muCDXb%vj->*Gfvcjya`IYoYCKq)V^9H@T%+|d(8+X1RQ4KH~h<2;9_q~|pVsOu+ z13H_rUVJ65X_4pR98vlz=Ip#)Em`BEjbO0dQD8>N1Np zj!gMjcs>HJSawW;5_S{??A;FcabUS68>=h1#SM%3CKS)c)dHPZuOCS<+1vPtX}nCY z=ueJ3EXGy2&){ceY}+ET8gpYgAd9A)vGYYnv)MHd_g#;-*AS|5Mn3Hj`p*%O%{0sk z#5Y`GJU2eWT)#FOU0~O5eV-jb%!`J`hB7qUwhxgd;gEwFh43_1ZF`nt9HDzn@^Bch z8z;89UO3)mL;FYeXDOaixzWhZ0Z|) z<~m5T3%5g>=CCBMd5>RY+)C8-L#8y=mJ*I`%7I7s~CJ ztt+Rq9~x23?w1HYEf4)sHz)g}5V>$0GL zrCtg$GBo%|E)r~`A93_3iXP%f^%ty|H=OuG0T2W5pLr!ZNd6-sD$qgPk^<#)Ew8Aa z(D(~Hg^r8ZpyysUFq9EbBKSc^MyW_&>ng)vhn6?6UY5E!3;FGM68B=Vy-Lvm_2v-( z1AEYMt^>*o?gr<+eAer!xdn+q92v)5GK&R$qnF8&aT;XG*U0#nd&5n_k=^d6L+$+U z8i)i;`Y!EkhU9`rbI&Xq4rWZk_Q&<<>MOb%UgmmvDD5u@eN{Y$G{3rmxS+ zEu=FxbYhem$z2HAxb6ns^4{3{>w*k6-vQ_w%BNwAHp>JP_9>xC0&T;s2K%wb{1!t3 zi@j5n>hxs{Wbt)$$NMqt{z%OjT!j2tleH`(Q>I8$?^;)ZRXd&|Imk^19Pvfgi*Q;k zmsKkZq$th=bEqLO83r>W2(|jiw8!0 zS1%n}`2y}DBIKO_{j=#I(WOdLBQHviiPKy}4IsnAjA_#@vD7%&tAz@OF?1RuUHuhM ze-WN36+kj;YagLKjjuH|WX%9zHgq0tOMDv6IogMGOrB5XDzpV70xFII`2aIpEvwA z)V9`gcix9s;77Wz8rDHxxK$fFtRABE7XJ^TjoQtdh!^!)jZV5q;Wz5$V$?6nYOJX) zn0ep=2hk(xUc!%)uLLiMgxygE-j9j~9CeuuaKT>&Y*B>P_Y@)m*E?%#e4QIBYK}O< zOJPu2Dze*YRkZ-4{-c5=996U|+a9cwy@ ziv@_s3wMJnP%?El)WBSy8V_4ONlgZ)Er1nKQ-=J#Qjny`LS@RZIFMCdzg$SC#W5p- z9x8gPf5HbKsmurzyziU8jGONw^IYKf12OdE;{MLB0hDEHN%He?K?tU##q)557e~ee zi+w}4z!CuG|2d&Bf~!?7n7_2bM>&Z09%)9y}i;5&dq7}&04&L zc(qNozlBcKnK@4QFsDOi?QClypN!jeA!a_uD-(3-?)!!leKWtfftPe=*%D5kKdWCv zoBFcihUod;3aSoIlc+Fd=ehOP0uS<3%ic9jc;RpkvJc~%BEy`sER)$iEFJcYOyk6c z7n@MNA#4xAxMeS6a{u8XZo{j83kiJM9`eo)?bKCO4)B%9g)j8n^}&-`k7UTZm98UW7`e~ZMwk*T-=5Ha39aKRi2;USqi ze78DYh^vreO#mkyMrya5ksLTr)A4VNNc{GFq&`_kbz5}ri5^jyw_jX6%)!B)8-2_C zyCwIgF&Plr4Ax3#LlWQ<_GV^P5J}|v?-ISHW3XUozlNMKpKX1OC>!nMTnYp+kYXd;gdcdCHMKpTK@bu7N*v%4 zu*OojZ<>0My)`)Cz~!_5vt2TF>N5zcZQ?r1)%z8KHn#_&THoeYyy^%{EWQ&FCqri6 zQ~07SH?_2z@pDp#muo-5@JkEdEU5eoEF#{B=m`u3}d`w~f zqlCX}K36Q0Vkvi8XW%2XxphAr}2A{w(;6#U`_>I&VF-WTy0(605eyeJ8q#&Dyff5X85n6 zW$l%ny|yJ*7j#I3EMKRb8ssZe^p1g`%|iX9X*+ehV|H>6@ag(Yfv7AMXlOgH38wN_ z>0j*~zs^{Q&V&KrfK8Y_RimX+%coXd=$};qn=@lfja3?Qu=PRE=!l?keZAXKCC5_4 zc1W$Osv@F8SIwo@ZTXhpgX_Amui88n#xCIPY2&!Ou{u+`Y#fc-yD?c}47H*<~EhE(w zMk4SA(+w431Z-;-urKhNx>NP9e?icnH3aj1b0XDLb?Hec!)dWvBuW2h(P6c%PX6L~ z&*BaH0lUKBWJ3gdxNym`D=4r2cp4xj$9`JTuHkA8<37QSVDyqko$SAX;@A7B>{NHP zqBbUg_Jlu1c-gpNUS5aUy{Q(qbH|;*5VG4TCnql)HI%rn3!9D2$o+~)EhnC44<;cc zlPI2(PYoTvNybla+KM|hMx2A~%Xl5lfqd==SoIhh`G4irJ$ zZ<*Rn#MY1uRZz2f@)IKOiUy{U3T9-%H<+>2j(;D69u~8(#}#pVeGH#Pi~W@lt9*L& z9pw%2@P3w||4k3K`%e)vd6-{$op-fmJ+wSmCp8zKh3_?*0<^b_sZ$J%ZeK((aIsA5 z*Yg%~zU2x7RQ@fEc-u@j^swM}&#>}$h!IJ*xqun-xpov%b_U#BPMSHFOx=!)!&}T|2X#)_f%F>X~Ax)S_Bz4 z)e*bb;8w_X4kmA=c6+&F6T3!CcxC9@n81u(7Z_}~atOui>N{DYdA{wtFxh2CRm=Ba zE6?Nc#BCU9saf6vV(7lnNA*@&<1@V~L-t=KGc{FU(4=T}*V_xrK!;9}H14DrVehJ< zm?Wtv^sIg9L+UUVV236=4dLkIQ)qK{Qdg}2->jG-kIO5uLM}7ycem2J7_vo@PDxh_ z)JZ4WR@If=Q^1dmk5BKdZO0jxq3LlLH)tQlB(-waNq9NNs#$C*+3s>N|cW4>kSUNFyc9>x4rVffG@eQ%S3W}IYbDI(K9w@Z*qnS-N zaOm_RJc0PORN;%T{0}>niS*gy-2Jd*WZsPkez^mtx_XhPnFBLk9&~E$i`Ld>rPqbe z0Js;UM=g3Zi#RX8>+FBpK;1b_rBwwxletH}_3`PO+eC#qTVLjBJ!Qo9(V4f&nCRt^ z6fYa2L$+!DvMTwhSTfJDW-{>osi?%Ocmk;Ya9>FU+&PL*S(`)MpBUHwK7wY1*tL_d zGTT5*>xx|Jej&*6A_rd*%8&=yH=j!<<)sBvk+cq}hObC|k8mS6ABM?PwF1w#%9H1y z-!4T>u^G)VFQc%L4?lh^+dJ}L_yhkma+L2uIxh`pX|s;SooHO46Eg84Bzr~c&n>~C zauZEzZj;Eb?$7-XE+Ph%ipj6u2-|EPva16{*wu*_pWtoYnckED52TI-0mb(EtBpXY z`0;XVwF_YsS`BGPd>>O@Cx$1I55#I!ugiIg9T~<>Jx|K6d1%%PD)A(DFsO;~G}9Kj z&4#!C<6IrK`9;Jwr6CC_C7G3&m--XoATW9Cclo`mwj@-4=n(VxJf7cg!+c9FBK1~` z(O*}uZ4wxiyR|;D>UO_{_J<9w{Whwt>XA`SlPTXAOgxl_s+GW~L=iPKRobihG$LH^ zR(eb>?QFCYxJ=Nv)sgyZ>K!u(%#jYaDUoSW$pBW6$68N2m*NAO$lLqZCdGX3urnlL zJ68s_cZeeOTyR5>%H1yKthQt-5%oL{TjtAXFR1p^TCMJ4M6v!x;AJ_J{j`r%lY;%% zNmLJBa)4LYeypTP@5h@C?4ACxlr2q|b>~{<6zZkl1uV}*Dn=OEinX6<$qAGRT+vba zFt^IH5!TwA#m{Kw^>u3CX__>mR{!{3s)8K5j`w)!VrDZ42@ln9JtRM$wToQ$C#=qE z;s5Rqu~aioGy2m951m~lID6qE*w^9`llF#tvYNU(gg|2?JsZG0TuAFo?r*s`SDtm`vetP0xbyzqkU{poqe= zWR}_IIu4xU?X8GQ=Tk#yi3Wy4)T5I2JJra5o^649FDix12b;1%Tu&^omYN0-oL_~Q zebQEdg`P(n8~gT;DB_;<+ound8{q>I?Yy9siLC~v8S1+Y#Jfu%%2z9sBP1_!3Ff>+ zE=mIuou|=hs&Q2BLjun^rK>-pq!(tL%=ET->R0ngff~tIbpZ&^M6Pg(&U8_THNe&J zw#WRr{Y*bEe#|2`-;tah_9GO+*UB*$ZKZV@`=Uh=L4i-=z3YqpP7UIBizaNa?nyza z5h!X4V|0qYT~%nA`o^VZ(GJhpoTW=rDeD-#dl}G-h2j?y^1=nEVJLr_B;EfUq)i9n zMYCWObd@TH_&f<^%mBl9BCb&?IVg0MY+&84Eq?n@RCM=SJeuO-3R01twGdP|?h(KT zsZyj+Do-xFeeNjq z$)l2(B{ABJ8BiXvaIzse+D~5KrUDPRk|Y~bMu?d7ZjGoxBuv2~VXw{4Jo^4k1@Q}) zzzpQw0bhbMU)ZI)ii7esUs!qjUW^kA4|neetX?HuAbex@qAIu@ml%Bhde0i9iCGf~ zZff#SZ%&U8Z|oKnmZT$gz^v)~&#qxgYU{euGbyw@Iyp8PcCAn~Z5}>20X%uStzLg2 z?QCthJD=?_YFTys=n5VDZI`d$RjQ3bMnpfkT-Kuk^E-~tx!@`5KzfGM`-{S36E9Jl zqUFB{&)O`-7nZ!Ecl}7)Q|PO~U7euRCOiR&c0-xrmg=2kz_?E6lbTTbHnTPhki6T- zEKLCj{>t$(8wUy=-ZR7t_qavWmjK<6p1KfqjZ|)=F8OFmSe+LAe^3C*0}sk{rE@R- zKuyo~;JAh(m1CH_x^OKFzGz@^cNd4a{U&G%7?+6XX*)6#IlOmGPO19V`%HygU|>3bS<7VV>yp+8sle2@O8Bq`dU?dX@CUb#mo}iedJs$ z^^LVJ@qOxnBe?u+Xa%vr+G@vxF>SmC9bV?E1StrH6Y6{UBVXJ?z1=Yu=cDFJ8h+AG zez?;20NwQy*V%>r+oBGUy+Mhe;pub`ppdD)vTIL zsC}NF!uEvrDPE?K$4k)z6sf~7wgw68r7xL@jc_Pg^fnM>21T*%v>*%&pv}~8ev_R7 zP=thac04j~_a3F|qorq3^m%M@h{k8~wkUZ7ng)^eV=woPoO8*NOnTZVS&Y`f!Ch

>py3+J9g->gu< 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

a%Bvef%VPL<24*spMJ$14o51K6KL&?!4at( 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

;Y-ftZ`31b6(;U;#ooOV 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 MmNSA2U>yPd3j_;nsQ>@~ literal 0 HcmV?d00001 From 121476b33f514529861089a9f8261613d6b01a21 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:10:02 +0200 Subject: [PATCH 084/122] New console app for off sim simulations --- .vscode/launch.json | 46 ++ Cargo.lock | 129 ++++++ Cargo.toml | 2 +- .../Cargo.toml | 19 + .../src/main.rs | 432 ++++++++---------- src/systems/systems/src/hydraulic/mod.rs | 25 +- 6 files changed, 413 insertions(+), 240 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/systems/a320_hydraulic_simulation_graphs/Cargo.toml diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..67f291e9d08 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,46 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'a320_systems'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=a320_systems" + ], + "filter": { + "name": "a320_systems", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'systems'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=systems" + ], + "filter": { + "name": "systems", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ac0ac256d5e..ff41ef2aff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,21 @@ # 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" @@ -19,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" @@ -54,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" @@ -156,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" @@ -262,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" @@ -338,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" @@ -437,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" @@ -461,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" @@ -570,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" @@ -600,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" @@ -611,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 2321dc9f857..fd83c593e23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,5 @@ members = [ "src/systems/a320_systems", "src/systems/a320_systems_wasm", "src/systems/systems", - # "src/systems/a320_hydraulic_simulation_graphs" + "src/systems/a320_hydraulic_simulation_graphs", ] 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 index 5ec64194216..8dd60db1e41 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -1,19 +1,19 @@ -use systems::simulation::UpdateContext; use systems::engine::Engine; +use systems::simulation::UpdateContext; + pub use systems::hydraulic::*; use std::time::Duration; use uom::si::{ - ratio::percent, acceleration::foot_per_second_squared, f64::*, length::foot, pressure::{pascal, psi}, + ratio::percent, thermodynamic_temperature::degree_celsius, - time::second, + velocity::knot, volume::{gallon, liter}, volume_rate::gallon_per_second, - velocity::knot, }; use plotlib::page::Page; @@ -24,12 +24,77 @@ 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 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..."); green_loop_edp_simulation(); + yellow_green_ptu_loop_simulation(); } fn make_figure(h: &History) -> Figure { @@ -155,8 +220,10 @@ fn green_loop_edp_simulation() { 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"); - edp1.active = true; + let green_loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); let init_n2 = Ratio::new::(55.0); let engine1 = engine(init_n2); @@ -173,10 +240,10 @@ fn green_loop_edp_simulation() { green_loop_history.init( 0.0, vec![ - green_loop.loop_pressure.get::(), - green_loop.loop_volume.get::(), - green_loop.reservoir_volume.get::(), - green_loop.current_flow.get::(), + green_loop.get_pressure().get::(), + green_loop.get_loop_fluid_volume().get::(), + green_loop.get_reservoir_volume().get::(), + green_loop.get_current_flow().get::(), ], ); edp1_history.init( @@ -189,67 +256,68 @@ fn green_loop_edp_simulation() { accu_green_history.init( 0.0, vec![ - green_loop.loop_pressure.get::(), - green_loop.accumulator_gas_pressure.get::(), - green_loop.accumulator_fluid_volume.get::(), - green_loop.accumulator_gas_volume.get::(), + green_loop.get_pressure().get::(), + green_loop.get_accumulator_gas_pressure().get::(), + green_loop.get_accumulator_fluid_volume().get::(), + green_loop.get_accumulator_gas_volume().get::(), ], ); for x in 0..600 { if x == 50 { //After 5s - assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); + assert!(green_loop.get_pressure() >= Pressure::new::(2850.0)); } if x == 200 { - assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); - edp1.stop(); + assert!(green_loop.get_pressure() >= Pressure::new::(2850.0)); + edp1_controller.command_depressurise(); } if x >= 500 { //Shutdown + 30s - assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); + assert!(green_loop.get_pressure() <= Pressure::new::(250.0)); } - edp1.update(&context.delta(), &green_loop, &engine1); + edp1.update(&context.delta(), &green_loop, &engine1, &edp1_controller); green_loop.update( &context.delta(), Vec::new(), vec![&edp1], Vec::new(), Vec::new(), + &green_loop_controller, ); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); - println!("---PSI: {}", green_loop.loop_pressure.get::()); + println!("---PSI: {}", green_loop.get_pressure().get::()); println!( "--------Reservoir Volume (g): {}", - green_loop.reservoir_volume.get::() + green_loop.get_reservoir_volume().get::() ); println!( "--------Loop Volume (g): {}", - green_loop.loop_volume.get::() + green_loop.get_loop_fluid_volume().get::() ); println!( "--------Acc Fluid Volume (L): {}", - green_loop.accumulator_fluid_volume.get::() + green_loop.get_accumulator_fluid_volume().get::() ); println!( "--------Acc Gas Volume (L): {}", - green_loop.accumulator_gas_volume.get::() + green_loop.get_accumulator_gas_volume().get::() ); println!( "--------Acc Gas Pressure (psi): {}", - green_loop.accumulator_gas_pressure.get::() + green_loop.get_accumulator_gas_pressure().get::() ); } green_loop_history.update( context.delta().as_secs_f64(), vec![ - green_loop.loop_pressure.get::(), - green_loop.loop_volume.get::(), - green_loop.reservoir_volume.get::(), - green_loop.current_flow.get::(), + green_loop.get_pressure().get::(), + green_loop.get_loop_fluid_volume().get::(), + green_loop.get_reservoir_volume().get::(), + green_loop.get_current_flow().get::(), ], ); edp1_history.update( @@ -262,10 +330,10 @@ fn green_loop_edp_simulation() { accu_green_history.update( context.delta().as_secs_f64(), vec![ - green_loop.loop_pressure.get::(), - green_loop.accumulator_gas_pressure.get::(), - green_loop.accumulator_fluid_volume.get::(), - green_loop.accumulator_gas_volume.get::(), + green_loop.get_pressure().get::(), + green_loop.get_accumulator_gas_pressure().get::(), + green_loop.get_accumulator_fluid_volume().get::(), + green_loop.get_accumulator_gas_volume().get::(), ], ); } @@ -287,8 +355,7 @@ fn yellow_green_ptu_loop_simulation() { let mut loop_history = History::new(loop_var_names); let ptu_var_names = vec![ - "GREEN side flow".to_string(), - "YELLOW side flow".to_string(), + "Current flow".to_string(), "Press delta".to_string(), "PTU active GREEN".to_string(), "PTU active YELLOW".to_string(), @@ -312,140 +379,141 @@ fn yellow_green_ptu_loop_simulation() { let mut accu_yellow_history = History::new(yellow_acc_var_names); let mut epump = electric_pump(); - epump.stop(); + let mut epump_controller = TestPumpController::commanding_depressurise(); let mut yellow_loop = hydraulic_loop("YELLOW"); let mut edp1 = engine_driven_pump(); - assert!(!edp1.active); //Is off when created? + let mut edp1_controller = TestPumpController::commanding_depressurise(); let mut engine1 = engine(Ratio::new::(0.0)); let mut green_loop = hydraulic_loop("GREEN"); - let mut ptu = Ptu::new(""); + 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.loop_pressure.get::(), - yellow_loop.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::(), + green_loop.get_pressure().get::(), + yellow_loop.get_pressure().get::(), + green_loop.get_reservoir_volume().get::(), + yellow_loop.get_reservoir_volume().get::(), + green_loop.get_current_delta_vol().get::(), + yellow_loop.get_current_delta_vol().get::(), ], ); ptu_history.init( 0.0, vec![ - ptu.flow_to_left.get::(), - ptu.flow_to_right.get::(), - green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), - ptu.is_active_left as i8 as f64, - ptu.is_active_right as i8 as f64, + ptu.get_flow().get::(), + green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), + ptu.get_is_active_left_to_right() as i8 as f64, + ptu.get_is_active_right_to_left() as i8 as f64, ], ); accu_green_history.init( 0.0, vec![ - green_loop.loop_pressure.get::(), - green_loop.accumulator_gas_pressure.get::(), - green_loop.accumulator_fluid_volume.get::(), - green_loop.accumulator_gas_volume.get::(), + green_loop.get_pressure().get::(), + green_loop.get_accumulator_gas_pressure().get::(), + green_loop.get_accumulator_fluid_volume().get::(), + green_loop.get_accumulator_gas_volume().get::(), ], ); accu_yellow_history.init( 0.0, vec![ - yellow_loop.loop_pressure.get::(), - yellow_loop.accumulator_gas_pressure.get::(), - yellow_loop.accumulator_fluid_volume.get::(), - yellow_loop.accumulator_gas_volume.get::(), + yellow_loop.get_pressure().get::(), + yellow_loop.get_accumulator_gas_pressure().get::(), + yellow_loop.get_accumulator_fluid_volume().get::(), + yellow_loop.get_accumulator_gas_volume().get::(), ], ); - let yellow_res_at_start = yellow_loop.reservoir_volume; - let green_res_at_start = green_loop.reservoir_volume; + let yellow_res_at_start = yellow_loop.get_reservoir_volume(); + let green_res_at_start = green_loop.get_reservoir_volume(); engine1.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { //After 1s powering electric pump println!("------------YELLOW EPUMP ON------------"); - assert!(yellow_loop.loop_pressure <= Pressure::new::(50.0)); - assert!(yellow_loop.reservoir_volume == yellow_res_at_start); + assert!(yellow_loop.get_pressure() <= Pressure::new::(50.0)); + assert!(yellow_loop.get_reservoir_volume() == yellow_res_at_start); - assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); - assert!(green_loop.reservoir_volume == green_res_at_start); + assert!(green_loop.get_pressure() <= Pressure::new::(50.0)); + assert!(green_loop.get_reservoir_volume() == green_res_at_start); - epump.start(); + epump_controller.command_pressurise(); } if x == 110 { //10s later enabling ptu println!("--------------PTU ENABLED--------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2950.0)); - assert!(yellow_loop.reservoir_volume <= yellow_res_at_start); + assert!(yellow_loop.get_pressure() >= Pressure::new::(2950.0)); + assert!(yellow_loop.get_reservoir_volume() <= yellow_res_at_start); - assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); - assert!(green_loop.reservoir_volume == green_res_at_start); + assert!(green_loop.get_pressure() <= Pressure::new::(50.0)); + assert!(green_loop.get_reservoir_volume() == green_res_at_start); - ptu.enabling(true); + ptu_controller.command_enable(); } if x == 300 { //@30s, ptu should be supplying green loop println!("----------PTU SUPPLIES GREEN------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2400.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2400.0)); + assert!(yellow_loop.get_pressure() >= Pressure::new::(2400.0)); + assert!(green_loop.get_pressure() >= Pressure::new::(2400.0)); } if x == 400 { //@40s enabling edp println!("------------GREEN EDP1 ON------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); - edp1.start(); + assert!(yellow_loop.get_pressure() >= Pressure::new::(2600.0)); + assert!(green_loop.get_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.loop_pressure >= Pressure::new::(2900.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(!ptu.is_active_left && !ptu.is_active_right); + assert!(yellow_loop.get_pressure() >= Pressure::new::(2900.0)); + assert!(green_loop.get_pressure() >= Pressure::new::(2900.0)); } if x == 600 { //@60s diabling edp and epump println!("-------------ALL PUMPS OFF------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - edp1.stop(); - epump.stop(); + assert!(yellow_loop.get_pressure() >= Pressure::new::(2900.0)); + assert!(green_loop.get_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.loop_pressure < Pressure::new::(50.0)); - assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); + assert!(yellow_loop.get_pressure() < Pressure::new::(50.0)); + assert!(green_loop.get_pressure() <= Pressure::new::(50.0)); assert!( - green_loop.reservoir_volume > Volume::new::(0.0) - && green_loop.reservoir_volume <= green_res_at_start + green_loop.get_reservoir_volume() > Volume::new::(0.0) + && green_loop.get_reservoir_volume() <= green_res_at_start ); assert!( - yellow_loop.reservoir_volume > Volume::new::(0.0) - && yellow_loop.reservoir_volume <= yellow_res_at_start + yellow_loop.get_reservoir_volume() > Volume::new::(0.0) + && yellow_loop.get_reservoir_volume() <= yellow_res_at_start ); } - ptu.update(&green_loop, &yellow_loop); - edp1.update(&context.delta(), &green_loop, &engine1); - epump.update(&context.delta(), &yellow_loop); + ptu.update(&green_loop, &yellow_loop, &ptu_controller); + edp1.update(&context.delta(), &green_loop, &engine1, &edp1_controller); + epump.update(&context.delta(), &yellow_loop, &epump_controller); yellow_loop.update( &context.delta(), @@ -453,6 +521,7 @@ fn yellow_green_ptu_loop_simulation() { Vec::new(), Vec::new(), vec![&ptu], + &loop_controller, ); green_loop.update( &context.delta(), @@ -460,65 +529,65 @@ fn yellow_green_ptu_loop_simulation() { vec![&edp1], Vec::new(), vec![&ptu], + &loop_controller, ); loop_history.update( context.delta().as_secs_f64(), vec![ - green_loop.loop_pressure.get::(), - yellow_loop.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::(), + green_loop.get_pressure().get::(), + yellow_loop.get_pressure().get::(), + green_loop.get_reservoir_volume().get::(), + yellow_loop.get_reservoir_volume().get::(), + green_loop.get_current_delta_vol().get::(), + yellow_loop.get_current_delta_vol().get::(), ], ); ptu_history.update( context.delta().as_secs_f64(), vec![ - ptu.flow_to_left.get::(), - ptu.flow_to_right.get::(), - green_loop.loop_pressure.get::() - yellow_loop.loop_pressure.get::(), - ptu.is_active_left as i8 as f64, - ptu.is_active_right as i8 as f64, + ptu.get_flow().get::(), + green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), + ptu.get_is_active_left_to_right() as i8 as f64, + ptu.get_is_active_right_to_left() as i8 as f64, ], ); accu_green_history.update( context.delta().as_secs_f64(), vec![ - green_loop.loop_pressure.get::(), - green_loop.accumulator_gas_pressure.get::(), - green_loop.accumulator_fluid_volume.get::(), - green_loop.accumulator_gas_volume.get::(), + green_loop.get_pressure().get::(), + green_loop.get_accumulator_gas_pressure().get::(), + green_loop.get_accumulator_fluid_volume().get::(), + green_loop.get_accumulator_gas_volume().get::(), ], ); accu_yellow_history.update( context.delta().as_secs_f64(), vec![ - yellow_loop.loop_pressure.get::(), - yellow_loop.accumulator_gas_pressure.get::(), - yellow_loop.accumulator_fluid_volume.get::(), - yellow_loop.accumulator_gas_volume.get::(), + yellow_loop.get_pressure().get::(), + yellow_loop.get_accumulator_gas_pressure().get::(), + yellow_loop.get_accumulator_fluid_volume().get::(), + yellow_loop.get_accumulator_gas_volume().get::(), ], ); if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); - println!("---PSI YELLOW: {}", yellow_loop.loop_pressure.get::()); - println!("---RPM YELLOW: {}", epump.rpm); + println!("---PSI YELLOW: {}", yellow_loop.get_pressure().get::()); + println!("---RPM YELLOW: {}", epump.get_rpm()); println!( "---Priming State: {}/{}", - yellow_loop.loop_volume.get::(), - yellow_loop.max_loop_volume.get::() + yellow_loop.get_loop_fluid_volume().get::(), + yellow_loop.get_max_volume().get::() ); - println!("---PSI GREEN: {}", green_loop.loop_pressure.get::()); + println!("---PSI GREEN: {}", green_loop.get_pressure().get::()); println!("---N2 GREEN: {}", engine1.corrected_n2.get::()); println!( "---Priming State: {}/{}", - green_loop.loop_volume.get::(), - green_loop.max_loop_volume.get::() + green_loop.get_loop_fluid_volume().get::(), + green_loop.get_max_volume().get::() ); } } @@ -530,9 +599,9 @@ fn yellow_green_ptu_loop_simulation() { accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); } -fn hydraulic_loop(loop_color: &str) -> HydLoop { +fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { match loop_color { - "GREEN" => HydLoop::new( + "GREEN" => HydraulicLoop::new( loop_color, true, false, @@ -542,8 +611,10 @@ fn hydraulic_loop(loop_color: &str) -> HydLoop { Volume::new::(3.83), HydFluid::new(Pressure::new::(1450000000.0)), true, + Pressure::new::(1450.), + Pressure::new::(1750.), ), - "YELLOW" => HydLoop::new( + "YELLOW" => HydraulicLoop::new( loop_color, false, true, @@ -553,8 +624,10 @@ fn hydraulic_loop(loop_color: &str) -> HydLoop { Volume::new::(3.3), HydFluid::new(Pressure::new::(1450000000.0)), true, + Pressure::new::(1450.), + Pressure::new::(1750.), ), - _ => HydLoop::new( + _ => HydraulicLoop::new( loop_color, false, false, @@ -564,6 +637,8 @@ fn hydraulic_loop(loop_color: &str) -> HydLoop { Volume::new::(1.5), HydFluid::new(Pressure::new::(1450000000.0)), false, + Pressure::new::(1450.), + Pressure::new::(1750.), ), } } @@ -593,122 +668,3 @@ fn context(delta_time: Duration) -> UpdateContext { Acceleration::new::(0.), ) } - -struct PressureCaracteristic { - pressure: Pressure, - rpm_tab: Vec, - flow_tab: Vec, -} - - -fn show_carac(figure_title: &str, output_caracteristics: &[PressureCaracteristic]) { - use rustplotlib::{Axes2D, Line2D}; - - let mut all_axis: Vec> = Vec::new(); - let colors = ["blue", "yellow", "red", "black", "cyan", "magenta", "green"]; - let linestyles = ["--", "-.", "-"]; - let mut curr_axis = Axes2D::new(); - curr_axis = curr_axis.grid(true); - let mut color_idx = 0; - let mut style_idx = 0; - for cur_pressure in output_caracteristics { - let press_str = format!("P={:.0}", cur_pressure.pressure.get::()); - curr_axis = curr_axis - .add( - Line2D::new(press_str.as_str()) - .data(&cur_pressure.rpm_tab, &cur_pressure.flow_tab) - .color(colors[color_idx]) - //.marker("x") - .linestyle(linestyles[style_idx]) - .linewidth(1.0), - ) - .xlabel("RPM") - .ylabel("Max Flow") - .legend("best") - .xlim(0.0, *cur_pressure.rpm_tab.last().unwrap()); - //.ylim(-2.0, 2.0); - color_idx = (color_idx + 1) % colors.len(); - style_idx = (style_idx + 1) % linestyles.len(); - } - all_axis.push(Some(curr_axis)); - let fig = Figure::new().subplots(all_axis.len() as u32, 1, all_axis); - - use rustplotlib::backend::Matplotlib; - use rustplotlib::Backend; - let mut mpl = Matplotlib::new().unwrap(); - mpl.set_style("ggplot").unwrap(); - - fig.apply(&mut mpl).unwrap(); - - let _result = mpl.savefig(figure_title); - - mpl.wait().unwrap(); -} - -fn epump_charac() { - let mut output_caracteristics: Vec = Vec::new(); - let mut epump = ElectricPump::new("YELLOW"); - let context = context(Duration::from_secs_f64(0.0001)); //Small dt to freeze spool up effect - - let mut green_loop = hydraulic_loop("GREEN"); - - epump.start(); - for pressure in (0..3500).step_by(500) { - let mut rpm_tab: Vec = Vec::new(); - let mut flow_tab: Vec = Vec::new(); - for rpm in (0..10000).step_by(150) { - green_loop.loop_pressure = Pressure::new::(pressure as f64); - epump.rpm = rpm as f64; - epump.update(&context.delta(), &green_loop); - rpm_tab.push(rpm as f64); - let flow = epump.get_delta_vol_max() - / Time::new::(context.delta().as_secs_f64()); - let flow_gal = flow.get::() as f64; - flow_tab.push(flow_gal); - } - output_caracteristics.push(PressureCaracteristic { - pressure: green_loop.loop_pressure, - rpm_tab, - flow_tab, - }); - } - show_carac("Epump_carac", &output_caracteristics); -} - -fn engine_d_pump_charac() { - let mut output_caracteristics: Vec = Vec::new(); - let mut edpump = EngineDrivenPump::new("GREEN"); - - let mut green_loop = hydraulic_loop("GREEN"); - let mut engine1 = engine(Ratio::new::(0.0)); - - edpump.start(); - let context = context(Duration::from_secs_f64(1.0)); //Small dt to freeze spool up effect - - edpump.update(&context.delta(), &green_loop, &engine1); - for pressure in (0..3500).step_by(500) { - let mut rpm_tab: Vec = Vec::new(); - let mut flow_tab: Vec = Vec::new(); - for rpm in (0..10000).step_by(150) { - green_loop.loop_pressure = Pressure::new::(pressure as f64); - - engine1.corrected_n2 = Ratio::new::( - (rpm as f64) - / (EngineDrivenPump::PUMP_N2_GEAR_RATIO - * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM), - ); - edpump.update(&context.delta(), &green_loop, &engine1); - rpm_tab.push(rpm as f64); - let flow = edpump.get_delta_vol_max() - / Time::new::(context.delta().as_secs_f64()); - let flow_gal = flow.get::() as f64; - flow_tab.push(flow_gal); - } - output_caracteristics.push(PressureCaracteristic { - pressure: green_loop.loop_pressure, - rpm_tab, - flow_tab, - }); - } - show_carac("Eng_Driv_pump_carac", &output_caracteristics); -} diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 62f99f63f1f..6bd931593c4 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -84,7 +84,6 @@ pub trait PowerTransferUnitController { fn should_enable(&self) -> bool; } -//TODO enhance simulation with RPM and variable displacement on one side? pub struct PowerTransferUnit { is_enabled: bool, is_active_right: bool, @@ -419,6 +418,22 @@ impl HydraulicLoop { } } + pub fn get_current_flow(&self) -> VolumeRate { + self.current_flow + } + + pub fn get_current_delta_vol(&self) -> Volume { + self.current_delta_vol + } + + pub fn get_accumulator_gas_pressure(&self) -> Pressure { + self.accumulator.gas_pressure + } + + pub fn get_accumulator_fluid_volume(&self) -> Volume { + self.accumulator.fluid_volume + } + pub fn get_pressure(&self) -> Pressure { self.loop_pressure } @@ -431,6 +446,14 @@ impl HydraulicLoop { self.loop_volume } + pub fn get_max_volume(&self) -> Volume { + self.max_loop_volume + } + + pub fn get_accumulator_gas_volume(&self) -> Volume { + self.accumulator.gas_volume + } + pub fn get_usable_reservoir_fluid(&self, amount: Volume) -> Volume { let mut drawn = amount; if amount > self.reservoir_volume { From d70dbe992b8d47b5cee914e48bbc099846c74876 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 26 Apr 2021 19:26:32 +0200 Subject: [PATCH 085/122] Faster brake response --- src/systems/systems/src/hydraulic/brakecircuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 04db8c34f0b..9144a81aa95 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -29,7 +29,7 @@ pub struct BrakeActuator { } impl BrakeActuator { - const ACTUATOR_BASE_SPEED: f64 = 1.; // movement in percent/100 per second. 1 means 0 to 1 in 1s + 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.; From 038201f730f6cd479ebb02374b32123f956f3994 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 27 Apr 2021 09:11:27 +0200 Subject: [PATCH 086/122] Brake norm/altn switch added hysteresis --- src/systems/a320_systems/src/hydraulic.rs | 57 ++++++++++++++++++----- src/systems/systems/src/hydraulic/mod.rs | 6 +-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 3cde3b988f2..306bc7d0171 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -306,7 +306,7 @@ impl A320Hydraulic { min_hyd_loop_timestep: Duration, ) { // Process brake logic (which circuit brakes) and send brake demands (how much) - self.hyd_brake_logic.update_brake_demands(&self.green_loop); + self.hyd_brake_logic.update_brake_demands(&self.green_loop, &self.braking_circuit_altn); self.hyd_brake_logic.update_brake_pressure_limitation( &mut self.braking_circuit_norm, &mut self.braking_circuit_altn, @@ -743,16 +743,24 @@ pub struct A320HydraulicBrakingLogic { left_brake_yellow_output: f64, right_brake_green_output: f64, right_brake_yellow_output: f64, + normal_brakes_available: bool, anti_skid_activated: bool, autobrakes_setting: u8, } -//Implements brakes computers logic +// Implements brakes computers logic impl A320HydraulicBrakingLogic { - const MIN_PRESSURE_BRAKE_ALTN: f64 = 2000.; //Minimum pressure until main switched on ALTN brakes + // 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.; pub fn new() -> A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { - parking_brake_demand: true, //Position of parking brake lever + parking_brake_demand: true, // Position of parking brake lever weight_on_wheels: true, left_brake_pilot_input: 0.0, right_brake_pilot_input: 0.0, @@ -760,11 +768,23 @@ impl A320HydraulicBrakingLogic { left_brake_yellow_output: 1.0, // Actual command sent to left yellow circuit. Init 1 as considering park brake on on init right_brake_green_output: 0.0, // Actual command sent to right green circuit right_brake_yellow_output: 1.0, // Actual command sent to right yellow circuit. Init 1 as considering park brake on on init + normal_brakes_available: false, 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; + } + } + pub fn update_brake_pressure_limitation( &mut self, norm_brk: &mut BrakeCircuit, @@ -799,12 +819,15 @@ impl A320HydraulicBrakingLogic { } // Updates final brake demands per hydraulic loop based on pilot pedal demands - //TODO: think about where to build those brake demands from autobrake if not from brake pedals - pub fn update_brake_demands(&mut self, green_loop: &HydraulicLoop) { - let green_used_for_brakes = green_loop.get_pressure() //TODO Check this logic - > Pressure::new::(A320HydraulicBrakingLogic::MIN_PRESSURE_BRAKE_ALTN ) - && self.anti_skid_activated - && !self.parking_brake_demand; + pub fn update_brake_demands( + &mut self, + green_loop: &HydraulicLoop, + alternate_circuit: &BrakeCircuit, + ) { + self.update_normal_braking_availability(&green_loop.get_pressure()); + + 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; @@ -815,13 +838,23 @@ impl A320HydraulicBrakingLogic { self.left_brake_green_output = 0.; self.right_brake_green_output = 0.; if !self.parking_brake_demand { - //Normal braking but using alternate circuit + // 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 + // 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.get_brake_pressure_left().get::() + < Self::MIN_PRESSURE_PARK_BRAKE_EMERGENCY + || alternate_circuit.get_brake_pressure_right().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; + } } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 6bd931593c4..20c637b0db1 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -98,12 +98,12 @@ impl PowerTransferUnit { const FLOW_DYNAMIC_LOW_PASS_LEFT_SIDE: f64 = 0.3; const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.3; - const EFFICIENCY_LEFT_TO_RIGHT: f64 = 0.81; - const EFFICIENCY_RIGHT_TO_LEFT: f64 = 0.70; + const EFFICIENCY_LEFT_TO_RIGHT: f64 = 0.98; + const EFFICIENCY_RIGHT_TO_LEFT: f64 = 0.98; //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.72; + const AGRESSIVENESS_FACTOR: f64 = 0.8; pub fn new() -> Self { Self { From 6fbf544398d53e2dae8d97cd01c4406cd3a64383 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 27 Apr 2021 10:42:21 +0200 Subject: [PATCH 087/122] further ptu adjustements --- src/systems/systems/src/hydraulic/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 20c637b0db1..034f9906974 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -95,15 +95,15 @@ pub struct PowerTransferUnit { 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.3; - const FLOW_DYNAMIC_LOW_PASS_RIGHT_SIDE: f64 = 0.3; + 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.98; - const EFFICIENCY_RIGHT_TO_LEFT: f64 = 0.98; + 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.8; + const AGRESSIVENESS_FACTOR: f64 = 0.78; pub fn new() -> Self { Self { From 45110defe5b1ad580b45d169b9ff4fa035873389 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 27 Apr 2021 10:52:39 +0200 Subject: [PATCH 088/122] cargo fmt --- src/systems/a320_systems/src/hydraulic.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 306bc7d0171..51ecc36b5db 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -306,7 +306,8 @@ impl A320Hydraulic { min_hyd_loop_timestep: Duration, ) { // Process brake logic (which circuit brakes) and send brake demands (how much) - self.hyd_brake_logic.update_brake_demands(&self.green_loop, &self.braking_circuit_altn); + self.hyd_brake_logic + .update_brake_demands(&self.green_loop, &self.braking_circuit_altn); self.hyd_brake_logic.update_brake_pressure_limitation( &mut self.braking_circuit_norm, &mut self.braking_circuit_altn, From 019bb4211e4919432c78092e1ce84e3f3d34bfe3 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 27 Apr 2021 18:32:22 +0200 Subject: [PATCH 089/122] Added auto braking on gear retraction --- src/systems/a320_systems/src/hydraulic.rs | 237 +++++++++++++++++++--- src/systems/a320_systems/src/lib.rs | 1 + src/systems/a320_systems_wasm/src/lib.rs | 3 + 3 files changed, 213 insertions(+), 28 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 51ecc36b5db..852a5070d2a 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,7 +1,6 @@ use std::time::Duration; use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; -use systems::engine::Engine; use systems::hydraulic::{ ElectricPump, EngineDrivenPump, HydFluid, HydraulicLoop, HydraulicLoopController, PowerTransferUnit, PowerTransferUnitController, PumpController, RamAirTurbine, @@ -13,7 +12,11 @@ use systems::overhead::{ use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; -use systems::{hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate}; +use systems::{engine::Engine, landing_gear::LandingGear}; +use systems::{ + hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate, + shared::DelayedTrueLogicGate, +}; pub(super) struct A320Hydraulic { hyd_brake_logic: A320HydraulicBrakingLogic, @@ -153,6 +156,7 @@ impl A320Hydraulic { engine2: &Engine, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, + landing_gear: &LandingGear, ) { let min_hyd_loop_timestep = Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz @@ -200,6 +204,7 @@ impl A320Hydraulic { engine2, overhead_panel, engine_fire_overhead, + landing_gear, min_hyd_loop_timestep, ); } @@ -296,6 +301,7 @@ impl A320Hydraulic { fn update_blue_actuators_volume(&mut self) {} // All the core hydraulics updates that needs to be done at the slowest fixed step rate + #[allow(clippy::too_many_arguments)] fn update_fixed_step( &mut self, context: &UpdateContext, @@ -303,11 +309,16 @@ impl A320Hydraulic { engine2: &Engine, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, + landing_gear: &LandingGear, min_hyd_loop_timestep: Duration, ) { // Process brake logic (which circuit brakes) and send brake demands (how much) - self.hyd_brake_logic - .update_brake_demands(&self.green_loop, &self.braking_circuit_altn); + self.hyd_brake_logic.update_brake_demands( + &context.with_delta(min_hyd_loop_timestep), + &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, @@ -738,6 +749,7 @@ impl SimulationElement for A320RamAirTurbineController { pub 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, @@ -745,6 +757,7 @@ pub struct A320HydraulicBrakingLogic { 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, } @@ -759,10 +772,13 @@ impl A320HydraulicBrakingLogic { // Feedback by avteknisyan — 25/04/2021 #pilot-feedback const MIN_PRESSURE_PARK_BRAKE_EMERGENCY: f64 = 507.; + const AUTOBRAKE_GEAR_RETRACTION_DURATION_S: f64 = 3.; + pub fn new() -> A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { parking_brake_demand: true, // Position of parking brake lever weight_on_wheels: true, + is_gear_lever_down: true, left_brake_pilot_input: 0.0, right_brake_pilot_input: 0.0, left_brake_green_output: 0.0, // Actual command sent to left green circuit @@ -770,6 +786,9 @@ impl A320HydraulicBrakingLogic { right_brake_green_output: 0.0, // Actual command sent to right green circuit right_brake_yellow_output: 1.0, // Actual command sent to right yellow circuit. Init 1 as considering park brake on on init 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, } @@ -822,39 +841,60 @@ impl A320HydraulicBrakingLogic { // Updates final brake demands per hydraulic loop based on pilot pedal demands pub fn update_brake_demands( &mut self, + context: &UpdateContext, green_loop: &HydraulicLoop, alternate_circuit: &BrakeCircuit, + landing_gear: &LandingGear, ) { self.update_normal_braking_availability(&green_loop.get_pressure()); - let green_used_for_brakes = - self.normal_brakes_available && self.anti_skid_activated && !self.parking_brake_demand; + 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 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.; + 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.; + self.left_brake_yellow_output = 0.; + self.right_brake_yellow_output = 0.; + } else { + self.left_brake_green_output = 0.2; // Slight brake pressure to stop the spinning wheels + self.right_brake_green_output = 0.2; // Slight brake pressure to stop the spinning wheels + } } 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; + 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 { - // 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.get_brake_pressure_left().get::() - < Self::MIN_PRESSURE_PARK_BRAKE_EMERGENCY - || alternate_circuit.get_brake_pressure_right().get::() + 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.get_brake_pressure_left().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; + || alternate_circuit.get_brake_pressure_right().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; + } } } } @@ -882,6 +922,7 @@ 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; @@ -946,6 +987,12 @@ impl SimulationElement for PushbackTug { self.previous_angle = self.angle; self.angle = state.read_f64("PUSHBACK ANGLE"); self.state = state.read_f64("PUSHBACK STATE"); + // println!( + // "TUGstate={}, TUGangle={}, isPushing={}", + // self.state, + // self.angle, + // self.is_pushing() + // ); } } @@ -1071,6 +1118,7 @@ mod tests { hydraulics: A320Hydraulic, overhead: A320HydraulicOverheadPanel, engine_fire_overhead: A320EngineFireOverheadPanel, + landing_gear: LandingGear, } impl A320HydraulicsTestAircraft { fn new() -> Self { @@ -1080,6 +1128,7 @@ mod tests { hydraulics: A320Hydraulic::new(), overhead: A320HydraulicOverheadPanel::new(), engine_fire_overhead: A320EngineFireOverheadPanel::new(), + landing_gear: LandingGear::new(), } } @@ -1128,6 +1177,7 @@ mod tests { &self.engine_2, &self.overhead, &self.engine_fire_overhead, + &self.landing_gear, ); self.overhead.update(&self.hydraulics); @@ -1138,6 +1188,7 @@ mod tests { self.hydraulics.accept(visitor); self.overhead.accept(visitor); self.engine_fire_overhead.accept(visitor); + self.landing_gear.accept(visitor); visitor.visit(self); } @@ -1282,6 +1333,18 @@ mod tests { 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::(60.)) + .start_eng2(Ratio::new::(60.)) + .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 @@ -1355,6 +1418,24 @@ mod tests { 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); @@ -1411,6 +1492,7 @@ mod tests { .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 { @@ -2221,6 +2303,105 @@ mod tests { assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); } + #[test] + fn check_auto_brake_at_gear_retraction_test() { + 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 check_brakes_inactive_in_flight_test() { + 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.)); + + // 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 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 writes_its_state() { let mut hyd_logic = A320Hydraulic::new(); diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 097cd63bb6d..fd3852ca101 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -110,6 +110,7 @@ impl Aircraft for A320 { &self.engine_2, &self.hydraulic_overhead, &self.engine_fire_overhead, + &self.landing_gear, ); self.hydraulic_overhead.update(&self.hydraulic); diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index 5134c1f28a1..b1bafdd13f2 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, @@ -77,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)?, @@ -117,6 +119,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(), From 12d7e058908534c7562cfb1a6caa139f6301550b Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Wed, 28 Apr 2021 13:23:58 +0200 Subject: [PATCH 090/122] Updated TUG behavior for PTU inhibit Angle value can be maxed out when pushing left or right from EFB, now PTU is considered attached whenever angle changed, then only detached when push-back state is back to 3 --- src/systems/a320_systems/src/hydraulic.rs | 40 ++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 852a5070d2a..7b1feeda531 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -169,8 +169,8 @@ impl A320Hydraulic { let number_of_steps_floating_point = time_to_catch.as_secs_f64() / min_hyd_loop_timestep.as_secs_f64(); - // Updating rat stowed pos on all frames in case it's used for graphics - self.ram_air_turbine.update_position(&context.delta()); + // 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 @@ -273,6 +273,15 @@ impl A320Hydraulic { 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 @@ -678,7 +687,7 @@ impl A320PowerTransferUnitController { forward_cargo_door.has_moved() || aft_cargo_door.has_moved(), ); self.nose_wheel_steering_pin_inserted - .update(context, pushback_tug.is_pushing()); + .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(); @@ -963,7 +972,9 @@ struct PushbackTug { // 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.; @@ -972,12 +983,25 @@ impl PushbackTug { Self { angle: 0., previous_angle: 0., - state: 0., + state: Self::STATE_NO_PUSHBACK, + is_connected_to_nose_gear: false, + } + } + + pub 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 + } + fn is_pushing(&self) -> bool { - // The angle keeps changing while pushing. + // 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 } @@ -987,12 +1011,6 @@ impl SimulationElement for PushbackTug { self.previous_angle = self.angle; self.angle = state.read_f64("PUSHBACK ANGLE"); self.state = state.read_f64("PUSHBACK STATE"); - // println!( - // "TUGstate={}, TUGangle={}, isPushing={}", - // self.state, - // self.angle, - // self.is_pushing() - // ); } } From 6736b9bb3a6b935de1f369cf230a09eb0a9a7195 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 29 Apr 2021 15:01:56 +0200 Subject: [PATCH 091/122] refactor: timestep via context --- .../src/main.rs | 26 ++-- src/systems/a320_systems/src/hydraulic.rs | 41 +++--- .../systems/src/apu/air_intake_flap.rs | 2 +- src/systems/systems/src/apu/aps3200.rs | 10 +- .../systems/src/hydraulic/brakecircuit.rs | 65 +++++---- src/systems/systems/src/hydraulic/mod.rs | 131 +++++++++--------- .../systems/src/simulation/update_context.rs | 10 +- 7 files changed, 148 insertions(+), 137 deletions(-) diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 8dd60db1e41..a44dbe87605 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -276,9 +276,9 @@ fn green_loop_edp_simulation() { assert!(green_loop.get_pressure() <= Pressure::new::(250.0)); } - edp1.update(&context.delta(), &green_loop, &engine1, &edp1_controller); + edp1.update(&context, &green_loop, &engine1, &edp1_controller); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&edp1], Vec::new(), @@ -312,7 +312,7 @@ fn green_loop_edp_simulation() { } green_loop_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), green_loop.get_loop_fluid_volume().get::(), @@ -321,14 +321,14 @@ fn green_loop_edp_simulation() { ], ); edp1_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ edp1.get_delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); accu_green_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), green_loop.get_accumulator_gas_pressure().get::(), @@ -512,11 +512,11 @@ fn yellow_green_ptu_loop_simulation() { } ptu.update(&green_loop, &yellow_loop, &ptu_controller); - edp1.update(&context.delta(), &green_loop, &engine1, &edp1_controller); - epump.update(&context.delta(), &yellow_loop, &epump_controller); + edp1.update(&context, &green_loop, &engine1, &edp1_controller); + epump.update(&context, &yellow_loop, &epump_controller); yellow_loop.update( - &context.delta(), + &context, vec![&epump], Vec::new(), Vec::new(), @@ -524,7 +524,7 @@ fn yellow_green_ptu_loop_simulation() { &loop_controller, ); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&edp1], Vec::new(), @@ -533,7 +533,7 @@ fn yellow_green_ptu_loop_simulation() { ); loop_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), yellow_loop.get_pressure().get::(), @@ -544,7 +544,7 @@ fn yellow_green_ptu_loop_simulation() { ], ); ptu_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ ptu.get_flow().get::(), green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), @@ -554,7 +554,7 @@ fn yellow_green_ptu_loop_simulation() { ); accu_green_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), green_loop.get_accumulator_gas_pressure().get::(), @@ -563,7 +563,7 @@ fn yellow_green_ptu_loop_simulation() { ], ); accu_yellow_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ yellow_loop.get_pressure().get::(), yellow_loop.get_accumulator_gas_pressure().get::(), diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 7b1feeda531..c55281a4694 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -199,13 +199,12 @@ impl A320Hydraulic { self.update_actuators_volume(); self.update_fixed_step( - context, + &context.with_delta(min_hyd_loop_timestep), engine1, engine2, overhead_panel, engine_fire_overhead, landing_gear, - min_hyd_loop_timestep, ); } @@ -310,7 +309,6 @@ impl A320Hydraulic { fn update_blue_actuators_volume(&mut self) {} // All the core hydraulics updates that needs to be done at the slowest fixed step rate - #[allow(clippy::too_many_arguments)] fn update_fixed_step( &mut self, context: &UpdateContext, @@ -319,11 +317,10 @@ impl A320Hydraulic { overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, landing_gear: &LandingGear, - min_hyd_loop_timestep: Duration, ) { // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic.update_brake_demands( - &context.with_delta(min_hyd_loop_timestep), + context, &self.green_loop, &self.braking_circuit_altn, &landing_gear, @@ -338,7 +335,7 @@ impl A320Hydraulic { ); self.power_transfer_unit_controller.update( - &context.with_delta(min_hyd_loop_timestep), + context, overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, @@ -353,7 +350,7 @@ impl A320Hydraulic { self.engine_driven_pump_1_controller .update(overhead_panel, engine_fire_overhead); self.engine_driven_pump_1.update( - &min_hyd_loop_timestep, + context, &self.green_loop, &engine1, &self.engine_driven_pump_1_controller, @@ -362,7 +359,7 @@ impl A320Hydraulic { self.engine_driven_pump_2_controller .update(overhead_panel, engine_fire_overhead); self.engine_driven_pump_2.update( - &min_hyd_loop_timestep, + context, &self.yellow_loop, &engine2, &self.engine_driven_pump_2_controller, @@ -370,34 +367,30 @@ impl A320Hydraulic { self.blue_electric_pump_controller.update(overhead_panel); self.blue_electric_pump.update( - &min_hyd_loop_timestep, + context, &self.blue_loop, &self.blue_electric_pump_controller, ); self.yellow_electric_pump_controller.update( - &context.with_delta(min_hyd_loop_timestep), + context, overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, ); self.yellow_electric_pump.update( - &min_hyd_loop_timestep, + context, &self.yellow_loop, &self.yellow_electric_pump_controller, ); - self.ram_air_turbine_controller - .update(&context.with_delta(min_hyd_loop_timestep)); - self.ram_air_turbine.update( - &min_hyd_loop_timestep, - &self.blue_loop, - &self.ram_air_turbine_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( - &min_hyd_loop_timestep, + context, Vec::new(), vec![&self.engine_driven_pump_1], Vec::new(), @@ -407,7 +400,7 @@ impl A320Hydraulic { self.yellow_loop_controller.update(engine_fire_overhead); self.yellow_loop.update( - &min_hyd_loop_timestep, + context, vec![&self.yellow_electric_pump], vec![&self.engine_driven_pump_2], Vec::new(), @@ -417,7 +410,7 @@ impl A320Hydraulic { self.blue_loop_controller.update(engine_fire_overhead); self.blue_loop.update( - &min_hyd_loop_timestep, + context, vec![&self.blue_electric_pump], Vec::new(), vec![&self.ram_air_turbine], @@ -425,10 +418,8 @@ impl A320Hydraulic { &self.blue_loop_controller, ); - self.braking_circuit_norm - .update(&min_hyd_loop_timestep, &self.green_loop); - self.braking_circuit_altn - .update(&min_hyd_loop_timestep, &self.yellow_loop); + self.braking_circuit_norm.update(context, &self.green_loop); + self.braking_circuit_altn.update(context, &self.yellow_loop); } } impl SimulationElement for A320Hydraulic { 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/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 9144a81aa95..8e973462bc5 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -62,8 +62,8 @@ impl BrakeActuator { Pressure::new::(Self::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI) * self.current_position } - pub fn update(&mut self, delta_time: &Duration, received_pressure: Pressure) { - let final_delta_position = self.update_position(delta_time, received_pressure); + pub 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; @@ -77,7 +77,7 @@ impl BrakeActuator { self.volume_to_res_accumulator = Volume::new::(0.); } - fn update_position(&mut self, delta_time: &Duration, loop_pressure: Pressure) -> f64 { + 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 @@ -86,10 +86,10 @@ impl BrakeActuator { let mut new_position = self.current_position; if delta_position_required > 0.001 { - new_position = self.current_position + delta_time.as_secs_f64() * self.base_speed; + 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 - delta_time.as_secs_f64() * self.base_speed; + 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.); @@ -225,7 +225,7 @@ impl BrakeCircuit { self.pressure_limitation_active = is_pressure_limit_active; } - fn update_brake_actuators(&mut self, delta_time: &Duration, hyd_pressure: Pressure) { + 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 @@ -239,12 +239,12 @@ impl BrakeCircuit { } self.left_brake_actuator - .update(delta_time, actual_max_allowed_pressure); + .update(context, actual_max_allowed_pressure); self.right_brake_actuator - .update(delta_time, actual_max_allowed_pressure); + .update(context, actual_max_allowed_pressure); } - pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydraulicLoop) { + 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.get_fluid_volume() > Volume::new::(0.) { @@ -253,7 +253,7 @@ impl BrakeCircuit { actual_pressure_available = hyd_loop.get_pressure(); } - self.update_brake_actuators(delta_time, actual_pressure_available); + self.update_brake_actuators(context, actual_pressure_available); let delta_vol = self.left_brake_actuator.get_used_volume() + self.right_brake_actuator.get_used_volume(); @@ -261,7 +261,7 @@ impl BrakeCircuit { if self.has_accumulator { let mut volume_into_accumulator = Volume::new::(0.); self.accumulator.update( - delta_time, + context, &mut volume_into_accumulator, hyd_loop.loop_pressure, ); @@ -293,7 +293,7 @@ impl BrakeCircuit { + (actual_pressure_available - self.accumulator_fluid_pressure_sensor_filtered) * (1. - E.powf( - -delta_time.as_secs_f64() + -context.delta_as_secs_f64() / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, )); } @@ -444,7 +444,7 @@ mod tests { for loop_idx in 0..15 { brake_actuator.update( - &Duration::from_secs_f64(0.1), + &context(Duration::from_secs_f64(0.1)), Pressure::new::(BrakeActuator::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI), ); println!( @@ -466,7 +466,10 @@ mod tests { brake_actuator.set_position_demand(-2.); for _ in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(3000.), + ); } assert!(brake_actuator.current_position <= 0.01); @@ -481,7 +484,10 @@ mod tests { brake_actuator.set_position_demand(1.2); for _ in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(20.), + ); } // We should not be able to move actuator @@ -500,7 +506,7 @@ mod tests { 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(&Duration::from_secs_f64(0.1), medium_pressure); + brake_actuator.update(&context(Duration::from_secs_f64(0.1)), medium_pressure); println!( "Loop {}, position: {}", loop_idx, brake_actuator.current_position @@ -517,7 +523,10 @@ mod tests { brake_actuator.set_position_demand(1.2); for _loop_idx in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(20.), + ); println!( "Loop {}, Low pressure: position: {}", _loop_idx, brake_actuator.current_position @@ -585,7 +594,7 @@ mod tests { < Pressure::new::(10.0) ); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -594,7 +603,7 @@ mod tests { ); brake_circuit_primed.set_brake_demand_left(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= Pressure::new::(50.)); @@ -602,7 +611,7 @@ mod tests { brake_circuit_primed.set_brake_demand_left(0.0); brake_circuit_primed.set_brake_demand_right(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); assert!(brake_circuit_primed.accumulator.get_fluid_volume() >= Volume::new::(0.1)); @@ -627,7 +636,7 @@ mod tests { < Pressure::new::(10.0) ); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -636,14 +645,14 @@ mod tests { ); brake_circuit_primed.set_brake_demand_left(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= 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(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); assert!(brake_circuit_primed.accumulator.get_fluid_volume() == Volume::new::(0.0)); @@ -662,7 +671,7 @@ mod tests { Volume::new::(0.1), ); - brake_circuit_primed.update(&Duration::from_secs_f64(5.), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(5.)), &hyd_loop); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -672,25 +681,25 @@ mod tests { brake_circuit_primed.set_brake_demand_left(1.0); brake_circuit_primed.set_brake_demand_right(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2900.)); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); let pressure_limit = Pressure::new::(1200.); brake_circuit_primed.set_brake_press_limit(pressure_limit); - brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2900.)); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); brake_circuit_primed.set_brake_limit_ena(true); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + 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.get_brake_pressure_left() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); - brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + 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.get_brake_pressure_left() <= pressure_limit); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 034f9906974..ef20200827c 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -11,9 +11,9 @@ use uom::si::{ volume_rate::gallon_per_second, }; -use crate::engine::Engine; use crate::shared::interpolation; use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; +use crate::{engine::Engine, simulation::UpdateContext}; pub mod brakecircuit; use crate::hydraulic::brakecircuit::ActuatorHydInterface; @@ -268,7 +268,7 @@ impl HydraulicAccumulator { } } - fn update(&mut self, delta_time: &Duration, delta_vol: &mut Volume, loop_pressure: Pressure) { + 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, @@ -284,7 +284,7 @@ impl HydraulicAccumulator { if accumulator_delta_press.get::() > 0.0 && !self.has_control_valve { let volume_from_acc = self .fluid_volume - .min(flow_variation * Time::new::(delta_time.as_secs_f64())); + .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; @@ -293,7 +293,7 @@ impl HydraulicAccumulator { } else if accumulator_delta_press.get::() < 0.0 { let volume_to_acc = delta_vol .max(Volume::new::(0.0)) - .max(flow_variation * Time::new::(delta_time.as_secs_f64())); + .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; @@ -301,7 +301,7 @@ impl HydraulicAccumulator { *delta_vol -= volume_to_acc; } - self.current_flow = self.current_delta_vol / Time::new::(delta_time.as_secs_f64()); + 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); } @@ -496,7 +496,7 @@ impl HydraulicLoop { fn update_ptu_flows( &mut self, - delta_time: &Duration, + context: &UpdateContext, ptus: Vec<&PowerTransferUnit>, delta_vol: &mut Volume, reservoir_return: &mut Volume, @@ -510,40 +510,32 @@ impl HydraulicLoop { } 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, - Time::new::(delta_time.as_secs_f64()), - ); - self.reservoir_volume -= - actual_flow * Time::new::(delta_time.as_secs_f64()); + 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 * Time::new::(delta_time.as_secs_f64()); + *reservoir_return -= actual_flow * context.delta_as_time(); } - *delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); + *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, - Time::new::(delta_time.as_secs_f64()), - ); - self.reservoir_volume -= - actual_flow * Time::new::(delta_time.as_secs_f64()); + 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 * Time::new::(delta_time.as_secs_f64()); + *reservoir_return -= actual_flow * context.delta_as_time(); } - *delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); + *delta_vol += actual_flow * context.delta_as_time(); } } self.ptu_active = ptu_act; @@ -551,7 +543,7 @@ impl HydraulicLoop { pub fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, electric_pumps: Vec<&ElectricPump>, engine_driven_pumps: Vec<&EngineDrivenPump>, ram_air_pumps: Vec<&RamAirTurbine>, @@ -581,14 +573,14 @@ impl HydraulicLoop { } // Storing max pump capacity available. for now used in PTU model to limit it's input flow - self.current_max_flow = delta_vol_max / Time::new::(delta_time.as_secs_f64()); + 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 - * delta_time.as_secs_f64() + * context.delta_as_secs_f64() * (self.loop_pressure.get::() - 14.7) / 3000.0, ); @@ -598,11 +590,11 @@ impl HydraulicLoop { reservoir_return += static_leaks_vol; //Updates current delta_vol and reservoir return quantity based on current ptu flows - self.update_ptu_flows(delta_time, ptus, &mut delta_vol, &mut reservoir_return); + self.update_ptu_flows(context, ptus, &mut delta_vol, &mut reservoir_return); // Updates current accumulator state and updates loop delta_vol self.accumulator - .update(delta_time, &mut delta_vol, self.loop_pressure); + .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 @@ -650,7 +642,7 @@ impl HydraulicLoop { self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure self.current_delta_vol = delta_vol; - self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); + self.current_flow = delta_vol / context.delta_as_time(); if self.loop_pressure <= self.min_pressure_pressurised_lo_hyst { self.is_pressurised = false; @@ -707,7 +699,7 @@ impl Pump { fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, line: &HydraulicLoop, rpm: f64, controller: &T, @@ -721,7 +713,7 @@ impl Pump { let flow = Self::calculate_flow(rpm, self.current_displacement) .max(VolumeRate::new::(0.)); - self.delta_vol_max = flow * Time::new::(delta_time.as_secs_f64()); + self.delta_vol_max = flow * context.delta_as_time(); self.delta_vol_min = Volume::new::(0.0); } @@ -790,22 +782,22 @@ impl ElectricPump { pub fn update( &mut self, - delta_time: &Duration, + 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) * delta_time.as_secs_f64(); + 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) * delta_time.as_secs_f64(); + 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(delta_time, line, self.rpm, controller); + self.pump.update(context, line, self.rpm, controller); self.is_active = controller.should_pressurise(); } @@ -860,7 +852,7 @@ impl EngineDrivenPump { pub fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, line: &HydraulicLoop, engine: &Engine, controller: &T, @@ -868,7 +860,7 @@ impl EngineDrivenPump { let n2_rpm = engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; let pump_rpm = n2_rpm * Self::PUMP_N2_GEAR_RATIO; - self.pump.update(delta_time, line, pump_rpm, controller); + self.pump.update(context, line, pump_rpm, controller); self.is_active = controller.should_pressurise(); } @@ -1052,14 +1044,14 @@ impl RamAirTurbine { pub fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, line: &HydraulicLoop, controller: &T, ) { self.deployment_commanded = controller.should_deploy(); self.pump.update( - delta_time, + context, line, self.wind_turbine.get_rpm(), &self.pump_controller, @@ -1247,9 +1239,9 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&context.delta(), &green_loop, &engine1, &pump_controller); + edp1.update(&context, &green_loop, &engine1, &pump_controller); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&edp1], Vec::new(), @@ -1304,9 +1296,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } - epump.update(&context.delta(), &yellow_loop, &pump_controller); + epump.update(&context, &yellow_loop, &pump_controller); yellow_loop.update( - &context.delta(), + &context, vec![&epump], Vec::new(), Vec::new(), @@ -1354,9 +1346,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } - epump.update(&context.delta(), &blue_loop, &pump_controller); + epump.update(&context, &blue_loop, &pump_controller); blue_loop.update( - &context.delta(), + &context, vec![&epump], Vec::new(), Vec::new(), @@ -1436,9 +1428,9 @@ mod tests { } rat.update_physics(&context.delta(), &indicated_airspeed); - rat.update(&context.delta(), &blue_loop, &rat_controller); + rat.update(&context, &blue_loop, &rat_controller); blue_loop.update( - &context.delta(), + &context, Vec::new(), Vec::new(), vec![&rat], @@ -1570,15 +1562,15 @@ mod tests { ptu.update(&green_loop, &yellow_loop, &ptu_controller); engine_driven_pump.update( - &context.delta(), + &context, &green_loop, &engine1, &engine_driven_pump_controller, ); - electric_pump.update(&context.delta(), &yellow_loop, &electric_pump_controller); + electric_pump.update(&context, &yellow_loop, &electric_pump_controller); yellow_loop.update( - &context.delta(), + &context, vec![&electric_pump], Vec::new(), Vec::new(), @@ -1586,7 +1578,7 @@ mod tests { &yellow_loop_controller, ); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&engine_driven_pump], Vec::new(), @@ -1699,47 +1691,60 @@ mod tests { fn zero_flow_above_3000_psi_after_25ms() { let n2 = Ratio::new::(60.0); let pressure = Pressure::new::(3100.); - let time = Duration::from_millis(25); + let context = context(Duration::from_millis(25)); let displacement = Volume::new::(0.); - assert!(delta_vol_equality_check(n2, displacement, pressure, time)) + assert!(delta_vol_equality_check( + n2, + displacement, + pressure, + &context + )) } fn delta_vol_equality_check( n2: Ratio, displacement: Volume, pressure: Pressure, - time: Duration, + context: &UpdateContext, ) -> bool { - let actual = get_edp_actual_delta_vol_when(n2, pressure, time); - let predicted = get_edp_predicted_delta_vol_when(n2, displacement, time); + let actual = get_edp_actual_delta_vol_when(n2, pressure, context); + let predicted = get_edp_predicted_delta_vol_when(n2, displacement, context); println!("Actual: {}", actual.get::()); println!("Predicted: {}", predicted.get::()); actual == predicted } - fn get_edp_actual_delta_vol_when(n2: Ratio, pressure: Pressure, time: Duration) -> Volume { + fn get_edp_actual_delta_vol_when( + n2: Ratio, + pressure: Pressure, + context: &UpdateContext, + ) -> Volume { let eng = engine(n2); let mut edp = engine_driven_pump(); - let dummy_update = Duration::from_secs(1); let mut line = hydraulic_loop("GREEN"); let engine_driven_pump_controller = TestPumpController::commanding_pressurise(); line.loop_pressure = pressure; - edp.update(&dummy_update, &line, &eng, &engine_driven_pump_controller); //Update 10 times to stabilize displacement + edp.update( + &context.with_delta(Duration::from_secs(1)), + &line, + &eng, + &engine_driven_pump_controller, + ); //Update 10 times to stabilize displacement - edp.update(&time, &line, &eng, &engine_driven_pump_controller); + edp.update(context, &line, &eng, &engine_driven_pump_controller); edp.get_delta_vol_max() } fn get_edp_predicted_delta_vol_when( n2: Ratio, displacement: Volume, - time: Duration, + context: &UpdateContext, ) -> Volume { let n2_rpm = n2.get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; let edp_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; let expected_flow = Pump::calculate_flow(edp_rpm, displacement); - expected_flow * Time::new::(time.as_secs_f64()) + expected_flow * context.delta_as_time() } } } diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index 4099dfe422d..eac091a6f41 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -1,7 +1,7 @@ use std::time::Duration; use uom::si::{ acceleration::foot_per_second_squared, f64::*, length::foot, - thermodynamic_temperature::degree_celsius, velocity::knot, + thermodynamic_temperature::degree_celsius, time::second, velocity::knot, }; use super::SimulatorReader; @@ -70,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 } From feac1edec868d28990cf3cee4552b7c2ac4ec4dd Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 29 Apr 2021 15:01:56 +0200 Subject: [PATCH 092/122] refactor: timestep via context --- .../src/main.rs | 26 ++-- src/systems/a320_systems/src/hydraulic.rs | 41 +++--- .../systems/src/apu/air_intake_flap.rs | 2 +- src/systems/systems/src/apu/aps3200.rs | 10 +- .../systems/src/hydraulic/brakecircuit.rs | 65 +++++---- src/systems/systems/src/hydraulic/mod.rs | 133 +++++++++--------- .../systems/src/simulation/update_context.rs | 10 +- 7 files changed, 148 insertions(+), 139 deletions(-) diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 8dd60db1e41..a44dbe87605 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -276,9 +276,9 @@ fn green_loop_edp_simulation() { assert!(green_loop.get_pressure() <= Pressure::new::(250.0)); } - edp1.update(&context.delta(), &green_loop, &engine1, &edp1_controller); + edp1.update(&context, &green_loop, &engine1, &edp1_controller); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&edp1], Vec::new(), @@ -312,7 +312,7 @@ fn green_loop_edp_simulation() { } green_loop_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), green_loop.get_loop_fluid_volume().get::(), @@ -321,14 +321,14 @@ fn green_loop_edp_simulation() { ], ); edp1_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ edp1.get_delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); accu_green_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), green_loop.get_accumulator_gas_pressure().get::(), @@ -512,11 +512,11 @@ fn yellow_green_ptu_loop_simulation() { } ptu.update(&green_loop, &yellow_loop, &ptu_controller); - edp1.update(&context.delta(), &green_loop, &engine1, &edp1_controller); - epump.update(&context.delta(), &yellow_loop, &epump_controller); + edp1.update(&context, &green_loop, &engine1, &edp1_controller); + epump.update(&context, &yellow_loop, &epump_controller); yellow_loop.update( - &context.delta(), + &context, vec![&epump], Vec::new(), Vec::new(), @@ -524,7 +524,7 @@ fn yellow_green_ptu_loop_simulation() { &loop_controller, ); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&edp1], Vec::new(), @@ -533,7 +533,7 @@ fn yellow_green_ptu_loop_simulation() { ); loop_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), yellow_loop.get_pressure().get::(), @@ -544,7 +544,7 @@ fn yellow_green_ptu_loop_simulation() { ], ); ptu_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ ptu.get_flow().get::(), green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), @@ -554,7 +554,7 @@ fn yellow_green_ptu_loop_simulation() { ); accu_green_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ green_loop.get_pressure().get::(), green_loop.get_accumulator_gas_pressure().get::(), @@ -563,7 +563,7 @@ fn yellow_green_ptu_loop_simulation() { ], ); accu_yellow_history.update( - context.delta().as_secs_f64(), + context.delta_as_secs_f64(), vec![ yellow_loop.get_pressure().get::(), yellow_loop.get_accumulator_gas_pressure().get::(), diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 7b1feeda531..c55281a4694 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -199,13 +199,12 @@ impl A320Hydraulic { self.update_actuators_volume(); self.update_fixed_step( - context, + &context.with_delta(min_hyd_loop_timestep), engine1, engine2, overhead_panel, engine_fire_overhead, landing_gear, - min_hyd_loop_timestep, ); } @@ -310,7 +309,6 @@ impl A320Hydraulic { fn update_blue_actuators_volume(&mut self) {} // All the core hydraulics updates that needs to be done at the slowest fixed step rate - #[allow(clippy::too_many_arguments)] fn update_fixed_step( &mut self, context: &UpdateContext, @@ -319,11 +317,10 @@ impl A320Hydraulic { overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, landing_gear: &LandingGear, - min_hyd_loop_timestep: Duration, ) { // Process brake logic (which circuit brakes) and send brake demands (how much) self.hyd_brake_logic.update_brake_demands( - &context.with_delta(min_hyd_loop_timestep), + context, &self.green_loop, &self.braking_circuit_altn, &landing_gear, @@ -338,7 +335,7 @@ impl A320Hydraulic { ); self.power_transfer_unit_controller.update( - &context.with_delta(min_hyd_loop_timestep), + context, overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, @@ -353,7 +350,7 @@ impl A320Hydraulic { self.engine_driven_pump_1_controller .update(overhead_panel, engine_fire_overhead); self.engine_driven_pump_1.update( - &min_hyd_loop_timestep, + context, &self.green_loop, &engine1, &self.engine_driven_pump_1_controller, @@ -362,7 +359,7 @@ impl A320Hydraulic { self.engine_driven_pump_2_controller .update(overhead_panel, engine_fire_overhead); self.engine_driven_pump_2.update( - &min_hyd_loop_timestep, + context, &self.yellow_loop, &engine2, &self.engine_driven_pump_2_controller, @@ -370,34 +367,30 @@ impl A320Hydraulic { self.blue_electric_pump_controller.update(overhead_panel); self.blue_electric_pump.update( - &min_hyd_loop_timestep, + context, &self.blue_loop, &self.blue_electric_pump_controller, ); self.yellow_electric_pump_controller.update( - &context.with_delta(min_hyd_loop_timestep), + context, overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, ); self.yellow_electric_pump.update( - &min_hyd_loop_timestep, + context, &self.yellow_loop, &self.yellow_electric_pump_controller, ); - self.ram_air_turbine_controller - .update(&context.with_delta(min_hyd_loop_timestep)); - self.ram_air_turbine.update( - &min_hyd_loop_timestep, - &self.blue_loop, - &self.ram_air_turbine_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( - &min_hyd_loop_timestep, + context, Vec::new(), vec![&self.engine_driven_pump_1], Vec::new(), @@ -407,7 +400,7 @@ impl A320Hydraulic { self.yellow_loop_controller.update(engine_fire_overhead); self.yellow_loop.update( - &min_hyd_loop_timestep, + context, vec![&self.yellow_electric_pump], vec![&self.engine_driven_pump_2], Vec::new(), @@ -417,7 +410,7 @@ impl A320Hydraulic { self.blue_loop_controller.update(engine_fire_overhead); self.blue_loop.update( - &min_hyd_loop_timestep, + context, vec![&self.blue_electric_pump], Vec::new(), vec![&self.ram_air_turbine], @@ -425,10 +418,8 @@ impl A320Hydraulic { &self.blue_loop_controller, ); - self.braking_circuit_norm - .update(&min_hyd_loop_timestep, &self.green_loop); - self.braking_circuit_altn - .update(&min_hyd_loop_timestep, &self.yellow_loop); + self.braking_circuit_norm.update(context, &self.green_loop); + self.braking_circuit_altn.update(context, &self.yellow_loop); } } impl SimulationElement for A320Hydraulic { 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/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 9144a81aa95..8e973462bc5 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -62,8 +62,8 @@ impl BrakeActuator { Pressure::new::(Self::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI) * self.current_position } - pub fn update(&mut self, delta_time: &Duration, received_pressure: Pressure) { - let final_delta_position = self.update_position(delta_time, received_pressure); + pub 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; @@ -77,7 +77,7 @@ impl BrakeActuator { self.volume_to_res_accumulator = Volume::new::(0.); } - fn update_position(&mut self, delta_time: &Duration, loop_pressure: Pressure) -> f64 { + 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 @@ -86,10 +86,10 @@ impl BrakeActuator { let mut new_position = self.current_position; if delta_position_required > 0.001 { - new_position = self.current_position + delta_time.as_secs_f64() * self.base_speed; + 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 - delta_time.as_secs_f64() * self.base_speed; + 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.); @@ -225,7 +225,7 @@ impl BrakeCircuit { self.pressure_limitation_active = is_pressure_limit_active; } - fn update_brake_actuators(&mut self, delta_time: &Duration, hyd_pressure: Pressure) { + 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 @@ -239,12 +239,12 @@ impl BrakeCircuit { } self.left_brake_actuator - .update(delta_time, actual_max_allowed_pressure); + .update(context, actual_max_allowed_pressure); self.right_brake_actuator - .update(delta_time, actual_max_allowed_pressure); + .update(context, actual_max_allowed_pressure); } - pub fn update(&mut self, delta_time: &Duration, hyd_loop: &HydraulicLoop) { + 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.get_fluid_volume() > Volume::new::(0.) { @@ -253,7 +253,7 @@ impl BrakeCircuit { actual_pressure_available = hyd_loop.get_pressure(); } - self.update_brake_actuators(delta_time, actual_pressure_available); + self.update_brake_actuators(context, actual_pressure_available); let delta_vol = self.left_brake_actuator.get_used_volume() + self.right_brake_actuator.get_used_volume(); @@ -261,7 +261,7 @@ impl BrakeCircuit { if self.has_accumulator { let mut volume_into_accumulator = Volume::new::(0.); self.accumulator.update( - delta_time, + context, &mut volume_into_accumulator, hyd_loop.loop_pressure, ); @@ -293,7 +293,7 @@ impl BrakeCircuit { + (actual_pressure_available - self.accumulator_fluid_pressure_sensor_filtered) * (1. - E.powf( - -delta_time.as_secs_f64() + -context.delta_as_secs_f64() / BrakeCircuit::ACC_PRESSURE_SENSOR_FILTER_TIMECONST, )); } @@ -444,7 +444,7 @@ mod tests { for loop_idx in 0..15 { brake_actuator.update( - &Duration::from_secs_f64(0.1), + &context(Duration::from_secs_f64(0.1)), Pressure::new::(BrakeActuator::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI), ); println!( @@ -466,7 +466,10 @@ mod tests { brake_actuator.set_position_demand(-2.); for _ in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(3000.)); + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(3000.), + ); } assert!(brake_actuator.current_position <= 0.01); @@ -481,7 +484,10 @@ mod tests { brake_actuator.set_position_demand(1.2); for _ in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(20.), + ); } // We should not be able to move actuator @@ -500,7 +506,7 @@ mod tests { 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(&Duration::from_secs_f64(0.1), medium_pressure); + brake_actuator.update(&context(Duration::from_secs_f64(0.1)), medium_pressure); println!( "Loop {}, position: {}", loop_idx, brake_actuator.current_position @@ -517,7 +523,10 @@ mod tests { brake_actuator.set_position_demand(1.2); for _loop_idx in 0..15 { - brake_actuator.update(&Duration::from_secs_f64(0.1), Pressure::new::(20.)); + brake_actuator.update( + &context(Duration::from_secs_f64(0.1)), + Pressure::new::(20.), + ); println!( "Loop {}, Low pressure: position: {}", _loop_idx, brake_actuator.current_position @@ -585,7 +594,7 @@ mod tests { < Pressure::new::(10.0) ); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -594,7 +603,7 @@ mod tests { ); brake_circuit_primed.set_brake_demand_left(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= Pressure::new::(50.)); @@ -602,7 +611,7 @@ mod tests { brake_circuit_primed.set_brake_demand_left(0.0); brake_circuit_primed.set_brake_demand_right(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(1000.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); assert!(brake_circuit_primed.accumulator.get_fluid_volume() >= Volume::new::(0.1)); @@ -627,7 +636,7 @@ mod tests { < Pressure::new::(10.0) ); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(0.1)), &hyd_loop); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -636,14 +645,14 @@ mod tests { ); brake_circuit_primed.set_brake_demand_left(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_right() <= 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(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); assert!(brake_circuit_primed.accumulator.get_fluid_volume() == Volume::new::(0.0)); @@ -662,7 +671,7 @@ mod tests { Volume::new::(0.1), ); - brake_circuit_primed.update(&Duration::from_secs_f64(5.), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(5.)), &hyd_loop); assert!( brake_circuit_primed.get_brake_pressure_left() @@ -672,25 +681,25 @@ mod tests { brake_circuit_primed.set_brake_demand_left(1.0); brake_circuit_primed.set_brake_demand_right(1.0); - brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2900.)); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); let pressure_limit = Pressure::new::(1200.); brake_circuit_primed.set_brake_press_limit(pressure_limit); - brake_circuit_primed.update(&Duration::from_secs_f64(1.5), &hyd_loop); + brake_circuit_primed.update(&context(Duration::from_secs_f64(1.5)), &hyd_loop); assert!(brake_circuit_primed.get_brake_pressure_left() >= Pressure::new::(2900.)); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); brake_circuit_primed.set_brake_limit_ena(true); - brake_circuit_primed.update(&Duration::from_secs_f64(0.1), &hyd_loop); + 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.get_brake_pressure_left() >= Pressure::new::(2500.)); assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); - brake_circuit_primed.update(&Duration::from_secs_f64(1.), &hyd_loop); + 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.get_brake_pressure_left() <= pressure_limit); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 034f9906974..7862eefc9e4 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -5,15 +5,14 @@ use uom::si::{ f64::*, pressure::psi, ratio::percent, - time::second, velocity::knot, volume::{cubic_inch, gallon}, volume_rate::gallon_per_second, }; -use crate::engine::Engine; use crate::shared::interpolation; use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; +use crate::{engine::Engine, simulation::UpdateContext}; pub mod brakecircuit; use crate::hydraulic::brakecircuit::ActuatorHydInterface; @@ -268,7 +267,7 @@ impl HydraulicAccumulator { } } - fn update(&mut self, delta_time: &Duration, delta_vol: &mut Volume, loop_pressure: Pressure) { + 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, @@ -284,7 +283,7 @@ impl HydraulicAccumulator { if accumulator_delta_press.get::() > 0.0 && !self.has_control_valve { let volume_from_acc = self .fluid_volume - .min(flow_variation * Time::new::(delta_time.as_secs_f64())); + .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; @@ -293,7 +292,7 @@ impl HydraulicAccumulator { } else if accumulator_delta_press.get::() < 0.0 { let volume_to_acc = delta_vol .max(Volume::new::(0.0)) - .max(flow_variation * Time::new::(delta_time.as_secs_f64())); + .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; @@ -301,7 +300,7 @@ impl HydraulicAccumulator { *delta_vol -= volume_to_acc; } - self.current_flow = self.current_delta_vol / Time::new::(delta_time.as_secs_f64()); + 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); } @@ -496,7 +495,7 @@ impl HydraulicLoop { fn update_ptu_flows( &mut self, - delta_time: &Duration, + context: &UpdateContext, ptus: Vec<&PowerTransferUnit>, delta_vol: &mut Volume, reservoir_return: &mut Volume, @@ -510,40 +509,32 @@ impl HydraulicLoop { } 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, - Time::new::(delta_time.as_secs_f64()), - ); - self.reservoir_volume -= - actual_flow * Time::new::(delta_time.as_secs_f64()); + 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 * Time::new::(delta_time.as_secs_f64()); + *reservoir_return -= actual_flow * context.delta_as_time(); } - *delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); + *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, - Time::new::(delta_time.as_secs_f64()), - ); - self.reservoir_volume -= - actual_flow * Time::new::(delta_time.as_secs_f64()); + 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 * Time::new::(delta_time.as_secs_f64()); + *reservoir_return -= actual_flow * context.delta_as_time(); } - *delta_vol += actual_flow * Time::new::(delta_time.as_secs_f64()); + *delta_vol += actual_flow * context.delta_as_time(); } } self.ptu_active = ptu_act; @@ -551,7 +542,7 @@ impl HydraulicLoop { pub fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, electric_pumps: Vec<&ElectricPump>, engine_driven_pumps: Vec<&EngineDrivenPump>, ram_air_pumps: Vec<&RamAirTurbine>, @@ -581,14 +572,14 @@ impl HydraulicLoop { } // Storing max pump capacity available. for now used in PTU model to limit it's input flow - self.current_max_flow = delta_vol_max / Time::new::(delta_time.as_secs_f64()); + 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 - * delta_time.as_secs_f64() + * context.delta_as_secs_f64() * (self.loop_pressure.get::() - 14.7) / 3000.0, ); @@ -598,11 +589,11 @@ impl HydraulicLoop { reservoir_return += static_leaks_vol; //Updates current delta_vol and reservoir return quantity based on current ptu flows - self.update_ptu_flows(delta_time, ptus, &mut delta_vol, &mut reservoir_return); + self.update_ptu_flows(context, ptus, &mut delta_vol, &mut reservoir_return); // Updates current accumulator state and updates loop delta_vol self.accumulator - .update(delta_time, &mut delta_vol, self.loop_pressure); + .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 @@ -650,7 +641,7 @@ impl HydraulicLoop { self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure self.current_delta_vol = delta_vol; - self.current_flow = delta_vol / Time::new::(delta_time.as_secs_f64()); + self.current_flow = delta_vol / context.delta_as_time(); if self.loop_pressure <= self.min_pressure_pressurised_lo_hyst { self.is_pressurised = false; @@ -707,7 +698,7 @@ impl Pump { fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, line: &HydraulicLoop, rpm: f64, controller: &T, @@ -721,7 +712,7 @@ impl Pump { let flow = Self::calculate_flow(rpm, self.current_displacement) .max(VolumeRate::new::(0.)); - self.delta_vol_max = flow * Time::new::(delta_time.as_secs_f64()); + self.delta_vol_max = flow * context.delta_as_time(); self.delta_vol_min = Volume::new::(0.0); } @@ -790,22 +781,22 @@ impl ElectricPump { pub fn update( &mut self, - delta_time: &Duration, + 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) * delta_time.as_secs_f64(); + 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) * delta_time.as_secs_f64(); + 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(delta_time, line, self.rpm, controller); + self.pump.update(context, line, self.rpm, controller); self.is_active = controller.should_pressurise(); } @@ -860,7 +851,7 @@ impl EngineDrivenPump { pub fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, line: &HydraulicLoop, engine: &Engine, controller: &T, @@ -868,7 +859,7 @@ impl EngineDrivenPump { let n2_rpm = engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; let pump_rpm = n2_rpm * Self::PUMP_N2_GEAR_RATIO; - self.pump.update(delta_time, line, pump_rpm, controller); + self.pump.update(context, line, pump_rpm, controller); self.is_active = controller.should_pressurise(); } @@ -1052,14 +1043,14 @@ impl RamAirTurbine { pub fn update( &mut self, - delta_time: &Duration, + context: &UpdateContext, line: &HydraulicLoop, controller: &T, ) { self.deployment_commanded = controller.should_deploy(); self.pump.update( - delta_time, + context, line, self.wind_turbine.get_rpm(), &self.pump_controller, @@ -1129,7 +1120,6 @@ mod tests { length::foot, pressure::{pascal, psi}, thermodynamic_temperature::degree_celsius, - time::second, volume::{gallon, liter}, }; @@ -1247,9 +1237,9 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&context.delta(), &green_loop, &engine1, &pump_controller); + edp1.update(&context, &green_loop, &engine1, &pump_controller); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&edp1], Vec::new(), @@ -1304,9 +1294,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } - epump.update(&context.delta(), &yellow_loop, &pump_controller); + epump.update(&context, &yellow_loop, &pump_controller); yellow_loop.update( - &context.delta(), + &context, vec![&epump], Vec::new(), Vec::new(), @@ -1354,9 +1344,9 @@ mod tests { //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } - epump.update(&context.delta(), &blue_loop, &pump_controller); + epump.update(&context, &blue_loop, &pump_controller); blue_loop.update( - &context.delta(), + &context, vec![&epump], Vec::new(), Vec::new(), @@ -1436,9 +1426,9 @@ mod tests { } rat.update_physics(&context.delta(), &indicated_airspeed); - rat.update(&context.delta(), &blue_loop, &rat_controller); + rat.update(&context, &blue_loop, &rat_controller); blue_loop.update( - &context.delta(), + &context, Vec::new(), Vec::new(), vec![&rat], @@ -1570,15 +1560,15 @@ mod tests { ptu.update(&green_loop, &yellow_loop, &ptu_controller); engine_driven_pump.update( - &context.delta(), + &context, &green_loop, &engine1, &engine_driven_pump_controller, ); - electric_pump.update(&context.delta(), &yellow_loop, &electric_pump_controller); + electric_pump.update(&context, &yellow_loop, &electric_pump_controller); yellow_loop.update( - &context.delta(), + &context, vec![&electric_pump], Vec::new(), Vec::new(), @@ -1586,7 +1576,7 @@ mod tests { &yellow_loop_controller, ); green_loop.update( - &context.delta(), + &context, Vec::new(), vec![&engine_driven_pump], Vec::new(), @@ -1699,47 +1689,60 @@ mod tests { fn zero_flow_above_3000_psi_after_25ms() { let n2 = Ratio::new::(60.0); let pressure = Pressure::new::(3100.); - let time = Duration::from_millis(25); + let context = context(Duration::from_millis(25)); let displacement = Volume::new::(0.); - assert!(delta_vol_equality_check(n2, displacement, pressure, time)) + assert!(delta_vol_equality_check( + n2, + displacement, + pressure, + &context + )) } fn delta_vol_equality_check( n2: Ratio, displacement: Volume, pressure: Pressure, - time: Duration, + context: &UpdateContext, ) -> bool { - let actual = get_edp_actual_delta_vol_when(n2, pressure, time); - let predicted = get_edp_predicted_delta_vol_when(n2, displacement, time); + let actual = get_edp_actual_delta_vol_when(n2, pressure, context); + let predicted = get_edp_predicted_delta_vol_when(n2, displacement, context); println!("Actual: {}", actual.get::()); println!("Predicted: {}", predicted.get::()); actual == predicted } - fn get_edp_actual_delta_vol_when(n2: Ratio, pressure: Pressure, time: Duration) -> Volume { + fn get_edp_actual_delta_vol_when( + n2: Ratio, + pressure: Pressure, + context: &UpdateContext, + ) -> Volume { let eng = engine(n2); let mut edp = engine_driven_pump(); - let dummy_update = Duration::from_secs(1); let mut line = hydraulic_loop("GREEN"); let engine_driven_pump_controller = TestPumpController::commanding_pressurise(); line.loop_pressure = pressure; - edp.update(&dummy_update, &line, &eng, &engine_driven_pump_controller); //Update 10 times to stabilize displacement + edp.update( + &context.with_delta(Duration::from_secs(1)), + &line, + &eng, + &engine_driven_pump_controller, + ); //Update 10 times to stabilize displacement - edp.update(&time, &line, &eng, &engine_driven_pump_controller); + edp.update(context, &line, &eng, &engine_driven_pump_controller); edp.get_delta_vol_max() } fn get_edp_predicted_delta_vol_when( n2: Ratio, displacement: Volume, - time: Duration, + context: &UpdateContext, ) -> Volume { let n2_rpm = n2.get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; let edp_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; let expected_flow = Pump::calculate_flow(edp_rpm, displacement); - expected_flow * Time::new::(time.as_secs_f64()) + expected_flow * context.delta_as_time() } } } diff --git a/src/systems/systems/src/simulation/update_context.rs b/src/systems/systems/src/simulation/update_context.rs index 4099dfe422d..eac091a6f41 100644 --- a/src/systems/systems/src/simulation/update_context.rs +++ b/src/systems/systems/src/simulation/update_context.rs @@ -1,7 +1,7 @@ use std::time::Duration; use uom::si::{ acceleration::foot_per_second_squared, f64::*, length::foot, - thermodynamic_temperature::degree_celsius, velocity::knot, + thermodynamic_temperature::degree_celsius, time::second, velocity::knot, }; use super::SimulatorReader; @@ -70,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 } From 8f03a1b58e570caefbaecdd8dd542a8eb9977a41 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 16:22:47 +0200 Subject: [PATCH 093/122] Small adjustment to placeholder RAT model --- src/systems/systems/src/hydraulic/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 034f9906974..df4b51a3499 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -903,9 +903,9 @@ impl WindTurbine { const STOWED_ANGLE: f64 = std::f64::consts::PI / 2.; const PROPELLER_INERTIA: f64 = 2.; const RPM_GOVERNOR_BREAKPTS: [f64; 9] = [ - 0.0, 4000., 5000.0, 5500.0, 6000.0, 7500.0, 8000.0, 9000.0, 15000.0, + 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., 5., 1., 1.]; + const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 1., 1., 1.]; pub fn new() -> Self { Self { @@ -1018,9 +1018,9 @@ pub struct RamAirTurbine { } impl RamAirTurbine { const DISPLACEMENT_BREAKPTS: [f64; 9] = [ - 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, + 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, 1.15, 0.5, 0.0, 0.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. !!Warning, this will be affected by a different delta time const DISPLACEMENT_DYNAMICS: f64 = 0.2; From 0fafe4a0ae0be84029b4e2c235474c58765a69a0 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 16:48:15 +0200 Subject: [PATCH 094/122] Updated gitignore / json file --- .gitignore | 3 +- .vscode/launch.json | 46 ------------------- .../src/main.rs | 30 ++++++------ 3 files changed, 18 insertions(+), 61 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index ac5b19c7b79..ee205955d91 100644 --- a/.gitignore +++ b/.gitignore @@ -38,8 +38,7 @@ node_modules /target /src/instruments/src/EFB/web/ /src/systems/target/ -/src/systems/systems/*.png -.vscode/settings.json +/src/systems/a320_hydraulic_simulation_graphs/*.png !igniter.config.mjs .igniter /src/fdr2csv/*.exe diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 67f291e9d08..00000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'a320_systems'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=a320_systems" - ], - "filter": { - "name": "a320_systems", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'systems'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=systems" - ], - "filter": { - "name": "systems", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index a44dbe87605..77afa94ea79 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -92,9 +92,10 @@ impl PowerTransferUnitController for TestPowerTransferUnitController { fn main() { println!("Launching hyd simulation..."); + let path = "./src/systems/a320_hydraulic_simulation_graphs/"; - green_loop_edp_simulation(); - yellow_green_ptu_loop_simulation(); + green_loop_edp_simulation(path); + yellow_green_ptu_loop_simulation(path); } fn make_figure(h: &History) -> Figure { @@ -190,7 +191,7 @@ impl History { } //builds a graph using matplotlib python backend. PYTHON REQUIRED AS WELL AS MATPLOTLIB PACKAGE - pub fn show_matplotlib(&self, figure_title: &str) { + pub fn show_matplotlib(&self, figure_title: &str, path: &str) { let fig = make_figure(&self); use rustplotlib::backend::Matplotlib; @@ -200,14 +201,17 @@ impl History { fig.apply(&mut mpl).unwrap(); - let _result = mpl.savefig(figure_title); + 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() { +fn green_loop_edp_simulation(path : & str) { let green_loop_var_names = vec![ "Loop Pressure".to_string(), "Loop Volume".to_string(), @@ -338,12 +342,12 @@ fn green_loop_edp_simulation() { ); } - green_loop_history.show_matplotlib("green_loop_edp_simulation_press"); - edp1_history.show_matplotlib("green_loop_edp_simulation_EDP1 data"); - accu_green_history.show_matplotlib("green_loop_edp_simulation_Green Accum data"); + 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() { +fn yellow_green_ptu_loop_simulation(path : & str) { let loop_var_names = vec![ "GREEN Loop Pressure".to_string(), "YELLOW Loop Pressure".to_string(), @@ -592,11 +596,11 @@ fn yellow_green_ptu_loop_simulation() { } } - loop_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Loop_press"); - ptu_history.show_matplotlib("yellow_green_ptu_loop_simulation()_PTU"); + 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"); - accu_yellow_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Yellow_acc"); + 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 hydraulic_loop(loop_color: &str) -> HydraulicLoop { From 21904167ee2fe4cb2727682305f7a91f238c6366 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:08:44 +0200 Subject: [PATCH 095/122] Updated simvar.md pattern names --- docs/a320-simvars.md | 44 +++++++++---------- .../src/main.rs | 8 ++-- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index 1bbdf30e557..f0355d21850 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -104,29 +104,33 @@ - Bool - True if pedestal door video button is being held -- A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT +- A32NX_OVHD_HYD_{name}_PUMP_PB_HAS_FAULT - Bool - - True if engine 1 hyd pump fault - -- A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO - - Bool - - True if engine 1 hyd pump is on - -- A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT - - Bool - - True if engine 2 hyd pump fault + - True if engine {name} hyd pump fault + - {name} + - ENG_1 + - ENG_2 -- A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO +- 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_OVHD_HYD_EPUMPB_PB_HAS_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_OVHD_HYD_EPUMPB_PB_IS_AUTO +- 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_OVHD_HYD_PTU_PB_HAS_FAULT - Bool @@ -136,14 +140,6 @@ - Bool - True if PTU system on/auto -- A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT - - Bool - - True if yellow elec hyd pump fault - -- A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO - - Bool - - True if yellow elec hyd pump is on/auto - - A32NX_ENGMANSTART1_TOGGLE - Bool - True if manual engine 1 start on diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 77afa94ea79..95f796c1ea9 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -211,7 +211,7 @@ impl History { } //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s -fn green_loop_edp_simulation(path : & str) { +fn green_loop_edp_simulation(path: &str) { let green_loop_var_names = vec![ "Loop Pressure".to_string(), "Loop Volume".to_string(), @@ -342,12 +342,12 @@ fn green_loop_edp_simulation(path : & str) { ); } - green_loop_history.show_matplotlib("green_loop_edp_simulation_press" , &path); + 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) { +fn yellow_green_ptu_loop_simulation(path: &str) { let loop_var_names = vec![ "GREEN Loop Pressure".to_string(), "YELLOW Loop Pressure".to_string(), @@ -596,7 +596,7 @@ fn yellow_green_ptu_loop_simulation(path : & str) { } } - loop_history.show_matplotlib("yellow_green_ptu_loop_simulation()_Loop_press" , &path); + 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); From bb10956e80050cd456b12268781a0ccc606336ac Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:13:30 +0200 Subject: [PATCH 096/122] removed _HAS_FAULT from .FLT --- .../SimObjects/AirPlanes/FlyByWire_A320_NEO/approach.FLT | 5 ----- .../SimObjects/AirPlanes/FlyByWire_A320_NEO/apron.FLT | 5 ----- .../SimObjects/AirPlanes/FlyByWire_A320_NEO/cruise.FLT | 5 ----- .../SimObjects/AirPlanes/FlyByWire_A320_NEO/final.FLT | 5 ----- .../SimObjects/AirPlanes/FlyByWire_A320_NEO/runway.FLT | 5 ----- 5 files changed, 25 deletions(-) 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 84ac51085f8..c5772116424 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,17 +165,12 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 -A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_OVHD_HYD_PTU_PB_HAS_FAULT=0 A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 -A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 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 45a8f854e4e..1a9c48675bb 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,17 +170,12 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 -A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_OVHD_HYD_PTU_PB_HAS_FAULT=0 A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 -A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 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 ac8fe4d2da0..fe4370d0a47 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,17 +165,12 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 -A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_OVHD_HYD_PTU_PB_HAS_FAULT=0 A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 -A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 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 b4f4325e224..c9f4c8ffad6 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,17 +165,12 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 -A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_OVHD_HYD_PTU_PB_HAS_FAULT=0 A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 -A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=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 2767fa5d385..66c4a9ad8f5 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,17 +172,12 @@ A32NX_AIRCOND_RAMAIR_TOGGLE=0 A32NX_CALLS_EMERLOCK_TOGGLE=1 A32NX_CALLS_EMER_ON=0 A32NX_OVHD_COCKPITDOORVIDEO_TOGGLE=1 -A32NX_OVHD_HYD_ENG_1_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_1_PUMP_PB_IS_AUTO=1 -A32NX_OVHD_HYD_ENG_2_PUMP_PB_HAS_FAULT=0 A32NX_OVHD_HYD_ENG_2_PUMP_PB_IS_AUTO=1 A32NX_HYD_RATMANONLOCK_TOGGLE=0 -A32NX_OVHD_HYD_EPUMPB_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPB_PB_IS_AUTO=1 A32NX_HYD_ELECPUMPLOCK_TOGGLE=0 -A32NX_OVHD_HYD_PTU_PB_HAS_FAULT=0 A32NX_OVHD_HYD_PTU_PB_IS_AUTO=1 -A32NX_OVHD_HYD_EPUMPY_PB_HAS_FAULT=0 A32NX_OVHD_HYD_EPUMPY_PB_IS_AUTO=1 A32NX_ENGMANSTART1LOCK_TOGGLE=0 A32NX_ENGMANSTART2LOCK_TOGGLE=0 From cb12aa2b6d0a3361901a3de4d4eccaf93a0546cb Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:19:23 +0200 Subject: [PATCH 097/122] Speed up stress tests --- src/systems/a320_systems/src/hydraulic.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index c55281a4694..cdc3e09843f 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1874,7 +1874,7 @@ mod tests { + test_bed.get_brake_yellow_accumulator_fluid_volume(); // Now doing cycles of pressurisation on EDP and ePump - for _ in 1..51 { + for _ in 1..6 { test_bed = test_bed .start_eng2(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(50)); @@ -1947,7 +1947,7 @@ mod tests { let reservoir_level_after_priming = test_bed.get_green_reservoir_volume(); // Now doing cycles of pressurisation on EDP - for _ in 1..101 { + for _ in 1..6 { test_bed = test_bed .start_eng1(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(50)); @@ -2000,7 +2000,7 @@ mod tests { 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..51 { + for _ in 1..6 { test_bed = test_bed .start_eng1(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(50)); From ea21977283e2ad72c7b3d99b751565ee5c9cf111 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:24:34 +0200 Subject: [PATCH 098/122] Removed comment in fn interpolation --- src/systems/systems/src/shared/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index a8aa8ab9066..3f430aeba09 100644 --- a/src/systems/systems/src/shared/mod.rs +++ b/src/systems/systems/src/shared/mod.rs @@ -124,7 +124,6 @@ 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); - // The function also assumes xs are ordered from small to large. Consider adding a debug_assert! for that as well. if intermediate_x <= xs[0] { *ys.first().unwrap() From 8973270de7da81b43b94083e4e9c872257200f37 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 30 Apr 2021 18:28:52 +0200 Subject: [PATCH 099/122] Elec conditions added on brake gauges --- .../model/A320_NEO_INTERIOR.xml | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 736b230308a..83872a9cc33 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 @@ -3652,7 +3652,13 @@ Press_Arc_L 3 1 - (L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS, number) 1000 / + + (L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED, Bool) if{ + (L:A32NX_HYD_BRAKE_ALTN_LEFT_PRESS, number) 1000 / + } els{ + 0 + } + @@ -3660,7 +3666,13 @@ Press_Arc_R 3 1 - (L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS, number) 1000 / + + (L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED, Bool) if{ + (L:A32NX_HYD_BRAKE_ALTN_RIGHT_PRESS, number) 1000 / + } els{ + 0 + } + @@ -3668,7 +3680,13 @@ Accu_Press 1 1 - (L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS, number) 4000 / + + (L:A32NX_ELEC_DC_ESS_BUS_IS_POWERED, Bool) if{ + (L:A32NX_HYD_BRAKE_ALTN_ACC_PRESS, number) 4000 / + } els{ + 0 + } + From 3ae62aebd4d1920feb0aa4519306a5fae0868c36 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 2 May 2021 11:36:14 +0200 Subject: [PATCH 100/122] unit tests for controllers --- src/systems/a320_systems/src/hydraulic.rs | 323 +++++++++++++++++++++- src/systems/systems/src/hydraulic/mod.rs | 6 +- 2 files changed, 322 insertions(+), 7 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index cdc3e09843f..935574c5801 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -502,7 +502,7 @@ impl A320EngineDrivenPumpController { engine_number, engine_master_on_id: format!("GENERAL ENG STARTER ACTIVE:{}", engine_number), engine_master_on: false, - should_pressurise: false, + should_pressurise: true, } } @@ -814,7 +814,7 @@ impl A320HydraulicBrakingLogic { > 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 e + // Nominal braking from pedals is limited to 2538psi norm_brk.set_brake_limit_ena(true); norm_brk.set_brake_press_limit(Pressure::new::(2538.)); @@ -1119,7 +1119,10 @@ mod tests { mod a320_hydraulics { use super::*; use systems::simulation::{test::SimulationTestBed, Aircraft}; - use uom::si::{length::foot, ratio::percent, velocity::knot}; + use uom::si::{ + acceleration::foot_per_second_squared, length::foot, ratio::percent, + thermodynamic_temperature::degree_celsius, velocity::knot, + }; struct A320HydraulicsTestAircraft { engine_1: Engine, @@ -1303,7 +1306,7 @@ mod tests { ) } - fn get_brake_yellow_accumulator_fluid_volume(&mut self) -> Volume { + fn get_brake_yellow_accumulator_fluid_volume(&self) -> Volume { self.aircraft.get_yellow_brake_accumulator_fluid_volume() } @@ -1515,6 +1518,35 @@ mod tests { .write_f64("BRAKE RIGHT POSITION", position_percent.get::()); self } + + fn empty_brake_accumulator(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 test_bed() -> A320HydraulicsTestBed { @@ -2411,6 +2443,261 @@ mod tests { assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); } + #[test] + fn controller_blue_epump_test() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + overhead_panel.blue_epump_override_push_button.push_off(); + + let mut blue_epump_controller = A320BlueElectricPumpController::new(); + + blue_epump_controller.update(&overhead_panel); + assert!(!blue_epump_controller.should_pressurise()); + + blue_epump_controller.engine_1_master_on = true; + blue_epump_controller.engine_2_master_on = false; + blue_epump_controller.update(&overhead_panel); + assert!(blue_epump_controller.should_pressurise()); + + blue_epump_controller.engine_1_master_on = false; + blue_epump_controller.engine_2_master_on = true; + blue_epump_controller.update(&overhead_panel); + assert!(blue_epump_controller.should_pressurise()); + + blue_epump_controller.engine_1_master_on = true; + blue_epump_controller.engine_2_master_on = true; + blue_epump_controller.update(&overhead_panel); + assert!(blue_epump_controller.should_pressurise()); + + blue_epump_controller.engine_1_master_on = false; + blue_epump_controller.engine_2_master_on = false; + overhead_panel.blue_epump_override_push_button.push_on(); + blue_epump_controller.update(&overhead_panel); + assert!(blue_epump_controller.should_pressurise()); + + blue_epump_controller.engine_1_master_on = false; + blue_epump_controller.engine_2_master_on = false; + overhead_panel.blue_epump_override_push_button.push_off(); + blue_epump_controller.update(&overhead_panel); + assert!(!blue_epump_controller.should_pressurise()); + } + + fn context(delta_time: Duration) -> UpdateContext { + UpdateContext::new( + delta_time, + Velocity::new::(250.), + Length::new::(5000.), + ThermodynamicTemperature::new::(25.0), + true, + Acceleration::new::(0.), + ) + } + + #[test] + fn controller_yellow_epump_test() { + let mut fwd_door = Door::new(1); + let mut 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); + 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); + 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); + assert!(!yellow_epump_controller.should_pressurise()); + + fwd_door = moving_door(1); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door); + assert!(yellow_epump_controller.should_pressurise()); + 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); + assert!(!yellow_epump_controller.should_pressurise()); + + aft_door = moving_door(2); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door); + assert!(yellow_epump_controller.should_pressurise()); + 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); + assert!(!yellow_epump_controller.should_pressurise()); + } + + #[test] + fn controller_engine_driven_pump1_test() { + 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.update(&overhead_panel, &fire_overhead_panel); + assert!(edp1_controller.should_pressurise()); + + overhead_panel.edp1_push_button.push_off(); + edp1_controller.update(&overhead_panel, &fire_overhead_panel); + assert!(!edp1_controller.should_pressurise()); + + overhead_panel.edp1_push_button.push_auto(); + fire_overhead_panel.eng1_fire_pb.set(true); + edp1_controller.update(&overhead_panel, &fire_overhead_panel); + assert!(!edp1_controller.should_pressurise()); + } + + #[test] + fn controller_engine_driven_pump2_test() { + 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(1); + + edp2_controller.update(&overhead_panel, &fire_overhead_panel); + assert!(edp2_controller.should_pressurise()); + + overhead_panel.edp1_push_button.push_off(); + edp2_controller.update(&overhead_panel, &fire_overhead_panel); + assert!(!edp2_controller.should_pressurise()); + + overhead_panel.edp1_push_button.push_auto(); + fire_overhead_panel.eng1_fire_pb.set(true); + edp2_controller.update(&overhead_panel, &fire_overhead_panel); + assert!(!edp2_controller.should_pressurise()); + } + + #[test] + fn controller_ptu_on_off_cargo_door_test() { + 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_test() { + 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] + // 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_test() { + 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() + .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 writes_its_state() { let mut hyd_logic = A320Hydraulic::new(); @@ -2422,5 +2709,33 @@ mod tests { assert!(test_bed.contains_key("HYD_YELLOW_EDPUMP_LOW_PRESS")); assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); } + + 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 + } } } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 11fcbaed769..57e7bf23120 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1405,14 +1405,14 @@ mod tests { if time >= 30. && time < 30. + timestep { println!("ASSERT RAT OUT AND SPINING"); - assert!(blue_loop.loop_pressure >= Pressure::new::(2900.0)); + 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::(2500.0)); - assert!(rat.wind_turbine.rpm >= 5000.); + assert!(blue_loop.loop_pressure >= Pressure::new::(2000.0)); + assert!(rat.wind_turbine.rpm >= 4500.); } if time >= 70. && time < 70. + timestep { From 7e68c9180a164cc0dc4dceb79c2342aebee7393f Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 2 May 2021 12:58:02 +0200 Subject: [PATCH 101/122] more brake tests --- src/systems/a320_systems/src/hydraulic.rs | 223 ++++++++++++++++++---- 1 file changed, 185 insertions(+), 38 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 935574c5801..0012d479d0b 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1519,7 +1519,7 @@ mod tests { self } - fn empty_brake_accumulator(mut self) -> Self { + fn empty_brake_accumulator_using_park_brake(mut self) -> Self { self = self .set_park_brake(true) .run_waiting_for(Duration::from_secs(1)); @@ -1547,6 +1547,35 @@ mod tests { 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 { @@ -1601,16 +1630,16 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //Enabled on cold start + // Enabled on cold start assert!(test_bed.is_ptu_enabled()); - //Ptu push button disables PTU accordingly + // 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 + // Not all engines on or off should disable ptu if on ground and park brake on test_bed = test_bed .start_eng2(Ratio::new::(50.)) .run_one_tick(); @@ -1632,17 +1661,17 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //Enabled on cold start + // Enabled on cold start assert!(test_bed.is_ptu_enabled()); - //Ptu push button disables PTU accordingly + // 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 + ); // Should re enabled after 40s assert!(test_bed.is_ptu_enabled()); } @@ -1702,17 +1731,17 @@ mod tests { .set_cold_dark_inputs() .run_one_tick(); - //Enabled on cold start + // Enabled on cold start assert!(test_bed.is_ptu_enabled()); - //Yellow epump ON / Waiting 25s + // 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 + // 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::(3500.)); @@ -1725,13 +1754,13 @@ mod tests { assert!(test_bed.yellow_pressure() > Pressure::new::(2000.)); assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); - //Ptu push button disables PTU / green press should fall + // 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 + // 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()); @@ -1741,14 +1770,14 @@ mod tests { } #[test] - fn green_edp_buildup_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 + // Starting eng 1 test_bed = test_bed .start_eng1(Ratio::new::(50.)) .run_one_tick(); @@ -1756,8 +1785,10 @@ mod tests { // 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.)); //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.)); @@ -1787,7 +1818,7 @@ mod tests { } #[test] - fn edp_deactivation_test() { + fn edp_deactivation() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -1829,7 +1860,7 @@ mod tests { } #[test] - fn yellow_edp_buildup_test() { + fn yellow_edp_buildup() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -1844,7 +1875,9 @@ mod tests { 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::(500.)); //Blue is auto run + + //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.)); @@ -1875,13 +1908,14 @@ mod tests { #[test] // Checks numerical stability of reservoir level: level should remain after multiple pressure cycles - fn yellow_loop_reservoir_coherency_test() { + 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) - .set_park_brake(false) // Park brake off to not use fluid in brakes + // 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 @@ -1952,7 +1986,7 @@ mod tests { #[test] // Checks numerical stability of reservoir level: level should remain after multiple pressure cycles - fn green_loop_reservoir_coherency_test() { + fn green_loop_reservoir_coherency() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2006,7 +2040,7 @@ mod tests { #[test] // Checks numerical stability of reservoir level: level should remain after multiple pressure cycles - fn blue_loop_reservoir_coherency_test() { + fn blue_loop_reservoir_coherency() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2075,14 +2109,14 @@ mod tests { } #[test] - fn yellow_green_edp_firevalve_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 + // PTU would mess up the test test_bed = test_bed.set_ptu_state(false).run_one_tick(); assert!(!test_bed.is_ptu_enabled()); @@ -2138,7 +2172,7 @@ mod tests { } #[test] - fn yellow_brake_accumulator_test() { + fn yellow_brake_accumulator() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2216,7 +2250,7 @@ mod tests { } #[test] - fn norm_brake_vs_altn_brake_test() { + fn norm_brake_vs_altn_brake() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2284,7 +2318,7 @@ mod tests { } #[test] - fn check_brake_inversion_test() { + fn no_brake_inversion() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2345,7 +2379,7 @@ mod tests { } #[test] - fn check_auto_brake_at_gear_retraction_test() { + fn auto_brake_at_gear_retraction() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2397,7 +2431,87 @@ mod tests { } #[test] - fn check_brakes_inactive_in_flight_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) + ); + + // // 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 brakes_inactive_in_flight() { let mut test_bed = test_bed_with() .set_cold_dark_inputs() .in_flight() @@ -2427,6 +2541,15 @@ mod tests { 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 @@ -2435,7 +2558,7 @@ mod tests { .set_gear_down() .run_waiting_for(Duration::from_secs(1)); - // Brakes should work normally + // 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.)); @@ -2444,7 +2567,31 @@ mod tests { } #[test] - fn controller_blue_epump_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] + fn controller_blue_epump() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); overhead_panel.blue_epump_override_push_button.push_off(); @@ -2493,7 +2640,7 @@ mod tests { } #[test] - fn controller_yellow_epump_test() { + fn controller_yellow_epump() { let mut fwd_door = Door::new(1); let mut aft_door = Door::new(2); let context = context(Duration::from_millis(100)); @@ -2532,7 +2679,7 @@ mod tests { } #[test] - fn controller_engine_driven_pump1_test() { + fn controller_engine_driven_pump1() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); let mut fire_overhead_panel = A320EngineFireOverheadPanel::new(); overhead_panel.edp1_push_button.push_auto(); @@ -2554,7 +2701,7 @@ mod tests { } #[test] - fn controller_engine_driven_pump2_test() { + fn controller_engine_driven_pump2() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); let mut fire_overhead_panel = A320EngineFireOverheadPanel::new(); overhead_panel.edp2_push_button.push_auto(); @@ -2576,7 +2723,7 @@ mod tests { } #[test] - fn controller_ptu_on_off_cargo_door_test() { + fn controller_ptu_on_off_cargo_door() { let tug = PushbackTug::new(); let context = context(Duration::from_millis(100)); @@ -2622,7 +2769,7 @@ mod tests { } #[test] - fn controller_ptu_tug_test() { + fn controller_ptu_tug() { let fwd_door = Door::new(1); let aft_door = Door::new(2); let mut tug = PushbackTug::new(); @@ -2656,7 +2803,7 @@ mod tests { #[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_test() { + fn brake_logic_green_backup_emergency() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2687,7 +2834,7 @@ mod tests { // With no more fluid in yellow accumulator, green should work as emergency test_bed = test_bed - .empty_brake_accumulator() + .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)); From cb7dd32f81dcb1544f470952d14c8ccf7a3cbee4 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 2 May 2021 13:08:38 +0200 Subject: [PATCH 102/122] remove comments --- src/systems/a320_systems/src/hydraulic.rs | 37 ----------------------- 1 file changed, 37 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 0012d479d0b..02572a2a06e 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -2471,43 +2471,6 @@ mod tests { assert!( test_bed.get_brake_yellow_accumulator_fluid_volume() <= Volume::new::(0.01) ); - - // // 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] From 98a8fd8fda284ab69f087b18ea547791130d886f Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 2 May 2021 20:01:26 +0200 Subject: [PATCH 103/122] split blue controller tests --- .../src/main.rs | 183 ++++++++++++++++++ src/systems/a320_systems/src/hydraulic.rs | 21 +- src/systems/systems/src/hydraulic/mod.rs | 2 +- 3 files changed, 204 insertions(+), 2 deletions(-) diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 95f796c1ea9..155dc91b032 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -80,6 +80,12 @@ impl TestPowerTransferUnitController { } } + fn commanding_enabled() -> Self { + Self { + should_enable: true, + } + } + fn command_enable(&mut self) { self.should_enable = true; } @@ -96,6 +102,7 @@ fn main() { green_loop_edp_simulation(path); yellow_green_ptu_loop_simulation(path); + yellow_epump_plus_edp2_with_ptu(path); } fn make_figure(h: &History) -> Figure { @@ -603,6 +610,182 @@ fn yellow_green_ptu_loop_simulation(path: &str) { 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 mut engine2 = engine(Ratio::new::(75.0)); + + 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.get_pressure().get::(), + yellow_loop.get_pressure().get::(), + green_loop.get_reservoir_volume().get::(), + yellow_loop.get_reservoir_volume().get::(), + green_loop.get_current_delta_vol().get::(), + yellow_loop.get_current_delta_vol().get::(), + ], + ); + ptu_history.init( + 0.0, + vec![ + ptu.get_flow().get::(), + green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), + ptu.get_is_active_left_to_right() as i8 as f64, + ptu.get_is_active_right_to_left() as i8 as f64, + ], + ); + accu_green_history.init( + 0.0, + vec![ + green_loop.get_pressure().get::(), + green_loop.get_accumulator_gas_pressure().get::(), + green_loop.get_accumulator_fluid_volume().get::(), + green_loop.get_accumulator_gas_volume().get::(), + ], + ); + accu_yellow_history.init( + 0.0, + vec![ + yellow_loop.get_pressure().get::(), + yellow_loop.get_accumulator_gas_pressure().get::(), + yellow_loop.get_accumulator_fluid_volume().get::(), + yellow_loop.get_accumulator_gas_volume().get::(), + ], + ); + + engine2.corrected_n2 = Ratio::new::(100.0); + 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.get_pressure().get::()); + } + ptu.update(&green_loop, &yellow_loop, &ptu_controller); + edp2.update(&context, &yellow_loop, &engine2, &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.get_pressure().get::(), + yellow_loop.get_pressure().get::(), + green_loop.get_reservoir_volume().get::(), + yellow_loop.get_reservoir_volume().get::(), + green_loop.get_current_delta_vol().get::(), + yellow_loop.get_current_delta_vol().get::(), + ], + ); + ptu_history.update( + context.delta_as_secs_f64(), + vec![ + ptu.get_flow().get::(), + green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), + ptu.get_is_active_left_to_right() as i8 as f64, + ptu.get_is_active_right_to_left() as i8 as f64, + ], + ); + + accu_green_history.update( + context.delta_as_secs_f64(), + vec![ + green_loop.get_pressure().get::(), + green_loop.get_accumulator_gas_pressure().get::(), + green_loop.get_accumulator_fluid_volume().get::(), + green_loop.get_accumulator_gas_volume().get::(), + ], + ); + accu_yellow_history.update( + context.delta_as_secs_f64(), + vec![ + yellow_loop.get_pressure().get::(), + yellow_loop.get_accumulator_gas_pressure().get::(), + yellow_loop.get_accumulator_fluid_volume().get::(), + yellow_loop.get_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( diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 02572a2a06e..07d382ad462 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -2554,7 +2554,7 @@ mod tests { } #[test] - fn controller_blue_epump() { + fn controller_blue_epump_split_master_switch() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); overhead_panel.blue_epump_override_push_button.push_off(); @@ -2572,12 +2572,31 @@ mod tests { blue_epump_controller.engine_2_master_on = true; blue_epump_controller.update(&overhead_panel); assert!(blue_epump_controller.should_pressurise()); + } + + #[test] + fn controller_blue_epump_on_off_master_switch() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + overhead_panel.blue_epump_override_push_button.push_off(); + + let mut blue_epump_controller = A320BlueElectricPumpController::new(); blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = true; blue_epump_controller.update(&overhead_panel); assert!(blue_epump_controller.should_pressurise()); + blue_epump_controller.engine_1_master_on = false; + blue_epump_controller.engine_2_master_on = false; + blue_epump_controller.update(&overhead_panel); + assert!(!blue_epump_controller.should_pressurise()); + } + + #[test] + fn controller_blue_epump_override() { + let mut overhead_panel = A320HydraulicOverheadPanel::new(); + let mut blue_epump_controller = A320BlueElectricPumpController::new(); + blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_on(); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 57e7bf23120..e8652327d8a 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -835,7 +835,7 @@ impl EngineDrivenPump { ]; const DISPLACEMENT_MAP: [f64; 9] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.0, 0.0]; - const DISPLACEMENT_DYNAMICS: f64 = 0.3; //0.1 == 90% filtering on max displacement transient + const DISPLACEMENT_DYNAMICS: f64 = 0.9; //0.1 == 90% filtering on max displacement transient pub fn new(id: &str) -> Self { Self { From a82c29d86fed582b631b01c650f67e1746c5eb04 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Sun, 2 May 2021 22:19:50 +0200 Subject: [PATCH 104/122] test update and corrected max edp displacement --- .../src/main.rs | 2 +- src/systems/a320_systems/src/hydraulic.rs | 29 +++++++++++++++++-- src/systems/systems/src/hydraulic/mod.rs | 4 +-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 155dc91b032..9f6987fe68c 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -652,7 +652,7 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { let mut edp2 = engine_driven_pump(); let mut edp2_controller = TestPumpController::commanding_depressurise(); - let mut engine2 = engine(Ratio::new::(75.0)); + let mut engine2 = engine(Ratio::new::(100.0)); let mut green_loop = hydraulic_loop("GREEN"); diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 07d382ad462..969139772ed 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1744,7 +1744,7 @@ mod tests { // 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::(3500.)); + assert!(test_bed.green_pressure() < Pressure::new::(3100.)); assert!(!test_bed.is_blue_pressurised()); assert!(test_bed.blue_pressure() < Pressure::new::(50.)); @@ -1752,7 +1752,7 @@ mod tests { assert!(test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() > Pressure::new::(2000.)); - assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); + assert!(test_bed.yellow_pressure() < Pressure::new::(3100.)); // Ptu push button disables PTU / green press should fall test_bed = test_bed @@ -1769,6 +1769,29 @@ mod tests { 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() @@ -1945,7 +1968,7 @@ mod tests { .start_eng2(Ratio::new::(50.)) .run_waiting_for(Duration::from_secs(50)); - assert!(test_bed.yellow_pressure() < Pressure::new::(3500.)); + 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(); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index e8652327d8a..9b4b5d5c8a0 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -831,9 +831,9 @@ impl EngineDrivenPump { const PUMP_N2_GEAR_RATIO: f64 = 0.211; const DISPLACEMENT_BREAKPTS: [f64; 9] = [ - 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3050.0, 3500.0, + 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.4, 2.0, 0.0, 0.0]; + const DISPLACEMENT_MAP: [f64; 9] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.9, 0.0, 0.0]; const DISPLACEMENT_DYNAMICS: f64 = 0.9; //0.1 == 90% filtering on max displacement transient From d9641f02279e8f8f2ef44465f40ef64b52b12268 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 3 May 2021 10:36:04 +0200 Subject: [PATCH 105/122] Split some tests --- src/systems/a320_systems/src/hydraulic.rs | 193 ++++++++++++++-------- 1 file changed, 120 insertions(+), 73 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 969139772ed..9aba129a80c 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -502,7 +502,7 @@ impl A320EngineDrivenPumpController { engine_number, engine_master_on_id: format!("GENERAL ENG STARTER ACTIVE:{}", engine_number), engine_master_on: false, - should_pressurise: true, + should_pressurise: false, } } @@ -2576,6 +2576,50 @@ mod tests { 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_split_master_switch() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); @@ -2633,21 +2677,10 @@ mod tests { assert!(!blue_epump_controller.should_pressurise()); } - fn context(delta_time: Duration) -> UpdateContext { - UpdateContext::new( - delta_time, - Velocity::new::(250.), - Length::new::(5000.), - ThermodynamicTemperature::new::(25.0), - true, - Acceleration::new::(0.), - ) - } - #[test] - fn controller_yellow_epump() { - let mut fwd_door = Door::new(1); - let mut aft_door = Door::new(2); + 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(); @@ -2665,32 +2698,45 @@ mod tests { overhead_panel.yellow_epump_push_button.push_auto(); yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door); 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()); - fwd_door = moving_door(1); + let aft_door = non_moving_door(2); + let fwd_door = moving_door(1); yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door); assert!(yellow_epump_controller.should_pressurise()); - fwd_door = non_moving_door(1); + 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); assert!(!yellow_epump_controller.should_pressurise()); - aft_door = moving_door(2); + let aft_door = moving_door(2); yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door); assert!(yellow_epump_controller.should_pressurise()); - aft_door = non_moving_door(2); + 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); assert!(!yellow_epump_controller.should_pressurise()); } #[test] - fn controller_engine_driven_pump1() { + fn controller_engine_driven_pump1_overhead_button_logic_with_eng_on() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); - let mut fire_overhead_panel = A320EngineFireOverheadPanel::new(); + let 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); assert!(edp1_controller.should_pressurise()); @@ -2700,29 +2746,63 @@ mod tests { assert!(!edp1_controller.should_pressurise()); overhead_panel.edp1_push_button.push_auto(); + edp1_controller.update(&overhead_panel, &fire_overhead_panel); + 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); + assert!(edp1_controller.should_pressurise()); + fire_overhead_panel.eng1_fire_pb.set(true); edp1_controller.update(&overhead_panel, &fire_overhead_panel); assert!(!edp1_controller.should_pressurise()); } #[test] - fn controller_engine_driven_pump2() { + fn controller_engine_driven_pump2_overhead_button_logic_with_eng_on() { let mut overhead_panel = A320HydraulicOverheadPanel::new(); - let mut fire_overhead_panel = A320EngineFireOverheadPanel::new(); + let 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(1); + let mut edp2_controller = A320EngineDrivenPumpController::new(2); + edp2_controller.engine_master_on = true; edp2_controller.update(&overhead_panel, &fire_overhead_panel); assert!(edp2_controller.should_pressurise()); - overhead_panel.edp1_push_button.push_off(); + overhead_panel.edp2_push_button.push_off(); edp2_controller.update(&overhead_panel, &fire_overhead_panel); assert!(!edp2_controller.should_pressurise()); - overhead_panel.edp1_push_button.push_auto(); - fire_overhead_panel.eng1_fire_pb.set(true); + overhead_panel.edp2_push_button.push_auto(); + edp2_controller.update(&overhead_panel, &fire_overhead_panel); + 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); + assert!(edp2_controller.should_pressurise()); + + fire_overhead_panel.eng2_fire_pb.set(true); edp2_controller.update(&overhead_panel, &fire_overhead_panel); assert!(!edp2_controller.should_pressurise()); } @@ -2806,50 +2886,6 @@ mod tests { assert!(ptu_controller.should_enable()); } - #[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 writes_its_state() { let mut hyd_logic = A320Hydraulic::new(); @@ -2862,6 +2898,17 @@ mod tests { assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); } + 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; From b1118dc09f4cf9c966b83eb98c5559e17608241d Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Mon, 3 May 2021 20:38:35 +0200 Subject: [PATCH 106/122] More tests, refactored low pump faults --- src/systems/a320_systems/src/hydraulic.rs | 603 +++++++++++++++++++--- src/systems/systems/src/hydraulic/mod.rs | 2 +- 2 files changed, 526 insertions(+), 79 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 9aba129a80c..488ac6a6ba8 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,5 +1,7 @@ use std::time::Duration; -use uom::si::{f64::*, pressure::pascal, pressure::psi, velocity::knot, volume::gallon}; +use uom::si::{ + f64::*, pressure::pascal, pressure::psi, ratio::percent, velocity::knot, volume::gallon, +}; use systems::hydraulic::{ ElectricPump, EngineDrivenPump, HydFluid, HydraulicLoop, HydraulicLoopController, @@ -55,8 +57,10 @@ pub(super) struct A320Hydraulic { lag_time_accumulator: Duration, } impl A320Hydraulic { - const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1450.0; - const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 1750.0; + const MIN_PRESS_BLUE_PRESSURISED_LO_HYST: f64 = 1450.0; + const MIN_PRESS_BLUE_PRESSURISED_HI_HYST: f64 = 1750.0; + const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1740.0; + const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 2200.0; const HYDRAULIC_SIM_TIME_STEP: u64 = 100; // Refresh rate of hydraulic simulation in ms const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; // Refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update @@ -75,8 +79,8 @@ impl A320Hydraulic { Volume::new::(1.56), HydFluid::new(Pressure::new::(1450000000.0)), false, - Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), - Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), + Pressure::new::(Self::MIN_PRESS_BLUE_PRESSURISED_LO_HYST), + Pressure::new::(Self::MIN_PRESS_BLUE_PRESSURISED_HI_HYST), ), blue_loop_controller: A320HydraulicLoopController::new(None), green_loop: HydraulicLoop::new( @@ -219,31 +223,23 @@ impl A320Hydraulic { } } - fn green_edp_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_1_controller.should_pressurise() - && !self.green_loop.is_pressurised() + fn green_edp_has_low_press_fault(&self) -> bool { + self.engine_driven_pump_1_controller + .has_pressure_low_fault() } - fn yellow_epump_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.yellow_electric_pump_controller.should_pressurise() - && !self.yellow_loop.is_pressurised() + fn yellow_epump_has_low_press_fault(&self) -> bool { + self.yellow_electric_pump_controller + .has_pressure_low_fault() } - fn yellow_edp_has_fault(&self) -> bool { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.engine_driven_pump_2_controller.should_pressurise() - && !self.yellow_loop.is_pressurised() + 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 { - // TODO wrong logic: can fake it using pump flow == 0 until we implement check valve sections in each hyd loop - // at current state, PTU activation is able to clear a pump fault by rising pressure, which is wrong - self.blue_electric_pump_controller.should_pressurise() && !self.blue_loop.is_pressurised() + self.blue_electric_pump_controller.has_pressure_low_fault() } #[cfg(test)] @@ -347,8 +343,13 @@ impl A320Hydraulic { &self.power_transfer_unit_controller, ); - self.engine_driven_pump_1_controller - .update(overhead_panel, engine_fire_overhead); + self.engine_driven_pump_1_controller.update( + overhead_panel, + engine_fire_overhead, + engine1.corrected_n2(), + self.green_loop.is_pressurised(), + ); + self.engine_driven_pump_1.update( context, &self.green_loop, @@ -356,8 +357,13 @@ impl A320Hydraulic { &self.engine_driven_pump_1_controller, ); - self.engine_driven_pump_2_controller - .update(overhead_panel, engine_fire_overhead); + self.engine_driven_pump_2_controller.update( + overhead_panel, + engine_fire_overhead, + engine2.corrected_n2(), + self.yellow_loop.is_pressurised(), + ); + self.engine_driven_pump_2.update( context, &self.yellow_loop, @@ -365,7 +371,8 @@ impl A320Hydraulic { &self.engine_driven_pump_2_controller, ); - self.blue_electric_pump_controller.update(overhead_panel); + self.blue_electric_pump_controller + .update(overhead_panel, self.blue_loop.is_pressurised()); self.blue_electric_pump.update( context, &self.blue_loop, @@ -377,6 +384,7 @@ impl A320Hydraulic { overhead_panel, &self.forward_cargo_door, &self.aft_cargo_door, + self.yellow_loop.is_pressurised(), ); self.yellow_electric_pump.update( context, @@ -434,6 +442,7 @@ impl SimulationElement for A320Hydraulic { 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); @@ -456,13 +465,6 @@ impl SimulationElement for A320Hydraulic { visitor.visit(self); } - - fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.green_edp_has_fault()); - writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.blue_epump_has_fault()); - writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.yellow_edp_has_fault()); - writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.yellow_epump_has_fault()); - } } struct A320HydraulicLoopController { @@ -495,6 +497,7 @@ struct A320EngineDrivenPumpController { engine_master_on_id: String, engine_master_on: bool, should_pressurise: bool, + has_pressure_low_fault: bool, } impl A320EngineDrivenPumpController { fn new(engine_number: usize) -> Self { @@ -502,17 +505,24 @@ impl A320EngineDrivenPumpController { engine_number, engine_master_on_id: format!("GENERAL ENG STARTER ACTIVE:{}", engine_number), engine_master_on: false, - should_pressurise: false, + should_pressurise: true, + has_pressure_low_fault: false, } } + fn update_low_pressure_fault(&mut self, engine_n2: Ratio, pressure_switch_state: bool) { + self.has_pressure_low_fault = + self.should_pressurise() && (!pressure_switch_state || engine_n2.get::() < 5.) + } + fn update( &mut self, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, + engine_n2: Ratio, + pressure_switch_state: bool, ) { if overhead_panel.edp_push_button_is_auto(self.engine_number) - && self.engine_master_on && !engine_fire_overhead.fire_push_button_is_released(self.engine_number) { self.should_pressurise = true; @@ -521,6 +531,12 @@ impl A320EngineDrivenPumpController { { self.should_pressurise = false; } + + self.update_low_pressure_fault(engine_n2, pressure_switch_state); + } + + fn has_pressure_low_fault(&self) -> bool { + self.has_pressure_low_fault } } impl PumpController for A320EngineDrivenPumpController { @@ -532,10 +548,21 @@ impl SimulationElement for A320EngineDrivenPumpController { fn read(&mut self, state: &mut SimulatorReader) { self.engine_master_on = state.read_bool(&self.engine_master_on_id); } + + fn write(&self, writer: &mut SimulatorWriter) { + if self.engine_number == 1 { + writer.write_bool("HYD_GREEN_EDPUMP_LOW_PRESS", self.has_pressure_low_fault()); + } else if self.engine_number == 2 { + writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.has_pressure_low_fault()); + } else { + panic!("The A320 only supports two engines."); + } + } } struct A320BlueElectricPumpController { should_pressurise: bool, + has_pressure_low_fault: bool, engine_1_master_on: bool, engine_2_master_on: bool, } @@ -543,12 +570,13 @@ impl A320BlueElectricPumpController { fn new() -> Self { Self { should_pressurise: false, + has_pressure_low_fault: false, engine_1_master_on: false, engine_2_master_on: false, } } - fn update(&mut self, overhead_panel: &A320HydraulicOverheadPanel) { + fn update(&mut self, overhead_panel: &A320HydraulicOverheadPanel, pressure_switch_state: bool) { if overhead_panel.blue_epump_push_button.is_auto() { if self.engine_1_master_on || self.engine_2_master_on @@ -561,19 +589,36 @@ impl A320BlueElectricPumpController { } else if overhead_panel.blue_epump_push_button_is_off() { self.should_pressurise = false; } + + self.update_low_pressure_fault(pressure_switch_state); + } + + fn update_low_pressure_fault(&mut self, pressure_switch_state: bool) { + self.has_pressure_low_fault = self.should_pressurise() && !pressure_switch_state + } + + 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.engine_1_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:1"); self.engine_2_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:2"); } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.has_pressure_low_fault()); + } } + impl Default for A320BlueElectricPumpController { fn default() -> Self { Self::new() @@ -582,6 +627,7 @@ impl Default for A320BlueElectricPumpController { struct A320YellowElectricPumpController { should_pressurise: bool, + has_pressure_low_fault: bool, should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate, } impl A320YellowElectricPumpController { @@ -591,6 +637,7 @@ impl A320YellowElectricPumpController { fn new() -> Self { Self { should_pressurise: false, + has_pressure_low_fault: false, should_activate_yellow_pump_for_cargo_door_operation: DelayedFalseLogicGate::new( Self::DURATION_OF_YELLOW_PUMP_ACTIVATION_AFTER_CARGO_DOOR_OPERATION, ), @@ -603,6 +650,7 @@ impl A320YellowElectricPumpController { 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( @@ -614,6 +662,16 @@ impl A320YellowElectricPumpController { || self .should_activate_yellow_pump_for_cargo_door_operation .output(); + + self.update_low_pressure_fault(pressure_switch_state); + } + + fn update_low_pressure_fault(&mut self, pressure_switch_state: bool) { + self.has_pressure_low_fault = self.should_pressurise() && !pressure_switch_state + } + + fn has_pressure_low_fault(&self) -> bool { + self.has_pressure_low_fault } #[cfg(test)] @@ -627,6 +685,11 @@ impl PumpController for A320YellowElectricPumpController { self.should_pressurise } } +impl SimulationElement for A320YellowElectricPumpController { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.has_pressure_low_fault()); + } +} impl Default for A320YellowElectricPumpController { fn default() -> Self { Self::new() @@ -1028,12 +1091,14 @@ impl A320HydraulicOverheadPanel { } pub(super) fn update(&mut self, hyd: &A320Hydraulic) { - self.edp1_push_button.set_fault(hyd.green_edp_has_fault()); - self.edp2_push_button.set_fault(hyd.yellow_edp_has_fault()); + 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_fault()); + .set_fault(hyd.yellow_epump_has_low_press_fault()); } fn yellow_epump_push_button_is_auto(&self) -> bool { @@ -1144,6 +1209,18 @@ mod tests { } } + 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.get_acc_fluid_volume() } @@ -1231,6 +1308,14 @@ mod tests { 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() } @@ -1263,6 +1348,26 @@ mod tests { Volume::new::(self.simulation_test_bed.read_f64("HYD_YELLOW_RESERVOIR")) } + fn is_green_edp_fault_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_GREEN_EDPUMP_LOW_PRESS") + } + + fn is_yellow_edp_fault_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_YELLOW_EDPUMP_LOW_PRESS") + } + + fn is_yellow_epump_fault_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_YELLOW_EPUMP_LOW_PRESS") + } + + fn is_blue_epump_fault_low(&mut self) -> bool { + self.simulation_test_bed + .read_bool("HYD_BLUE_EPUMP_LOW_PRESS") + } + fn get_brake_left_yellow_pressure(&mut self) -> Pressure { Pressure::new::( self.simulation_test_bed @@ -1416,6 +1521,14 @@ mod tests { self } + fn stopping_eng1(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:1", false); + self.aircraft.set_engine_1_n2(Ratio::new::(25.)); + + self + } + fn stop_eng2(mut self) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:2", false); @@ -1424,6 +1537,14 @@ mod tests { self } + fn stopping_eng2(mut self) -> Self { + self.simulation_test_bed + .write_bool("GENERAL ENG STARTER ACTIVE:2", false); + self.aircraft.set_engine_2_n2(Ratio::new::(25.)); + + self + } + fn set_park_brake(mut self, is_set: bool) -> Self { self.simulation_test_bed .write_bool("BRAKE PARKING INDICATOR", is_set); @@ -1840,6 +1961,294 @@ mod tests { assert!(test_bed.yellow_pressure() < Pressure::new::(50.)); } + #[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_fault_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_fault_low()); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .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_fault_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_fault_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_fault_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_fault_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_fault_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_fault_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_fault_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_fault_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_fault_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_fault_low()); + + // Waiting for 5s pressure should be at 3000 psi + test_bed = test_bed + .start_eng2(Ratio::new::(50.)) + .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_fault_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_fault_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_fault_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_fault_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_fault_low()); + + // Waiting for 5s pressure should be at 3000 psi in EDP section + test_bed = test_bed + .start_eng2(Ratio::new::(50.)) + .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_fault_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::(60.)) + .run_one_tick(); + + // EDP should be LOW pressure state + assert!(test_bed.is_green_edp_fault_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_fault_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_fault_low()); + + // Waiting for 5s pressure should be at 3000 psi in EDP section + test_bed = test_bed + .start_eng1(Ratio::new::(50.)) + .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_fault_low()); + } + + #[test] + fn yellow_epump_fault_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_fault_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_fault_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_fault_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_fault_low()); + } + + #[test] + fn blue_epump_fault_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_fault_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_fault_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_fault_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_fault_low()); + } + #[test] fn edp_deactivation() { let mut test_bed = test_bed_with() @@ -1899,7 +2308,7 @@ mod tests { assert!(test_bed.green_pressure() < Pressure::new::(50.)); assert!(!test_bed.is_blue_pressurised()); - //Blue is auto run + // 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.)); @@ -2627,17 +3036,17 @@ mod tests { let mut blue_epump_controller = A320BlueElectricPumpController::new(); - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, true); assert!(!blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = false; - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, true); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = true; - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, true); assert!(blue_epump_controller.should_pressurise()); } @@ -2650,12 +3059,12 @@ mod tests { blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = true; - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, true); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, false); assert!(!blue_epump_controller.should_pressurise()); } @@ -2667,13 +3076,13 @@ mod tests { blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_on(); - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, true); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_off(); - blue_epump_controller.update(&overhead_panel); + blue_epump_controller.update(&overhead_panel, false); assert!(!blue_epump_controller.should_pressurise()); } @@ -2688,15 +3097,15 @@ mod tests { 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); + 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); + 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); + yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door, true); assert!(!yellow_epump_controller.should_pressurise()); } @@ -2713,19 +3122,19 @@ mod tests { let aft_door = non_moving_door(2); let fwd_door = moving_door(1); - yellow_epump_controller.update(&context, &overhead_panel, &fwd_door, &aft_door); + 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); + 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); + 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); + 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()); } @@ -2738,15 +3147,30 @@ mod tests { let mut edp1_controller = A320EngineDrivenPumpController::new(1); edp1_controller.engine_master_on = true; - edp1_controller.update(&overhead_panel, &fire_overhead_panel); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(edp1_controller.should_pressurise()); overhead_panel.edp1_push_button.push_off(); - edp1_controller.update(&overhead_panel, &fire_overhead_panel); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(!edp1_controller.should_pressurise()); overhead_panel.edp1_push_button.push_auto(); - edp1_controller.update(&overhead_panel, &fire_overhead_panel); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(edp1_controller.should_pressurise()); } @@ -2760,11 +3184,21 @@ mod tests { let mut edp1_controller = A320EngineDrivenPumpController::new(1); edp1_controller.engine_master_on = true; - edp1_controller.update(&overhead_panel, &fire_overhead_panel); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(edp1_controller.should_pressurise()); fire_overhead_panel.eng1_fire_pb.set(true); - edp1_controller.update(&overhead_panel, &fire_overhead_panel); + edp1_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(!edp1_controller.should_pressurise()); } @@ -2777,15 +3211,30 @@ mod tests { let mut edp2_controller = A320EngineDrivenPumpController::new(2); edp2_controller.engine_master_on = true; - edp2_controller.update(&overhead_panel, &fire_overhead_panel); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(edp2_controller.should_pressurise()); overhead_panel.edp2_push_button.push_off(); - edp2_controller.update(&overhead_panel, &fire_overhead_panel); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(!edp2_controller.should_pressurise()); overhead_panel.edp2_push_button.push_auto(); - edp2_controller.update(&overhead_panel, &fire_overhead_panel); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(edp2_controller.should_pressurise()); } @@ -2799,11 +3248,21 @@ mod tests { let mut edp2_controller = A320EngineDrivenPumpController::new(2); edp2_controller.engine_master_on = true; - edp2_controller.update(&overhead_panel, &fire_overhead_panel); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(edp2_controller.should_pressurise()); fire_overhead_panel.eng2_fire_pb.set(true); - edp2_controller.update(&overhead_panel, &fire_overhead_panel); + edp2_controller.update( + &overhead_panel, + &fire_overhead_panel, + Ratio::new::(50.), + true, + ); assert!(!edp2_controller.should_pressurise()); } @@ -2886,18 +3345,6 @@ mod tests { assert!(ptu_controller.should_enable()); } - #[test] - fn writes_its_state() { - let mut hyd_logic = A320Hydraulic::new(); - let mut test_bed = SimulationTestBed::new(); - test_bed.run_without_update(&mut hyd_logic); - - assert!(test_bed.contains_key("HYD_GREEN_EDPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("HYD_BLUE_EPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("HYD_YELLOW_EDPUMP_LOW_PRESS")); - assert!(test_bed.contains_key("HYD_YELLOW_EPUMP_LOW_PRESS")); - } - fn context(delta_time: Duration) -> UpdateContext { UpdateContext::new( delta_time, diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 9b4b5d5c8a0..ef0be3109d6 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -835,7 +835,7 @@ impl EngineDrivenPump { ]; const DISPLACEMENT_MAP: [f64; 9] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.9, 0.0, 0.0]; - const DISPLACEMENT_DYNAMICS: f64 = 0.9; //0.1 == 90% filtering on max displacement transient + const DISPLACEMENT_DYNAMICS: f64 = 0.95; //0.1 == 90% filtering on max displacement transient pub fn new(id: &str) -> Self { Self { From 09455b14c0ee1481cc885f1b4a9bee2c66145693 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 4 May 2021 10:16:46 +0200 Subject: [PATCH 107/122] updated fault pressure mechanism --- src/systems/a320_systems/src/hydraulic.rs | 308 +++++++++++++++++----- 1 file changed, 249 insertions(+), 59 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 488ac6a6ba8..c233f01c19f 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -371,8 +371,12 @@ impl A320Hydraulic { &self.engine_driven_pump_2_controller, ); - self.blue_electric_pump_controller - .update(overhead_panel, self.blue_loop.is_pressurised()); + self.blue_electric_pump_controller.update( + overhead_panel, + self.blue_loop.is_pressurised(), + engine1.corrected_n2(), + engine2.corrected_n2(), + ); self.blue_electric_pump.update( context, &self.blue_loop, @@ -496,8 +500,10 @@ 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 { fn new(engine_number: usize) -> Self { @@ -505,14 +511,27 @@ impl A320EngineDrivenPumpController { 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_fault(&mut self, engine_n2: Ratio, pressure_switch_state: bool) { + fn update_low_pressure_state(&mut self, engine_n2: Ratio, 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.; + + // Faking low engine oil pressure using N2 slow level. TODO Get real oil pressure (treshold is 18psi) + let faked_is_engine_low_oil_pressure = engine_n2.get::() < 25.; + + // 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.should_pressurise() && (!pressure_switch_state || engine_n2.get::() < 5.) + self.is_pressure_low && (!faked_is_engine_low_oil_pressure || !self.weight_on_wheels); } fn update( @@ -532,7 +551,7 @@ impl A320EngineDrivenPumpController { self.should_pressurise = false; } - self.update_low_pressure_fault(engine_n2, pressure_switch_state); + self.update_low_pressure_state(engine_n2, pressure_switch_state); } fn has_pressure_low_fault(&self) -> bool { @@ -547,13 +566,14 @@ impl PumpController for A320EngineDrivenPumpController { 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.has_pressure_low_fault()); + 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.has_pressure_low_fault()); + writer.write_bool("HYD_YELLOW_EDPUMP_LOW_PRESS", self.is_pressure_low); } else { panic!("The A320 only supports two engines."); } @@ -563,20 +583,30 @@ impl SimulationElement for A320EngineDrivenPumpController { struct A320BlueElectricPumpController { should_pressurise: bool, has_pressure_low_fault: bool, + is_pressure_low: bool, engine_1_master_on: bool, engine_2_master_on: bool, + weight_on_wheels: bool, } impl A320BlueElectricPumpController { fn new() -> Self { Self { should_pressurise: false, has_pressure_low_fault: false, + is_pressure_low: true, engine_1_master_on: false, engine_2_master_on: false, + weight_on_wheels: true, } } - fn update(&mut self, overhead_panel: &A320HydraulicOverheadPanel, pressure_switch_state: bool) { + fn update( + &mut self, + overhead_panel: &A320HydraulicOverheadPanel, + pressure_switch_state: bool, + engine1_n2: Ratio, + engine2_n2: Ratio, + ) { if overhead_panel.blue_epump_push_button.is_auto() { if self.engine_1_master_on || self.engine_2_master_on @@ -590,11 +620,23 @@ impl A320BlueElectricPumpController { self.should_pressurise = false; } - self.update_low_pressure_fault(pressure_switch_state); + self.update_low_pressure_state(pressure_switch_state, engine1_n2, engine2_n2); } - fn update_low_pressure_fault(&mut self, pressure_switch_state: bool) { - self.has_pressure_low_fault = self.should_pressurise() && !pressure_switch_state + fn update_low_pressure_state( + &mut self, + pressure_switch_state: bool, + engine1_n2: Ratio, + engine2_n2: Ratio, + ) { + // Faking low engine oil pressure using N2 slow level. TODO Get real oil pressure + let faked_is_engine_low_oil_pressure = + engine1_n2.get::() < 25. && engine2_n2.get::() < 25.; + + self.is_pressure_low = self.should_pressurise() && !pressure_switch_state; + + self.has_pressure_low_fault = + self.is_pressure_low && (!faked_is_engine_low_oil_pressure || !self.weight_on_wheels); } fn has_pressure_low_fault(&self) -> bool { @@ -612,10 +654,11 @@ impl SimulationElement for A320BlueElectricPumpController { fn read(&mut self, state: &mut SimulatorReader) { self.engine_1_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:1"); self.engine_2_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:2"); + 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.has_pressure_low_fault()); + writer.write_bool("HYD_BLUE_EPUMP_LOW_PRESS", self.is_pressure_low); } } @@ -628,6 +671,7 @@ impl Default for A320BlueElectricPumpController { 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 { @@ -638,6 +682,7 @@ impl A320YellowElectricPumpController { 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, ), @@ -663,11 +708,13 @@ impl A320YellowElectricPumpController { .should_activate_yellow_pump_for_cargo_door_operation .output(); - self.update_low_pressure_fault(pressure_switch_state); + self.update_low_pressure_state(pressure_switch_state); } - fn update_low_pressure_fault(&mut self, pressure_switch_state: bool) { - self.has_pressure_low_fault = self.should_pressurise() && !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 { @@ -687,7 +734,7 @@ impl PumpController for A320YellowElectricPumpController { } impl SimulationElement for A320YellowElectricPumpController { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.has_pressure_low_fault()); + writer.write_bool("HYD_YELLOW_EPUMP_LOW_PRESS", self.is_pressure_low); } } impl Default for A320YellowElectricPumpController { @@ -1348,22 +1395,32 @@ mod tests { Volume::new::(self.simulation_test_bed.read_f64("HYD_YELLOW_RESERVOIR")) } - fn is_green_edp_fault_low(&mut self) -> bool { + fn is_green_edp_press_low(&mut self) -> bool { self.simulation_test_bed .read_bool("HYD_GREEN_EDPUMP_LOW_PRESS") } - fn is_yellow_edp_fault_low(&mut self) -> bool { + 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_fault_low(&mut self) -> bool { + fn is_yellow_epump_press_low(&mut self) -> bool { self.simulation_test_bed .read_bool("HYD_YELLOW_EPUMP_LOW_PRESS") } - fn is_blue_epump_fault_low(&mut self) -> bool { + fn is_blue_epump_press_low(&mut self) -> bool { self.simulation_test_bed .read_bool("HYD_BLUE_EPUMP_LOW_PRESS") } @@ -1961,6 +2018,134 @@ mod tests { 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::(50.)) + .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::(50.)) + .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 green_edp_press_low_engine_off_to_on() { let mut test_bed = test_bed_with() @@ -1973,7 +2158,7 @@ mod tests { assert!(test_bed.is_green_edp_commanded_on()); // EDP should be LOW pressure state - assert!(test_bed.is_green_edp_fault_low()); + assert!(test_bed.is_green_edp_press_low()); // Starting eng 1 N2 is low at start test_bed = test_bed @@ -1981,7 +2166,7 @@ mod tests { .run_one_tick(); // Engine commanded on but pressure couldn't rise enough: we are in fault low - assert!(test_bed.is_green_edp_fault_low()); + assert!(test_bed.is_green_edp_press_low()); // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed @@ -1991,13 +2176,13 @@ mod tests { // 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_fault_low()); + 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_fault_low()); + assert!(!test_bed.is_green_edp_press_low()); } #[test] @@ -2012,14 +2197,14 @@ mod tests { 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_fault_low()); + 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_fault_low()); + assert!(!test_bed.is_green_edp_press_low()); // Waiting for 25s pressure should drop and still no fault test_bed = test_bed @@ -2028,7 +2213,7 @@ mod tests { assert!(!test_bed.is_green_pressurised()); assert!(test_bed.green_pressure() < Pressure::new::(500.)); - assert!(test_bed.is_green_edp_fault_low()); + assert!(test_bed.is_green_edp_press_low()); } #[test] @@ -2043,14 +2228,14 @@ mod tests { 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_fault_low()); + 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_fault_low()); + assert!(!test_bed.is_yellow_edp_press_low()); // Waiting for 25s pressure should drop and still no fault test_bed = test_bed @@ -2059,7 +2244,7 @@ mod tests { assert!(!test_bed.is_yellow_pressurised()); assert!(test_bed.yellow_pressure() < Pressure::new::(500.)); - assert!(test_bed.is_yellow_edp_fault_low()); + assert!(test_bed.is_yellow_edp_press_low()); } #[test] @@ -2074,7 +2259,7 @@ mod tests { assert!(test_bed.is_yellow_edp_commanded_on()); // EDP should be LOW pressure state - assert!(test_bed.is_yellow_edp_fault_low()); + assert!(test_bed.is_yellow_edp_press_low()); // Starting eng 2 N2 is low at start test_bed = test_bed @@ -2082,7 +2267,7 @@ mod tests { .run_one_tick(); // Engine commanded on but pressure couldn't rise enough: we are in fault low - assert!(test_bed.is_yellow_edp_fault_low()); + assert!(test_bed.is_yellow_edp_press_low()); // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed @@ -2092,13 +2277,13 @@ mod tests { // 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_fault_low()); + 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_fault_low()); + assert!(!test_bed.is_yellow_edp_press_low()); } #[test] @@ -2115,7 +2300,7 @@ mod tests { assert!(test_bed.is_yellow_edp_commanded_on()); // EDP should be LOW pressure state - assert!(test_bed.is_yellow_edp_fault_low()); + 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)); @@ -2123,7 +2308,7 @@ mod tests { // 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_fault_low()); + assert!(test_bed.is_yellow_edp_press_low()); // Starting eng 2 N2 is low at start test_bed = test_bed @@ -2131,7 +2316,7 @@ mod tests { .run_one_tick(); // Engine commanded on but pressure couldn't rise enough: we are in fault low - assert!(test_bed.is_yellow_edp_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 @@ -2141,7 +2326,7 @@ mod tests { // 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_fault_low()); + assert!(!test_bed.is_yellow_edp_press_low()); } #[test] @@ -2154,7 +2339,7 @@ mod tests { .run_one_tick(); // EDP should be LOW pressure state - assert!(test_bed.is_green_edp_fault_low()); + 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)); @@ -2164,7 +2349,7 @@ mod tests { 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_fault_low()); + assert!(test_bed.is_green_edp_press_low()); // Starting eng 1 N2 is low at start test_bed = test_bed @@ -2172,7 +2357,7 @@ mod tests { .run_one_tick(); // Engine commanded on but pressure couldn't rise enough: we are in fault low - assert!(test_bed.is_green_edp_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 @@ -2182,11 +2367,11 @@ mod tests { // 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_fault_low()); + assert!(!test_bed.is_green_edp_press_low()); } #[test] - fn yellow_epump_fault_low_at_pump_on() { + fn yellow_epump_press_low_at_pump_on() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2194,13 +2379,13 @@ mod tests { .run_one_tick(); // EDP should not be in fault low when cold start - assert!(!test_bed.is_yellow_epump_fault_low()); + 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_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)); @@ -2208,17 +2393,17 @@ mod tests { // 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_fault_low()); + 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_fault_low()); + assert!(!test_bed.is_yellow_epump_press_low()); } #[test] - fn blue_epump_fault_low_at_pump_on() { + fn blue_epump_press_low_at_pump_on() { let mut test_bed = test_bed_with() .engines_off() .on_the_ground() @@ -2226,13 +2411,13 @@ mod tests { .run_one_tick(); // EDP should not be in fault low when cold start - assert!(!test_bed.is_blue_epump_fault_low()); + 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_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)); @@ -2240,13 +2425,13 @@ mod tests { // 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_fault_low()); + 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_fault_low()); + assert!(!test_bed.is_blue_epump_press_low()); } #[test] @@ -3031,27 +3216,31 @@ mod tests { #[test] fn controller_blue_epump_split_master_switch() { + let engine_on_n2 = Ratio::new::(50.); + let engine_off_n2 = Ratio::new::(1.); let mut overhead_panel = A320HydraulicOverheadPanel::new(); overhead_panel.blue_epump_override_push_button.push_off(); let mut blue_epump_controller = A320BlueElectricPumpController::new(); - blue_epump_controller.update(&overhead_panel, true); + blue_epump_controller.update(&overhead_panel, true, engine_off_n2, engine_off_n2); assert!(!blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = false; - blue_epump_controller.update(&overhead_panel, true); + blue_epump_controller.update(&overhead_panel, true, engine_on_n2, engine_off_n2); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = true; - blue_epump_controller.update(&overhead_panel, true); + blue_epump_controller.update(&overhead_panel, true, engine_off_n2, engine_on_n2); assert!(blue_epump_controller.should_pressurise()); } #[test] fn controller_blue_epump_on_off_master_switch() { + let engine_on_n2 = Ratio::new::(50.); + let engine_off_n2 = Ratio::new::(1.); let mut overhead_panel = A320HydraulicOverheadPanel::new(); overhead_panel.blue_epump_override_push_button.push_off(); @@ -3059,30 +3248,31 @@ mod tests { blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = true; - blue_epump_controller.update(&overhead_panel, true); + blue_epump_controller.update(&overhead_panel, true, engine_on_n2, engine_on_n2); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; - blue_epump_controller.update(&overhead_panel, false); + blue_epump_controller.update(&overhead_panel, false, engine_off_n2, engine_off_n2); assert!(!blue_epump_controller.should_pressurise()); } #[test] fn controller_blue_epump_override() { + let engine_off_n2 = Ratio::new::(1.); let mut overhead_panel = A320HydraulicOverheadPanel::new(); let mut blue_epump_controller = A320BlueElectricPumpController::new(); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_on(); - blue_epump_controller.update(&overhead_panel, true); + blue_epump_controller.update(&overhead_panel, true, engine_off_n2, engine_off_n2); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_off(); - blue_epump_controller.update(&overhead_panel, false); + blue_epump_controller.update(&overhead_panel, false, engine_off_n2, engine_off_n2); assert!(!blue_epump_controller.should_pressurise()); } From 8b3cf49896bd95aca4265f673f04709b3dd0bbf0 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 4 May 2021 13:20:13 +0200 Subject: [PATCH 108/122] Updated blue faults using override --- src/systems/a320_systems/src/hydraulic.rs | 72 ++++++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index c233f01c19f..161dc075bb2 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -620,11 +620,17 @@ impl A320BlueElectricPumpController { self.should_pressurise = false; } - self.update_low_pressure_state(pressure_switch_state, engine1_n2, engine2_n2); + self.update_low_pressure_state( + overhead_panel, + pressure_switch_state, + engine1_n2, + engine2_n2, + ); } fn update_low_pressure_state( &mut self, + overhead_panel: &A320HydraulicOverheadPanel, pressure_switch_state: bool, engine1_n2: Ratio, engine2_n2: Ratio, @@ -635,8 +641,9 @@ impl A320BlueElectricPumpController { self.is_pressure_low = self.should_pressurise() && !pressure_switch_state; - self.has_pressure_low_fault = - self.is_pressure_low && (!faked_is_engine_low_oil_pressure || !self.weight_on_wheels); + self.has_pressure_low_fault = self.is_pressure_low + && ((!faked_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 { @@ -1425,6 +1432,11 @@ mod tests { .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 @@ -2146,6 +2158,60 @@ mod tests { 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::(50.)) + .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() From 4f4f4e54596b4520e0f4326c9df053d50e72105f Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 4 May 2021 16:15:46 +0200 Subject: [PATCH 109/122] Separate low pressure values for EDP sections --- src/systems/a320_systems/src/hydraulic.rs | 32 ++++++++++++++++------- src/systems/systems/src/hydraulic/mod.rs | 4 +-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 161dc075bb2..e2a57b30d50 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -5,7 +5,7 @@ use uom::si::{ use systems::hydraulic::{ ElectricPump, EngineDrivenPump, HydFluid, HydraulicLoop, HydraulicLoopController, - PowerTransferUnit, PowerTransferUnitController, PumpController, RamAirTurbine, + PowerTransferUnit, PowerTransferUnitController, PressureSwitch, PumpController, RamAirTurbine, RamAirTurbineController, }; use systems::overhead::{ @@ -29,9 +29,11 @@ pub(super) struct A320Hydraulic { 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, @@ -57,10 +59,10 @@ pub(super) struct A320Hydraulic { lag_time_accumulator: Duration, } impl A320Hydraulic { - const MIN_PRESS_BLUE_PRESSURISED_LO_HYST: f64 = 1450.0; - const MIN_PRESS_BLUE_PRESSURISED_HI_HYST: f64 = 1750.0; - const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1740.0; - const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 2200.0; + 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; const HYDRAULIC_SIM_TIME_STEP: u64 = 100; // Refresh rate of hydraulic simulation in ms const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; // Refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update @@ -79,8 +81,8 @@ impl A320Hydraulic { Volume::new::(1.56), HydFluid::new(Pressure::new::(1450000000.0)), false, - Pressure::new::(Self::MIN_PRESS_BLUE_PRESSURISED_LO_HYST), - Pressure::new::(Self::MIN_PRESS_BLUE_PRESSURISED_HI_HYST), + 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( @@ -112,9 +114,17 @@ impl A320Hydraulic { ), 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), @@ -343,11 +353,13 @@ impl A320Hydraulic { &self.power_transfer_unit_controller, ); + self.engine_driven_pump_1_pressure_switch + .update(self.green_loop.get_pressure()); self.engine_driven_pump_1_controller.update( overhead_panel, engine_fire_overhead, engine1.corrected_n2(), - self.green_loop.is_pressurised(), + self.engine_driven_pump_1_pressure_switch.is_pressurised(), ); self.engine_driven_pump_1.update( @@ -357,11 +369,13 @@ impl A320Hydraulic { &self.engine_driven_pump_1_controller, ); + self.engine_driven_pump_2_pressure_switch + .update(self.yellow_loop.get_pressure()); self.engine_driven_pump_2_controller.update( overhead_panel, engine_fire_overhead, engine2.corrected_n2(), - self.yellow_loop.is_pressurised(), + self.engine_driven_pump_2_pressure_switch.is_pressurised(), ); self.engine_driven_pump_2.update( diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index ef0be3109d6..202b84e02a2 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -753,8 +753,8 @@ pub struct ElectricPump { pump: Pump, } impl ElectricPump { - const SPOOLUP_TIME: f64 = 4.0; - const SPOOLDOWN_TIME: f64 = 4.0; + 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, From d303e79673c5a75daf7cb55cc3f32d8dc28879a3 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 6 May 2021 13:06:41 +0200 Subject: [PATCH 110/122] refactor: getter naming according to RFC 344 The Rust naming guideline states: use getters without a get prefix. --- .../src/main.rs | 262 +++++++++--------- src/systems/a320_systems/src/hydraulic.rs | 12 +- .../systems/src/hydraulic/brakecircuit.rs | 126 ++++----- src/systems/systems/src/hydraulic/mod.rs | 116 ++++---- 4 files changed, 258 insertions(+), 258 deletions(-) diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 9f6987fe68c..bcacefc025f 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -251,40 +251,40 @@ fn green_loop_edp_simulation(path: &str) { green_loop_history.init( 0.0, vec![ - green_loop.get_pressure().get::(), - green_loop.get_loop_fluid_volume().get::(), - green_loop.get_reservoir_volume().get::(), - green_loop.get_current_flow().get::(), + 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.get_delta_vol_max().get::(), + edp1.delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); accu_green_history.init( 0.0, vec![ - green_loop.get_pressure().get::(), - green_loop.get_accumulator_gas_pressure().get::(), - green_loop.get_accumulator_fluid_volume().get::(), - green_loop.get_accumulator_gas_volume().get::(), + 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.get_pressure() >= Pressure::new::(2850.0)); + assert!(green_loop.pressure() >= Pressure::new::(2850.0)); } if x == 200 { - assert!(green_loop.get_pressure() >= Pressure::new::(2850.0)); + assert!(green_loop.pressure() >= Pressure::new::(2850.0)); edp1_controller.command_depressurise(); } if x >= 500 { //Shutdown + 30s - assert!(green_loop.get_pressure() <= Pressure::new::(250.0)); + assert!(green_loop.pressure() <= Pressure::new::(250.0)); } edp1.update(&context, &green_loop, &engine1, &edp1_controller); @@ -299,52 +299,52 @@ fn green_loop_edp_simulation(path: &str) { if x % 20 == 0 { println!("Iteration {}", x); println!("-------------------------------------------"); - println!("---PSI: {}", green_loop.get_pressure().get::()); + println!("---PSI: {}", green_loop.pressure().get::()); println!( "--------Reservoir Volume (g): {}", - green_loop.get_reservoir_volume().get::() + green_loop.reservoir_volume().get::() ); println!( "--------Loop Volume (g): {}", - green_loop.get_loop_fluid_volume().get::() + green_loop.loop_fluid_volume().get::() ); println!( "--------Acc Fluid Volume (L): {}", - green_loop.get_accumulator_fluid_volume().get::() + green_loop.accumulator_fluid_volume().get::() ); println!( "--------Acc Gas Volume (L): {}", - green_loop.get_accumulator_gas_volume().get::() + green_loop.accumulator_gas_volume().get::() ); println!( "--------Acc Gas Pressure (psi): {}", - green_loop.get_accumulator_gas_pressure().get::() + green_loop.accumulator_gas_pressure().get::() ); } green_loop_history.update( context.delta_as_secs_f64(), vec![ - green_loop.get_pressure().get::(), - green_loop.get_loop_fluid_volume().get::(), - green_loop.get_reservoir_volume().get::(), - green_loop.get_current_flow().get::(), + 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.get_delta_vol_max().get::(), + edp1.delta_vol_max().get::(), engine1.corrected_n2.get::() as f64, ], ); accu_green_history.update( context.delta_as_secs_f64(), vec![ - green_loop.get_pressure().get::(), - green_loop.get_accumulator_gas_pressure().get::(), - green_loop.get_accumulator_fluid_volume().get::(), - green_loop.get_accumulator_gas_volume().get::(), + green_loop.pressure().get::(), + green_loop.accumulator_gas_pressure().get::(), + green_loop.accumulator_fluid_volume().get::(), + green_loop.accumulator_gas_volume().get::(), ], ); } @@ -410,55 +410,55 @@ fn yellow_green_ptu_loop_simulation(path: &str) { loop_history.init( 0.0, vec![ - green_loop.get_pressure().get::(), - yellow_loop.get_pressure().get::(), - green_loop.get_reservoir_volume().get::(), - yellow_loop.get_reservoir_volume().get::(), - green_loop.get_current_delta_vol().get::(), - yellow_loop.get_current_delta_vol().get::(), + 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.get_flow().get::(), - green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), - ptu.get_is_active_left_to_right() as i8 as f64, - ptu.get_is_active_right_to_left() as i8 as f64, + 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.get_pressure().get::(), - green_loop.get_accumulator_gas_pressure().get::(), - green_loop.get_accumulator_fluid_volume().get::(), - green_loop.get_accumulator_gas_volume().get::(), + 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.get_pressure().get::(), - yellow_loop.get_accumulator_gas_pressure().get::(), - yellow_loop.get_accumulator_fluid_volume().get::(), - yellow_loop.get_accumulator_gas_volume().get::(), + 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.get_reservoir_volume(); - let green_res_at_start = green_loop.get_reservoir_volume(); + let yellow_res_at_start = yellow_loop.reservoir_volume(); + let green_res_at_start = green_loop.reservoir_volume(); engine1.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { //After 1s powering electric pump println!("------------YELLOW EPUMP ON------------"); - assert!(yellow_loop.get_pressure() <= Pressure::new::(50.0)); - assert!(yellow_loop.get_reservoir_volume() == yellow_res_at_start); + assert!(yellow_loop.pressure() <= Pressure::new::(50.0)); + assert!(yellow_loop.reservoir_volume() == yellow_res_at_start); - assert!(green_loop.get_pressure() <= Pressure::new::(50.0)); - assert!(green_loop.get_reservoir_volume() == green_res_at_start); + assert!(green_loop.pressure() <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume() == green_res_at_start); epump_controller.command_pressurise(); } @@ -466,11 +466,11 @@ fn yellow_green_ptu_loop_simulation(path: &str) { if x == 110 { //10s later enabling ptu println!("--------------PTU ENABLED--------------"); - assert!(yellow_loop.get_pressure() >= Pressure::new::(2950.0)); - assert!(yellow_loop.get_reservoir_volume() <= yellow_res_at_start); + assert!(yellow_loop.pressure() >= Pressure::new::(2950.0)); + assert!(yellow_loop.reservoir_volume() <= yellow_res_at_start); - assert!(green_loop.get_pressure() <= Pressure::new::(50.0)); - assert!(green_loop.get_reservoir_volume() == green_res_at_start); + assert!(green_loop.pressure() <= Pressure::new::(50.0)); + assert!(green_loop.reservoir_volume() == green_res_at_start); ptu_controller.command_enable(); } @@ -478,30 +478,30 @@ fn yellow_green_ptu_loop_simulation(path: &str) { if x == 300 { //@30s, ptu should be supplying green loop println!("----------PTU SUPPLIES GREEN------------"); - assert!(yellow_loop.get_pressure() >= Pressure::new::(2400.0)); - assert!(green_loop.get_pressure() >= Pressure::new::(2400.0)); + 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.get_pressure() >= Pressure::new::(2600.0)); - assert!(green_loop.get_pressure() >= Pressure::new::(2000.0)); + 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.get_pressure() >= Pressure::new::(2900.0)); - assert!(green_loop.get_pressure() >= Pressure::new::(2900.0)); + 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.get_pressure() >= Pressure::new::(2900.0)); - assert!(green_loop.get_pressure() >= Pressure::new::(2900.0)); + assert!(yellow_loop.pressure() >= Pressure::new::(2900.0)); + assert!(green_loop.pressure() >= Pressure::new::(2900.0)); edp1_controller.command_depressurise(); epump_controller.command_depressurise(); } @@ -509,16 +509,16 @@ fn yellow_green_ptu_loop_simulation(path: &str) { if x == 800 { //@80s diabling edp and epump println!("-----------IS PRESSURE OFF?-----------"); - assert!(yellow_loop.get_pressure() < Pressure::new::(50.0)); - assert!(green_loop.get_pressure() <= Pressure::new::(50.0)); + assert!(yellow_loop.pressure() < Pressure::new::(50.0)); + assert!(green_loop.pressure() <= Pressure::new::(50.0)); assert!( - green_loop.get_reservoir_volume() > Volume::new::(0.0) - && green_loop.get_reservoir_volume() <= green_res_at_start + green_loop.reservoir_volume() > Volume::new::(0.0) + && green_loop.reservoir_volume() <= green_res_at_start ); assert!( - yellow_loop.get_reservoir_volume() > Volume::new::(0.0) - && yellow_loop.get_reservoir_volume() <= yellow_res_at_start + yellow_loop.reservoir_volume() > Volume::new::(0.0) + && yellow_loop.reservoir_volume() <= yellow_res_at_start ); } @@ -546,59 +546,59 @@ fn yellow_green_ptu_loop_simulation(path: &str) { loop_history.update( context.delta_as_secs_f64(), vec![ - green_loop.get_pressure().get::(), - yellow_loop.get_pressure().get::(), - green_loop.get_reservoir_volume().get::(), - yellow_loop.get_reservoir_volume().get::(), - green_loop.get_current_delta_vol().get::(), - yellow_loop.get_current_delta_vol().get::(), + 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.get_flow().get::(), - green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), - ptu.get_is_active_left_to_right() as i8 as f64, - ptu.get_is_active_right_to_left() as i8 as f64, + 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.get_pressure().get::(), - green_loop.get_accumulator_gas_pressure().get::(), - green_loop.get_accumulator_fluid_volume().get::(), - green_loop.get_accumulator_gas_volume().get::(), + 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.get_pressure().get::(), - yellow_loop.get_accumulator_gas_pressure().get::(), - yellow_loop.get_accumulator_fluid_volume().get::(), - yellow_loop.get_accumulator_gas_volume().get::(), + 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.get_pressure().get::()); - println!("---RPM YELLOW: {}", epump.get_rpm()); + println!("---PSI YELLOW: {}", yellow_loop.pressure().get::()); + println!("---RPM YELLOW: {}", epump.rpm()); println!( "---Priming State: {}/{}", - yellow_loop.get_loop_fluid_volume().get::(), - yellow_loop.get_max_volume().get::() + yellow_loop.loop_fluid_volume().get::(), + yellow_loop.max_volume().get::() ); - println!("---PSI GREEN: {}", green_loop.get_pressure().get::()); + println!("---PSI GREEN: {}", green_loop.pressure().get::()); println!("---N2 GREEN: {}", engine1.corrected_n2.get::()); println!( "---Priming State: {}/{}", - green_loop.get_loop_fluid_volume().get::(), - green_loop.get_max_volume().get::() + green_loop.loop_fluid_volume().get::(), + green_loop.max_volume().get::() ); } } @@ -666,39 +666,39 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { loop_history.init( 0.0, vec![ - green_loop.get_pressure().get::(), - yellow_loop.get_pressure().get::(), - green_loop.get_reservoir_volume().get::(), - yellow_loop.get_reservoir_volume().get::(), - green_loop.get_current_delta_vol().get::(), - yellow_loop.get_current_delta_vol().get::(), + 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.get_flow().get::(), - green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), - ptu.get_is_active_left_to_right() as i8 as f64, - ptu.get_is_active_right_to_left() as i8 as f64, + 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.get_pressure().get::(), - green_loop.get_accumulator_gas_pressure().get::(), - green_loop.get_accumulator_fluid_volume().get::(), - green_loop.get_accumulator_gas_volume().get::(), + 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.get_pressure().get::(), - yellow_loop.get_accumulator_gas_pressure().get::(), - yellow_loop.get_accumulator_fluid_volume().get::(), - yellow_loop.get_accumulator_gas_volume().get::(), + yellow_loop.pressure().get::(), + yellow_loop.accumulator_gas_pressure().get::(), + yellow_loop.accumulator_fluid_volume().get::(), + yellow_loop.accumulator_gas_volume().get::(), ], ); @@ -715,7 +715,7 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { } if x >= 400 { - println!("Gpress={}", green_loop.get_pressure().get::()); + println!("Gpress={}", green_loop.pressure().get::()); } ptu.update(&green_loop, &yellow_loop, &ptu_controller); edp2.update(&context, &yellow_loop, &engine2, &edp2_controller); @@ -741,40 +741,40 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { loop_history.update( context.delta_as_secs_f64(), vec![ - green_loop.get_pressure().get::(), - yellow_loop.get_pressure().get::(), - green_loop.get_reservoir_volume().get::(), - yellow_loop.get_reservoir_volume().get::(), - green_loop.get_current_delta_vol().get::(), - yellow_loop.get_current_delta_vol().get::(), + 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.get_flow().get::(), - green_loop.get_pressure().get::() - yellow_loop.get_pressure().get::(), - ptu.get_is_active_left_to_right() as i8 as f64, - ptu.get_is_active_right_to_left() as i8 as f64, + 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.get_pressure().get::(), - green_loop.get_accumulator_gas_pressure().get::(), - green_loop.get_accumulator_fluid_volume().get::(), - green_loop.get_accumulator_gas_volume().get::(), + 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.get_pressure().get::(), - yellow_loop.get_accumulator_gas_pressure().get::(), - yellow_loop.get_accumulator_fluid_volume().get::(), - yellow_loop.get_accumulator_gas_volume().get::(), + yellow_loop.pressure().get::(), + yellow_loop.accumulator_gas_pressure().get::(), + yellow_loop.accumulator_fluid_volume().get::(), + yellow_loop.accumulator_gas_volume().get::(), ], ); } diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index e2a57b30d50..fa649316646 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -354,7 +354,7 @@ impl A320Hydraulic { ); self.engine_driven_pump_1_pressure_switch - .update(self.green_loop.get_pressure()); + .update(self.green_loop.pressure()); self.engine_driven_pump_1_controller.update( overhead_panel, engine_fire_overhead, @@ -370,7 +370,7 @@ impl A320Hydraulic { ); self.engine_driven_pump_2_pressure_switch - .update(self.yellow_loop.get_pressure()); + .update(self.yellow_loop.pressure()); self.engine_driven_pump_2_controller.update( overhead_panel, engine_fire_overhead, @@ -977,7 +977,7 @@ impl A320HydraulicBrakingLogic { alternate_circuit: &BrakeCircuit, landing_gear: &LandingGear, ) { - self.update_normal_braking_availability(&green_loop.get_pressure()); + 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( @@ -1018,9 +1018,9 @@ impl A320HydraulicBrakingLogic { 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.get_brake_pressure_left().get::() + if alternate_circuit.left_brake_pressure().get::() < Self::MIN_PRESSURE_PARK_BRAKE_EMERGENCY - || alternate_circuit.get_brake_pressure_right().get::() + || alternate_circuit.right_brake_pressure().get::() < Self::MIN_PRESSURE_PARK_BRAKE_EMERGENCY { self.left_brake_green_output = self.left_brake_pilot_input; @@ -1290,7 +1290,7 @@ mod tests { } fn get_yellow_brake_accumulator_fluid_volume(&self) -> Volume { - self.hydraulics.braking_circuit_altn.get_acc_fluid_volume() + self.hydraulics.braking_circuit_altn.acc_fluid_volume() } fn is_nws_pin_inserted(&self) -> bool { diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 8e973462bc5..91591243d09 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -11,9 +11,9 @@ use uom::si::{acceleration::foot_per_second_squared, f64::*, pressure::psi, volu use super::HydraulicAccumulator; -pub trait ActuatorHydInterface { - fn get_used_volume(&self) -> Volume; - fn get_reservoir_return(&self) -> Volume; +pub trait Actuator { + fn used_volume(&self) -> Volume; + fn reservoir_return(&self) -> Volume; } pub struct BrakeActuator { @@ -100,11 +100,11 @@ impl BrakeActuator { } } -impl ActuatorHydInterface for BrakeActuator { - fn get_used_volume(&self) -> Volume { +impl Actuator for BrakeActuator { + fn used_volume(&self) -> Volume { self.volume_to_actuator_accumulator } - fn get_reservoir_return(&self) -> Volume { + fn reservoir_return(&self) -> Volume { self.volume_to_res_accumulator } } @@ -150,14 +150,14 @@ impl SimulationElement for BrakeCircuit { fn write(&self, writer: &mut SimulatorWriter) { writer.write_f64( &self.id_left_press, - self.get_brake_pressure_left().get::(), + self.left_brake_pressure().get::(), ); writer.write_f64( &self.id_right_press, - self.get_brake_pressure_right().get::(), + self.right_brake_pressure().get::(), ); if self.has_accumulator { - writer.write_f64(&self.id_acc_press, self.get_acc_pressure().get::()); + writer.write_f64(&self.id_acc_press, self.acc_pressure().get::()); } } } @@ -247,16 +247,16 @@ impl BrakeCircuit { 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.get_fluid_volume() > Volume::new::(0.) { - actual_pressure_available = self.accumulator.get_raw_gas_press(); + if self.accumulator.fluid_volume() > Volume::new::(0.) { + actual_pressure_available = self.accumulator.raw_gas_press(); } else { - actual_pressure_available = hyd_loop.get_pressure(); + actual_pressure_available = hyd_loop.pressure(); } self.update_brake_actuators(context, actual_pressure_available); - let delta_vol = self.left_brake_actuator.get_used_volume() - + self.right_brake_actuator.get_used_volume(); + 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.); @@ -279,8 +279,8 @@ impl BrakeCircuit { self.volume_to_actuator_accumulator += delta_vol; } - self.volume_to_res_accumulator += self.left_brake_actuator.get_reservoir_return(); - self.volume_to_res_accumulator += self.right_brake_actuator.get_reservoir_return(); + 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(); @@ -305,19 +305,19 @@ impl BrakeCircuit { self.demanded_brake_position_right = brake_ratio.min(1.0).max(0.0); } - pub fn get_brake_pressure_left(&self) -> Pressure { + pub fn left_brake_pressure(&self) -> Pressure { self.pressure_applied_left } - pub fn get_brake_pressure_right(&self) -> Pressure { + pub fn right_brake_pressure(&self) -> Pressure { self.pressure_applied_right } - pub fn get_acc_pressure(&self) -> Pressure { + pub fn acc_pressure(&self) -> Pressure { self.accumulator_fluid_pressure_sensor_filtered } - pub fn get_acc_fluid_volume(&self) -> Volume { - self.accumulator.fluid_volume + pub fn acc_fluid_volume(&self) -> Volume { + self.accumulator.fluid_volume() } pub fn reset_accumulators(&mut self) { @@ -326,11 +326,11 @@ impl BrakeCircuit { } } -impl ActuatorHydInterface for BrakeCircuit { - fn get_used_volume(&self) -> Volume { +impl Actuator for BrakeCircuit { + fn used_volume(&self) -> Volume { self.volume_to_actuator_accumulator } - fn get_reservoir_return(&self) -> Volume { + fn reservoir_return(&self) -> Volume { self.volume_to_res_accumulator } } @@ -404,7 +404,7 @@ impl AutoBrakeController { } } - pub fn get_brake_command(&self) -> f64 { + pub fn brake_command(&self) -> f64 { self.current_brake_dmnd } @@ -548,13 +548,13 @@ mod tests { ); assert!( - brake_circuit_unprimed.get_brake_pressure_left() - + brake_circuit_unprimed.get_brake_pressure_right() + 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.get_fluid_volume() == Volume::new::(0.0) + brake_circuit_unprimed.accumulator.fluid_volume() == Volume::new::(0.0) ); assert!(brake_circuit_unprimed.accumulator.gas_volume == init_max_vol); @@ -566,12 +566,12 @@ mod tests { ); assert!( - brake_circuit_unprimed.get_brake_pressure_left() - + brake_circuit_unprimed.get_brake_pressure_right() + 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.get_fluid_volume() == init_max_vol / 2.0); + assert!(brake_circuit_primed.accumulator.fluid_volume() == init_max_vol / 2.0); assert!(brake_circuit_primed.accumulator.gas_volume < init_max_vol); } @@ -589,32 +589,32 @@ mod tests { ); assert!( - brake_circuit_primed.get_brake_pressure_left() - + brake_circuit_primed.get_brake_pressure_right() + 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.get_brake_pressure_left() - + brake_circuit_primed.get_brake_pressure_right() + 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.get_brake_pressure_left() >= Pressure::new::(1000.)); - assert!(brake_circuit_primed.get_brake_pressure_right() <= Pressure::new::(50.)); - assert!(brake_circuit_primed.accumulator.get_fluid_volume() >= Volume::new::(0.1)); + 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.get_brake_pressure_right() >= Pressure::new::(1000.)); - assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); - assert!(brake_circuit_primed.accumulator.get_fluid_volume() >= Volume::new::(0.1)); + 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] @@ -631,31 +631,31 @@ mod tests { ); assert!( - brake_circuit_primed.get_brake_pressure_left() - + brake_circuit_primed.get_brake_pressure_right() + 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.get_brake_pressure_left() - + brake_circuit_primed.get_brake_pressure_right() + 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.get_brake_pressure_left() >= Pressure::new::(2500.)); - assert!(brake_circuit_primed.get_brake_pressure_right() <= Pressure::new::(50.)); + 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.get_brake_pressure_right() >= Pressure::new::(2500.)); - assert!(brake_circuit_primed.get_brake_pressure_left() <= Pressure::new::(50.)); - assert!(brake_circuit_primed.accumulator.get_fluid_volume() == Volume::new::(0.0)); + 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] @@ -674,8 +674,8 @@ mod tests { brake_circuit_primed.update(&context(Duration::from_secs_f64(5.)), &hyd_loop); assert!( - brake_circuit_primed.get_brake_pressure_left() - + brake_circuit_primed.get_brake_pressure_right() + brake_circuit_primed.left_brake_pressure() + + brake_circuit_primed.right_brake_pressure() < Pressure::new::(1.0) ); @@ -683,27 +683,27 @@ mod tests { 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.get_brake_pressure_left() >= Pressure::new::(2900.)); - assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); + 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.get_brake_pressure_left() >= Pressure::new::(2900.)); - assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2900.)); + 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_ena(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.get_brake_pressure_left() >= Pressure::new::(2500.)); - assert!(brake_circuit_primed.get_brake_pressure_right() >= Pressure::new::(2500.)); + 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.get_brake_pressure_left() <= pressure_limit); - assert!(brake_circuit_primed.get_brake_pressure_right() <= pressure_limit); + assert!(brake_circuit_primed.left_brake_pressure() <= pressure_limit); + assert!(brake_circuit_primed.right_brake_pressure() <= pressure_limit); } #[test] @@ -715,14 +715,14 @@ mod tests { ]); let context = context(Duration::from_secs_f64(0.)); - assert!(controller.get_brake_command() <= 0.0); + assert!(controller.brake_command() <= 0.0); controller.update(&context.delta(), &context); - assert!(controller.get_brake_command() <= 0.0); + assert!(controller.brake_command() <= 0.0); controller.set_enable(true); controller.update(&context.delta(), &context); - assert!(controller.get_brake_command() >= 0.0); + assert!(controller.brake_command() >= 0.0); } fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 202b84e02a2..1abc1fa3296 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -15,7 +15,7 @@ use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWr use crate::{engine::Engine, simulation::UpdateContext}; pub mod brakecircuit; -use crate::hydraulic::brakecircuit::ActuatorHydInterface; +use crate::hydraulic::brakecircuit::Actuator; // Trait common to all hydraulic pumps // Max gives maximum available volume at that time as if it is a variable displacement @@ -23,8 +23,8 @@ use crate::hydraulic::brakecircuit::ActuatorHydInterface; // Min will give minimum volume that will be outputed no matter what. example if there is a minimal displacement or // a fixed displacement (ie. elec pump) pub trait PressureSource { - fn get_delta_vol_max(&self) -> Volume; - fn get_delta_vol_min(&self) -> Volume; + fn delta_vol_max(&self) -> Volume; + fn delta_vol_min(&self) -> Volume; } //////////////////////////////////////////////////////////////////////////////// @@ -46,7 +46,7 @@ impl HydFluid { } } - pub fn get_bulk_mod(&self) -> Pressure { + pub fn bulk_mod(&self) -> Pressure { self.current_bulk } } @@ -115,11 +115,11 @@ impl PowerTransferUnit { } } - pub fn get_flow(&self) -> VolumeRate { + pub fn flow(&self) -> VolumeRate { self.last_flow } - pub fn get_is_active(&self) -> bool { + pub fn is_active(&self) -> bool { self.is_active_right || self.is_active_left } @@ -127,11 +127,11 @@ impl PowerTransferUnit { self.is_enabled } - pub fn get_is_active_left_to_right(&self) -> bool { + pub fn is_active_left_to_right(&self) -> bool { self.is_active_left } - pub fn get_is_active_right_to_left(&self) -> bool { + pub fn is_active_right_to_left(&self) -> bool { self.is_active_right } @@ -143,7 +143,7 @@ impl PowerTransferUnit { ) { self.is_enabled = controller.should_enable(); - let delta_p = loop_left.get_pressure() - loop_right.get_pressure(); + let delta_p = loop_left.pressure() - loop_right.pressure(); if !self.is_enabled || self.is_active_right && delta_p.get::() > -5. @@ -208,7 +208,7 @@ impl SimulationElement for PowerTransferUnit { writer.write_bool("HYD_PTU_ACTIVE_R2L", self.is_active_right); writer.write_f64( "HYD_PTU_MOTOR_FLOW", - self.get_flow().get::(), + self.flow().get::(), ); writer.write_bool("HYD_PTU_VALVE_OPENED", self.is_enabled()); } @@ -321,11 +321,11 @@ impl HydraulicAccumulator { volume_from_acc } - pub fn get_fluid_volume(&self) -> Volume { + pub fn fluid_volume(&self) -> Volume { self.fluid_volume } - pub fn get_raw_gas_press(&self) -> Pressure { + pub fn raw_gas_press(&self) -> Pressure { self.gas_pressure } } @@ -417,39 +417,39 @@ impl HydraulicLoop { } } - pub fn get_current_flow(&self) -> VolumeRate { + pub fn current_flow(&self) -> VolumeRate { self.current_flow } - pub fn get_current_delta_vol(&self) -> Volume { + pub fn current_delta_vol(&self) -> Volume { self.current_delta_vol } - pub fn get_accumulator_gas_pressure(&self) -> Pressure { + pub fn accumulator_gas_pressure(&self) -> Pressure { self.accumulator.gas_pressure } - pub fn get_accumulator_fluid_volume(&self) -> Volume { + pub fn accumulator_fluid_volume(&self) -> Volume { self.accumulator.fluid_volume } - pub fn get_pressure(&self) -> Pressure { + pub fn pressure(&self) -> Pressure { self.loop_pressure } - pub fn get_reservoir_volume(&self) -> Volume { + pub fn reservoir_volume(&self) -> Volume { self.reservoir_volume } - pub fn get_loop_fluid_volume(&self) -> Volume { + pub fn loop_fluid_volume(&self) -> Volume { self.loop_volume } - pub fn get_max_volume(&self) -> Volume { + pub fn max_volume(&self) -> Volume { self.max_loop_volume } - pub fn get_accumulator_gas_volume(&self) -> Volume { + pub fn accumulator_gas_volume(&self) -> Volume { self.accumulator.gas_volume } @@ -461,9 +461,9 @@ impl HydraulicLoop { drawn } - pub fn update_actuator_volumes(&mut self, actuator: &T) { - self.total_actuators_consumed_volume += actuator.get_used_volume(); - self.total_actuators_returned_volume += actuator.get_reservoir_return(); + 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 @@ -480,13 +480,13 @@ impl HydraulicLoop { // Method to update pressure of a loop. The more delta volume is added, the more pressure rises // directly from bulk modulus equation pub fn delta_pressure_from_delta_volume(&self, delta_vol: Volume) -> Pressure { - return delta_vol / self.high_pressure_volume * self.fluid.get_bulk_mod(); + 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 pub fn vol_to_target(&self, target_press: Pressure) -> Volume { (target_press - self.loop_pressure) * (self.high_pressure_volume) - / self.fluid.get_bulk_mod() + / self.fluid.bulk_mod() } pub fn is_fire_shutoff_valve_opened(&self) -> bool { @@ -558,17 +558,17 @@ impl HydraulicLoop { if self.fire_shutoff_valve_opened { for p in engine_driven_pumps { - delta_vol_max += p.get_delta_vol_max(); - delta_vol_min += p.get_delta_vol_min(); + delta_vol_max += p.delta_vol_max(); + delta_vol_min += p.delta_vol_min(); } } for p in electric_pumps { - delta_vol_max += p.get_delta_vol_max(); - delta_vol_min += p.get_delta_vol_min(); + delta_vol_max += p.delta_vol_max(); + delta_vol_min += p.delta_vol_min(); } for p in ram_air_pumps { - delta_vol_max += p.get_delta_vol_max(); - delta_vol_min += p.get_delta_vol_min(); + 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 @@ -656,10 +656,10 @@ impl HydraulicLoop { } impl SimulationElement for HydraulicLoop { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64(&self.pressure_id, self.get_pressure().get::()); + writer.write_f64(&self.pressure_id, self.pressure().get::()); writer.write_f64( &self.reservoir_id, - self.get_reservoir_volume().get::(), + self.reservoir_volume().get::(), ); if self.has_fire_valve { writer.write_bool(&self.fire_valve_id, self.is_fire_shutoff_valve_opened()); @@ -703,7 +703,7 @@ impl Pump { rpm: f64, controller: &T, ) { - let theoretical_displacement = self.calculate_displacement(line.get_pressure(), controller); + 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 @@ -736,11 +736,11 @@ impl Pump { } } impl PressureSource for Pump { - fn get_delta_vol_max(&self) -> Volume { + fn delta_vol_max(&self) -> Volume { self.delta_vol_max } - fn get_delta_vol_min(&self) -> Volume { + fn delta_vol_min(&self) -> Volume { self.delta_vol_min } } @@ -775,7 +775,7 @@ impl ElectricPump { } } - pub fn get_rpm(&self) -> f64 { + pub fn rpm(&self) -> f64 { self.rpm } @@ -805,11 +805,11 @@ impl ElectricPump { } } impl PressureSource for ElectricPump { - fn get_delta_vol_max(&self) -> Volume { - self.pump.get_delta_vol_max() + fn delta_vol_max(&self) -> Volume { + self.pump.delta_vol_max() } - fn get_delta_vol_min(&self) -> Volume { - self.pump.get_delta_vol_min() + fn delta_vol_min(&self) -> Volume { + self.pump.delta_vol_min() } } impl SimulationElement for ElectricPump { @@ -868,11 +868,11 @@ impl EngineDrivenPump { } } impl PressureSource for EngineDrivenPump { - fn get_delta_vol_min(&self) -> Volume { - self.pump.get_delta_vol_min() + fn delta_vol_min(&self) -> Volume { + self.pump.delta_vol_min() } - fn get_delta_vol_max(&self) -> Volume { - self.pump.get_delta_vol_max() + fn delta_vol_max(&self) -> Volume { + self.pump.delta_vol_max() } } impl SimulationElement for EngineDrivenPump { @@ -908,7 +908,7 @@ impl WindTurbine { } } - pub fn get_rpm(&self) -> f64 { + pub fn rpm(&self) -> f64 { self.rpm } @@ -969,7 +969,7 @@ impl WindTurbine { } impl SimulationElement for WindTurbine { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_f64("HYD_RAT_RPM", self.get_rpm()); + writer.write_f64("HYD_RAT_RPM", self.rpm()); } } impl Default for WindTurbine { @@ -1052,7 +1052,7 @@ impl RamAirTurbine { self.pump.update( context, line, - self.wind_turbine.get_rpm(), + self.wind_turbine.rpm(), &self.pump_controller, ); @@ -1063,7 +1063,7 @@ impl RamAirTurbine { 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.get_delta_vol_max().get::() / self.max_displacement; + let displacement_ratio = self.delta_vol_max().get::() / self.max_displacement; self.wind_turbine.update( &delta_time, &indicated_airspeed, @@ -1086,12 +1086,12 @@ impl RamAirTurbine { } } impl PressureSource for RamAirTurbine { - fn get_delta_vol_max(&self) -> Volume { - self.pump.get_delta_vol_max() + fn delta_vol_max(&self) -> Volume { + self.pump.delta_vol_max() } - fn get_delta_vol_min(&self) -> Volume { - self.pump.get_delta_vol_min() + fn delta_vol_min(&self) -> Volume { + self.pump.delta_vol_min() } } impl SimulationElement for RamAirTurbine { @@ -1260,7 +1260,7 @@ mod tests { ); println!( "--------Acc Fluid Volume (L): {}", - green_loop.accumulator.get_fluid_volume().get::() + green_loop.accumulator.fluid_volume().get::() ); println!( "--------Acc Gas Volume (L): {}", @@ -1268,7 +1268,7 @@ mod tests { ); println!( "--------Acc Gas Pressure (psi): {}", - green_loop.accumulator.get_raw_gas_press().get::() + green_loop.accumulator.raw_gas_press().get::() ); } } @@ -1441,7 +1441,7 @@ mod tests { println!("---PSI: {}", blue_loop.loop_pressure.get::()); println!("---RAT stow pos: {}", rat.position); println!("---RAT RPM: {}", rat.wind_turbine.rpm); - println!("---RAT volMax: {}", rat.get_delta_vol_max().get::()); + println!("---RAT volMax: {}", rat.delta_vol_max().get::()); println!( "--------Reservoir Volume (g): {}", blue_loop.reservoir_volume.get::() @@ -1731,7 +1731,7 @@ mod tests { ); //Update 10 times to stabilize displacement edp.update(context, &line, &eng, &engine_driven_pump_controller); - edp.get_delta_vol_max() + edp.delta_vol_max() } fn get_edp_predicted_delta_vol_when( From 332738c80f2d24ff74b7d354499ba17f49a87361 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 6 May 2021 13:36:05 +0200 Subject: [PATCH 111/122] chore: rust style guide comment formatting --- .../src/main.rs | 72 ++++---- src/systems/a320_systems/src/hydraulic.rs | 30 ++-- src/systems/a320_systems_wasm/src/lib.rs | 5 +- .../systems/src/hydraulic/brakecircuit.rs | 27 ++- src/systems/systems/src/hydraulic/mod.rs | 165 +++++++++--------- src/systems/systems/src/shared/mod.rs | 8 +- 6 files changed, 158 insertions(+), 149 deletions(-) diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index bcacefc025f..2d68229e869 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -116,15 +116,15 @@ fn make_figure(h: &History) -> Figure { Line2D::new(h.name_vector[idx].as_str()) .data(&h.time_vector, &cur_data) .color("blue") - //.marker("x") - //.linestyle("--") + // .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); + // .ylim(-2.0, 2.0); curr_axis = curr_axis.grid(true); all_axis.push(Some(curr_axis)); @@ -133,16 +133,18 @@ fn make_figure(h: &History) -> Figure { Figure::new().subplots(all_axis.len() as u32, 1, all_axis) } -//History class to record a simulation -pub struct History { - time_vector: Vec, //Simulation time starting from 0 - name_vector: Vec, //Name of each var saved - data_vector: Vec>, //Vector data for each var saved +/// 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 { - pub fn new(names: Vec) -> History { + fn new(names: Vec) -> History { History { time_vector: Vec::new(), name_vector: names.clone(), @@ -151,29 +153,29 @@ impl History { } } - //Sets initialisation values of each data before first step - pub fn init(&mut self, start_time: f64, values: Vec) { + /// 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 - pub fn update(&mut self, delta_time: f64, values: Vec) { + /// 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); } - pub fn push_data(&mut self, values: Vec) { + 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 - pub fn _show(self) { + /// 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) @@ -181,7 +183,7 @@ impl History { .y_label("Value"); for cur_data in self.data_vector { - //Here build the 2 by Xsamples 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])); @@ -197,8 +199,8 @@ impl History { Page::single(&v).save("scatter.svg").unwrap(); } - //builds a graph using matplotlib python backend. PYTHON REQUIRED AS WELL AS MATPLOTLIB PACKAGE - pub fn show_matplotlib(&self, figure_title: &str, path: &str) { + /// 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; @@ -217,7 +219,7 @@ impl History { } } -//Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s +/// 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(), @@ -275,7 +277,7 @@ fn green_loop_edp_simulation(path: &str) { ); for x in 0..600 { if x == 50 { - //After 5s + // After 5s assert!(green_loop.pressure() >= Pressure::new::(2850.0)); } if x == 200 { @@ -283,7 +285,7 @@ fn green_loop_edp_simulation(path: &str) { edp1_controller.command_depressurise(); } if x >= 500 { - //Shutdown + 30s + // Shutdown + 30s assert!(green_loop.pressure() <= Pressure::new::(250.0)); } @@ -452,7 +454,7 @@ fn yellow_green_ptu_loop_simulation(path: &str) { engine1.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { - //After 1s powering electric pump + // 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); @@ -464,7 +466,7 @@ fn yellow_green_ptu_loop_simulation(path: &str) { } if x == 110 { - //10s later enabling ptu + // 10s later enabling ptu println!("--------------PTU ENABLED--------------"); assert!(yellow_loop.pressure() >= Pressure::new::(2950.0)); assert!(yellow_loop.reservoir_volume() <= yellow_res_at_start); @@ -476,14 +478,14 @@ fn yellow_green_ptu_loop_simulation(path: &str) { } if x == 300 { - //@30s, ptu should be supplying green loop + // @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 + // @40s enabling edp println!("------------GREEN EDP1 ON------------"); assert!(yellow_loop.pressure() >= Pressure::new::(2600.0)); assert!(green_loop.pressure() >= Pressure::new::(2000.0)); @@ -491,14 +493,14 @@ fn yellow_green_ptu_loop_simulation(path: &str) { } if (500..=600).contains(&x) { - //10s later and during 10s, ptu should stay inactive + // 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 + // @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)); @@ -507,7 +509,7 @@ fn yellow_green_ptu_loop_simulation(path: &str) { } if x == 800 { - //@80s diabling edp and epump + // @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)); @@ -705,12 +707,12 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { engine2.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { - //After 1s powering electric pump + // After 1s powering electric pump epump_controller.command_pressurise(); } if x == 110 { - //10s later enabling edp2 + // 10s later enabling edp2 edp2_controller.command_pressurise(); } @@ -796,7 +798,7 @@ fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { Volume::new::(26.41), Volume::new::(10.0), Volume::new::(3.83), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(1450.), Pressure::new::(1750.), @@ -809,7 +811,7 @@ fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { Volume::new::(10.2), Volume::new::(8.0), Volume::new::(3.3), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(1450.), Pressure::new::(1750.), @@ -822,7 +824,7 @@ fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { Volume::new::(15.85), Volume::new::(8.0), Volume::new::(1.5), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), false, Pressure::new::(1450.), Pressure::new::(1750.), diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index fa649316646..2ac859de89f 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -4,7 +4,7 @@ use uom::si::{ }; use systems::hydraulic::{ - ElectricPump, EngineDrivenPump, HydFluid, HydraulicLoop, HydraulicLoopController, + ElectricPump, EngineDrivenPump, Fluid, HydraulicLoop, HydraulicLoopController, PowerTransferUnit, PowerTransferUnitController, PressureSwitch, PumpController, RamAirTurbine, RamAirTurbineController, }; @@ -64,8 +64,10 @@ impl A320Hydraulic { const MIN_PRESS_PRESSURISED_LO_HYST: f64 = 1450.0; const MIN_PRESS_PRESSURISED_HI_HYST: f64 = 1750.0; - const HYDRAULIC_SIM_TIME_STEP: u64 = 100; // Refresh rate of hydraulic simulation in ms - const ACTUATORS_SIM_TIME_STEP_MULT: u32 = 2; // Refresh rate of actuators as multiplier of hydraulics. 2 means double frequency update + // 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 { @@ -79,7 +81,7 @@ impl A320Hydraulic { Volume::new::(15.85), Volume::new::(8.0), Volume::new::(1.56), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), false, Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), @@ -93,7 +95,7 @@ impl A320Hydraulic { Volume::new::(26.41), Volume::new::(15.), Volume::new::(3.6), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), @@ -107,7 +109,7 @@ impl A320Hydraulic { Volume::new::(19.81), Volume::new::(10.0), Volume::new::(3.6), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(Self::MIN_PRESS_PRESSURISED_LO_HYST), Pressure::new::(Self::MIN_PRESS_PRESSURISED_HI_HYST), @@ -172,7 +174,7 @@ impl A320Hydraulic { engine_fire_overhead: &A320EngineFireOverheadPanel, landing_gear: &LandingGear, ) { - let min_hyd_loop_timestep = Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP); //Hyd Sim rate = 10 Hz + let min_hyd_loop_timestep = Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP_MILLISECONDS); self.total_sim_time_elapsed += context.delta(); @@ -225,8 +227,10 @@ impl A320Hydraulic { // 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_MULT; - let delta_time_physics = min_hyd_loop_timestep / Self::ACTUATORS_SIM_TIME_STEP_MULT; //If X times faster we divide step by X + 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); } @@ -861,7 +865,7 @@ impl A320RamAirTurbineController { // 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 + // Todo get speed from ADIRS && context.indicated_airspeed() > Velocity::new::(100.) } } @@ -1030,7 +1034,7 @@ impl A320HydraulicBrakingLogic { } } - //limiting final values + // 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.); @@ -1290,7 +1294,7 @@ mod tests { } fn get_yellow_brake_accumulator_fluid_volume(&self) -> Volume { - self.hydraulics.braking_circuit_altn.acc_fluid_volume() + self.hydraulics.braking_circuit_altn.accumulator_fluid_volume() } fn is_nws_pin_inserted(&self) -> bool { @@ -1366,7 +1370,7 @@ mod tests { fn run_one_tick(self) -> Self { self.run_waiting_for(Duration::from_millis( - A320Hydraulic::HYDRAULIC_SIM_TIME_STEP, + A320Hydraulic::HYDRAULIC_SIM_TIME_STEP_MILLISECONDS, )) } diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index 3fb9c7486b4..5fea2d9eba7 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -94,7 +94,10 @@ impl A320SimulatorReaderWriter { 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)?, - cargo_door_back_pos: AircraftVariable::from("EXIT OPEN", "Percent", 3)?, //TODO It is the catering door for now + + // 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)?, diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brakecircuit.rs index 91591243d09..fb9b934d673 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brakecircuit.rs @@ -9,7 +9,7 @@ use std::time::Duration; use uom::si::{acceleration::foot_per_second_squared, f64::*, pressure::psi, volume::gallon}; -use super::HydraulicAccumulator; +use super::Accumulator; pub trait Actuator { fn used_volume(&self) -> Volume; @@ -27,7 +27,6 @@ pub struct BrakeActuator { 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.; @@ -99,7 +98,6 @@ impl BrakeActuator { final_delta_position } } - impl Actuator for BrakeActuator { fn used_volume(&self) -> Volume { self.volume_to_actuator_accumulator @@ -131,7 +129,7 @@ pub struct BrakeCircuit { // Brake accumulator variables. Accumulator can have 0 volume if no accumulator has_accumulator: bool, - accumulator: HydraulicAccumulator, + 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 @@ -157,7 +155,7 @@ impl SimulationElement for BrakeCircuit { self.right_brake_pressure().get::(), ); if self.has_accumulator { - writer.write_f64(&self.id_acc_press, self.acc_pressure().get::()); + writer.write_f64(&self.id_acc_press, self.accumulator_pressure().get::()); } } } @@ -171,7 +169,8 @@ impl BrakeCircuit { [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)) - const ACC_PRESSURE_SENSOR_FILTER_TIMECONST: f64 = 0.1; //Time constant of the filter used to measure brake circuit pressure + // 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, @@ -201,7 +200,7 @@ impl BrakeCircuit { pressure_limitation: Pressure::new::(0.0), pressure_limitation_active: false, has_accumulator: has_accu, - accumulator: HydraulicAccumulator::new( + accumulator: Accumulator::new( Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), accumulator_volume, accumulator_fluid_volume_at_init, @@ -312,11 +311,11 @@ impl BrakeCircuit { self.pressure_applied_right } - pub fn acc_pressure(&self) -> Pressure { + pub fn accumulator_pressure(&self) -> Pressure { self.accumulator_fluid_pressure_sensor_filtered } - pub fn acc_fluid_volume(&self) -> Volume { + pub fn accumulator_fluid_volume(&self) -> Volume { self.accumulator.fluid_volume() } @@ -421,7 +420,7 @@ impl AutoBrakeController { mod tests { use super::*; use crate::{ - hydraulic::{HydFluid, HydraulicLoop}, + hydraulic::{Fluid, HydraulicLoop}, simulation::UpdateContext, }; use uom::si::{ @@ -504,7 +503,7 @@ mod tests { brake_actuator.set_position_demand(1.2); let medium_pressure = Pressure::new::(1500.); - //Update position with 1500psi only: should not reach max displacement + // 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!( @@ -735,7 +734,7 @@ mod tests { Volume::new::(26.41), Volume::new::(10.0), Volume::new::(3.83), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(1450.0), Pressure::new::(1750.0), @@ -748,7 +747,7 @@ mod tests { Volume::new::(10.2), Volume::new::(8.0), Volume::new::(3.3), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(1450.0), Pressure::new::(1750.0), @@ -761,7 +760,7 @@ mod tests { Volume::new::(15.85), Volume::new::(8.0), Volume::new::(1.5), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), false, Pressure::new::(1450.0), Pressure::new::(1750.0), diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 1abc1fa3296..07de473bdc0 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -17,31 +17,24 @@ use crate::{engine::Engine, simulation::UpdateContext}; pub mod brakecircuit; use crate::hydraulic::brakecircuit::Actuator; -// Trait common to all hydraulic pumps -// Max gives maximum available volume at that time as if it is a variable displacement -// pump it can be adjusted by pump regulation -// Min will give minimum volume that will be outputed no matter what. example if there is a minimal displacement or -// a fixed displacement (ie. elec pump) 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; } -//////////////////////////////////////////////////////////////////////////////// -// LOOP DEFINITION - INCLUDES RESERVOIR AND ACCUMULATOR -//////////////////////////////////////////////////////////////////////////////// - -//Implements fluid structure. -//TODO update method that can update physic constants from given temperature -//This would change pressure response to volume -pub struct HydFluid { - //temp : thermodynamic_temperature, +// 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 HydFluid { +impl Fluid { pub fn new(bulk: Pressure) -> Self { Self { - //temp:temp, current_bulk: bulk, } } @@ -56,7 +49,6 @@ pub struct PressureSwitch { high_hysteresis_threshold: Pressure, low_hysteresis_threshold: Pressure, } - impl PressureSwitch { pub fn new(high_threshold: Pressure, low_threshold: Pressure) -> Self { Self { @@ -92,7 +84,7 @@ pub struct PowerTransferUnit { last_flow: VolumeRate, } impl PowerTransferUnit { - //Low pass filter to handle flow dynamic: avoids instantaneous flow transient, + // 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; @@ -100,7 +92,7 @@ impl PowerTransferUnit { 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 + // 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; @@ -156,16 +148,16 @@ impl PowerTransferUnit { 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 + // 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 + // 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 + // 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::(); @@ -179,16 +171,16 @@ impl PowerTransferUnit { } else if delta_p.get::() < -500. || (self.is_active_right && delta_p.get::() < -5.) { - //Right sends flow to left + // 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 + // 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 + // 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::(); @@ -223,7 +215,7 @@ pub trait HydraulicLoopController { fn should_open_fire_shutoff_valve(&self) -> bool; } -struct HydraulicAccumulator { +struct Accumulator { total_volume: Volume, gas_init_precharge: Pressure, gas_pressure: Pressure, @@ -235,8 +227,7 @@ struct HydraulicAccumulator { flow_carac: [f64; 10], has_control_valve: bool, } - -impl HydraulicAccumulator { +impl Accumulator { const FLOW_DYNAMIC_LOW_PASS: f64 = 0.7; pub fn new( @@ -250,7 +241,7 @@ impl HydraulicAccumulator { // 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 + // 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 { @@ -333,8 +324,8 @@ pub struct HydraulicLoop { pressure_id: String, reservoir_id: String, fire_valve_id: String, - fluid: HydFluid, - accumulator: HydraulicAccumulator, + fluid: Fluid, + accumulator: Accumulator, connected_to_ptu_left_side: bool, connected_to_ptu_right_side: bool, loop_pressure: Pressure, @@ -345,7 +336,8 @@ pub struct HydraulicLoop { reservoir_volume: Volume, current_delta_vol: Volume, current_flow: VolumeRate, - current_max_flow: VolumeRate, //Current total max flow available from pressure sources + /// 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, @@ -355,10 +347,13 @@ pub struct HydraulicLoop { total_actuators_returned_volume: Volume, } impl HydraulicLoop { - const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1885.0; // Nitrogen PSI - const ACCUMULATOR_MAX_VOLUME: f64 = 0.264; // in gallons + // Nitrogen PSI + const ACCUMULATOR_GAS_PRE_CHARGE_PSI: f64 = 1885.0; + // in gallons + const ACCUMULATOR_MAX_VOLUME_GALLONS: f64 = 0.264; - const STATIC_LEAK_FLOW: f64 = 0.05; //Gallon per s of flow lost to reservoir @ 3000psi + // 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; @@ -377,7 +372,7 @@ impl HydraulicLoop { max_loop_volume: Volume, high_pressure_volume: Volume, reservoir_volume: Volume, - fluid: HydFluid, + fluid: Fluid, has_fire_valve: bool, min_pressure_pressurised_lo_hyst: Pressure, min_pressure_pressurised_hi_hyst: Pressure, @@ -396,9 +391,9 @@ impl HydraulicLoop { ptu_active: false, reservoir_volume, fluid, - accumulator: HydraulicAccumulator::new( - Pressure::new::(Self::ACCUMULATOR_GAS_PRE_CHARGE), - Volume::new::(Self::ACCUMULATOR_MAX_VOLUME), + 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, @@ -575,10 +570,10 @@ impl HydraulicLoop { 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 + // 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 + Self::STATIC_LEAK_FLOW_GALLON_PER_SECOND * context.delta_as_secs_f64() * (self.loop_pressure.get::() - 14.7) / 3000.0, @@ -588,7 +583,7 @@ impl HydraulicLoop { delta_vol -= static_leaks_vol; reservoir_return += static_leaks_vol; - //Updates current delta_vol and reservoir return quantity based on current ptu flows + // 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 @@ -596,17 +591,18 @@ impl HydraulicLoop { .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 + // 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); - delta_vol_max -= delta_loop_vol; //TODO check if we cross the deltaVolMin? + // 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 + // 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.); @@ -626,7 +622,8 @@ impl HydraulicLoop { delta_vol += actual_volume_added_to_pressurise; // Update reservoir - self.reservoir_volume -= actual_volume_added_to_pressurise; //%limit to 0 min? for case of negative added? + // %limit to 0 min? for case of negative added? + self.reservoir_volume -= actual_volume_added_to_pressurise; self.reservoir_volume += reservoir_return; // Update Volumes @@ -638,7 +635,8 @@ impl HydraulicLoop { // Loop Pressure update From Bulk modulus let press_delta = self.delta_pressure_from_delta_volume(delta_vol); self.loop_pressure += press_delta; - self.loop_pressure = self.loop_pressure.max(Pressure::new::(14.7)); //Forcing a min pressure + // 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(); @@ -760,7 +758,8 @@ impl ElectricPump { 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]; - const DISPLACEMENT_DYNAMICS: f64 = 1.0; //1 == No filtering + // 1 == No filtering + const DISPLACEMENT_DYNAMICS: f64 = 1.0; pub fn new(id: &str) -> Self { Self { @@ -785,15 +784,15 @@ impl ElectricPump { line: &HydraulicLoop, controller: &T, ) { - //TODO Simulate speed of pump depending on pump load (flow?/ current?) - //Pump startup/shutdown process + // 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 + // Limiting min and max speed self.rpm = self.rpm.min(Self::NOMINAL_SPEED).max(0.0); self.pump.update(context, line, self.rpm, controller); @@ -825,9 +824,10 @@ pub struct EngineDrivenPump { pump: Pump, } impl EngineDrivenPump { - const LEAP_1A26_MAX_N2_RPM: f64 = 16645.0; //according to the Type Certificate Data Sheet of LEAP 1A26 - //max N2 rpm is 116.5% @ 19391 RPM - //100% @ 16645 RPM + // 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; const PUMP_N2_GEAR_RATIO: f64 = 0.211; const DISPLACEMENT_BREAKPTS: [f64; 9] = [ @@ -835,7 +835,8 @@ impl EngineDrivenPump { ]; const DISPLACEMENT_MAP: [f64; 9] = [2.4, 2.4, 2.4, 2.4, 2.4, 2.0, 0.9, 0.0, 0.0]; - const DISPLACEMENT_DYNAMICS: f64 = 0.95; //0.1 == 90% filtering on max displacement transient + // 0.1 == 90% filtering on max displacement transient + const DISPLACEMENT_DYNAMICS: f64 = 0.95; pub fn new(id: &str) -> Self { Self { @@ -936,7 +937,7 @@ impl WindTurbine { pump_torque += displacement_ratio.max(0.35) * 1. * -self.speed; } pump_torque -= self.speed * 0.05; - //Static air drag of the propeller + // Static air drag of the propeller self.torque_sum += pump_torque; } @@ -1016,7 +1017,7 @@ impl RamAirTurbine { // 1 == no filtering. !!Warning, this will be affected by a different delta time const DISPLACEMENT_DYNAMICS: f64 = 0.2; - //Speed to go from 0 to 1 stow position per sec. 1 means full deploying in 1s + // 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 { @@ -1211,7 +1212,7 @@ mod tests { use super::*; #[test] - //Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s + /// Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s fn green_loop_edp_simulation() { let mut edp1 = engine_driven_pump(); let mut green_loop = hydraulic_loop("GREEN"); @@ -1225,7 +1226,7 @@ mod tests { for x in 0..600 { if x == 50 { - //After 5s + // After 5s assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); } if x == 200 { @@ -1233,7 +1234,7 @@ mod tests { pump_controller.command_depressurise(); } if x >= 500 { - //Shutdown + 30s + // Shutdown + 30s assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } @@ -1275,7 +1276,7 @@ mod tests { } #[test] - //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s + /// Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s fn yellow_loop_epump_simulation() { let mut epump = electric_pump(); let mut yellow_loop = hydraulic_loop("YELLOW"); @@ -1291,7 +1292,7 @@ mod tests { } if x >= 600 { - //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low + // X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); } epump.update(&context, &yellow_loop, &pump_controller); @@ -1325,7 +1326,7 @@ mod tests { } #[test] - //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s + /// Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s fn blue_loop_epump_simulation() { let mut epump = electric_pump(); let mut blue_loop = hydraulic_loop("BLUE"); @@ -1341,7 +1342,7 @@ mod tests { } if x >= 600 { - //X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low + // X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); } epump.update(&context, &blue_loop, &pump_controller); @@ -1375,7 +1376,7 @@ mod tests { } #[test] - //Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s + /// 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(); @@ -1456,9 +1457,9 @@ mod tests { } #[test] - //Runs green edp and yellow epump, checks pressure OK, - //shut green edp off, check drop of pressure and ptu effect - //shut yellow epump, check drop of pressure in both loops + /// Runs green edp and yellow epump, checks pressure OK, + /// shut green edp off, check drop of pressure and ptu effect + /// shut yellow epump, check drop of pressure in both loops fn yellow_green_ptu_loop_simulation() { let mut yellow_loop = hydraulic_loop("YELLOW"); let yellow_loop_controller = @@ -1487,7 +1488,7 @@ mod tests { engine1.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { - //After 1s powering electric pump + // After 1s powering electric pump println!("------------YELLOW EPUMP ON------------"); assert!(yellow_loop.loop_pressure <= Pressure::new::(50.0)); assert!(yellow_loop.reservoir_volume == yellow_res_at_start); @@ -1499,7 +1500,7 @@ mod tests { } if x == 110 { - //10s later enabling ptu + // 10s later enabling ptu println!("--------------PTU ENABLED--------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2950.0)); assert!(yellow_loop.reservoir_volume <= yellow_res_at_start); @@ -1511,14 +1512,14 @@ mod tests { } if x == 300 { - //@30s, ptu should be supplying green loop + // @30s, ptu should be supplying green loop println!("----------PTU SUPPLIES GREEN------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2400.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2400.0)); } if x == 400 { - //@40s enabling edp + // @40s enabling edp println!("------------GREEN EDP1 ON------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); @@ -1526,7 +1527,7 @@ mod tests { } if (500..=600).contains(&x) { - //10s later and during 10s, ptu should stay inactive + // 10s later and during 10s, ptu should stay inactive println!("------------IS PTU ACTIVE??------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); @@ -1534,7 +1535,7 @@ mod tests { } if x == 600 { - //@60s diabling edp and epump + // @60s diabling edp and epump println!("-------------ALL PUMPS OFF------------"); assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); @@ -1543,7 +1544,7 @@ mod tests { } if x == 800 { - //@80s disabling edp and epump + // @80s disabling edp and epump println!("-----------IS PRESSURE OFF?-----------"); assert!(yellow_loop.loop_pressure < Pressure::new::(50.0)); assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); @@ -1615,7 +1616,7 @@ mod tests { Volume::new::(26.41), Volume::new::(10.0), Volume::new::(3.83), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(1450.0), Pressure::new::(1750.0), @@ -1628,7 +1629,7 @@ mod tests { Volume::new::(10.2), Volume::new::(8.0), Volume::new::(3.3), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), true, Pressure::new::(1450.0), Pressure::new::(1750.0), @@ -1641,7 +1642,7 @@ mod tests { Volume::new::(15.85), Volume::new::(8.0), Volume::new::(1.5), - HydFluid::new(Pressure::new::(1450000000.0)), + Fluid::new(Pressure::new::(1450000000.0)), false, Pressure::new::(1450.0), Pressure::new::(1750.0), @@ -1728,7 +1729,7 @@ mod tests { &line, &eng, &engine_driven_pump_controller, - ); //Update 10 times to stabilize displacement + ); // Update 10 times to stabilize displacement edp.update(context, &line, &eng, &engine_driven_pump_controller); edp.delta_vol_max() diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index 3f430aeba09..276cbe81218 100644 --- a/src/systems/systems/src/shared/mod.rs +++ b/src/systems/systems/src/shared/mod.rs @@ -363,13 +363,13 @@ mod interpolation_tests { #[test] fn interpolation_before_first_element_test() { - //We expect to get first element of YS1 + // 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 + // We expect to get last element of YS1 assert!( (interpolation(&XS1, &YS1, 100000000.0) - *YS1.last().unwrap()).abs() < f64::EPSILON ); @@ -377,7 +377,7 @@ mod interpolation_tests { #[test] fn interpolation_first_element_test() { - //Giving first element of X tab we expect first of Y tab + // 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 @@ -386,7 +386,7 @@ mod interpolation_tests { #[test] fn interpolation_last_element_test() { - //Giving last element of X tab we expect last of Y tab + // 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 From 4df646bd9ec97ce7f0e5231b9907bb6c213f8382 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 6 May 2021 13:55:09 +0200 Subject: [PATCH 112/122] refactor: decrease visibility, remove dead code --- src/systems/a320_systems/src/hydraulic.rs | 24 ++-- .../{brakecircuit.rs => brake_circuit.rs} | 107 ++++++++---------- src/systems/systems/src/hydraulic/mod.rs | 75 ++++-------- 3 files changed, 81 insertions(+), 125 deletions(-) rename src/systems/systems/src/hydraulic/{brakecircuit.rs => brake_circuit.rs} (91%) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 2ac859de89f..a78b34ac718 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -16,7 +16,7 @@ use systems::simulation::{ }; use systems::{engine::Engine, landing_gear::LandingGear}; use systems::{ - hydraulic::brakecircuit::BrakeCircuit, shared::DelayedFalseLogicGate, + hydraulic::brake_circuit::BrakeCircuit, shared::DelayedFalseLogicGate, shared::DelayedTrueLogicGate, }; @@ -174,7 +174,8 @@ impl A320Hydraulic { engine_fire_overhead: &A320EngineFireOverheadPanel, landing_gear: &LandingGear, ) { - let min_hyd_loop_timestep = Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP_MILLISECONDS); + let min_hyd_loop_timestep = + Duration::from_millis(Self::HYDRAULIC_SIM_TIME_STEP_MILLISECONDS); self.total_sim_time_elapsed += context.delta(); @@ -230,7 +231,8 @@ impl A320Hydraulic { 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; + 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); } @@ -950,11 +952,11 @@ impl A320HydraulicBrakingLogic { || 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_ena(true); + 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_ena(true); + altn_brk.set_brake_limit_active(true); // If no pilot action, standard park brake pressure limit if !yellow_manual_braking_input { @@ -965,11 +967,11 @@ impl A320HydraulicBrakingLogic { } } else if !self.anti_skid_activated { altn_brk.set_brake_press_limit(Pressure::new::(1160.)); - altn_brk.set_brake_limit_ena(true); + 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_ena(true); + altn_brk.set_brake_limit_active(true); } } @@ -1050,10 +1052,6 @@ impl A320HydraulicBrakingLogic { } impl SimulationElement for A320HydraulicBrakingLogic { - fn accept(&mut self, visitor: &mut T) { - visitor.visit(self); - } - 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"); @@ -1294,7 +1292,9 @@ mod tests { } fn get_yellow_brake_accumulator_fluid_volume(&self) -> Volume { - self.hydraulics.braking_circuit_altn.accumulator_fluid_volume() + self.hydraulics + .braking_circuit_altn + .accumulator_fluid_volume() } fn is_nws_pin_inserted(&self) -> bool { diff --git a/src/systems/systems/src/hydraulic/brakecircuit.rs b/src/systems/systems/src/hydraulic/brake_circuit.rs similarity index 91% rename from src/systems/systems/src/hydraulic/brakecircuit.rs rename to src/systems/systems/src/hydraulic/brake_circuit.rs index fb9b934d673..a8be8a769ce 100644 --- a/src/systems/systems/src/hydraulic/brakecircuit.rs +++ b/src/systems/systems/src/hydraulic/brake_circuit.rs @@ -1,6 +1,6 @@ use crate::{ hydraulic::HydraulicLoop, - simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext}, + simulation::{SimulationElement, SimulatorWriter, UpdateContext}, }; use std::f64::consts::E; @@ -16,7 +16,7 @@ pub trait Actuator { fn reservoir_return(&self) -> Volume; } -pub struct BrakeActuator { +struct BrakeActuator { total_displacement: Volume, base_speed: f64, @@ -32,7 +32,7 @@ impl BrakeActuator { const MIN_PRESSURE_ALLOWED_TO_MOVE_ACTUATOR_PSI: f64 = 50.; const PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI: f64 = 3100.; - pub fn new(total_displacement: Volume) -> Self { + fn new(total_displacement: Volume) -> Self { Self { total_displacement, base_speed: BrakeActuator::ACTUATOR_BASE_SPEED, @@ -43,7 +43,7 @@ impl BrakeActuator { } } - pub fn set_position_demand(&mut self, required_position: f64) { + fn set_position_demand(&mut self, required_position: f64) { self.required_position = required_position; } @@ -57,11 +57,11 @@ impl BrakeActuator { } } - pub fn get_applied_brake_pressure(&self) -> Pressure { + fn get_applied_brake_pressure(&self) -> Pressure { Pressure::new::(Self::PRESSURE_FOR_MAX_BRAKE_DEFLECTION_PSI) * self.current_position } - pub fn update(&mut self, context: &UpdateContext, received_pressure: Pressure) { + fn update(&mut self, context: &UpdateContext, received_pressure: Pressure) { let final_delta_position = self.update_position(context, received_pressure); if final_delta_position > 0. { @@ -71,7 +71,7 @@ impl BrakeActuator { } } - pub fn reset_accumulators(&mut self) { + fn reset_accumulators(&mut self) { self.volume_to_actuator_accumulator = Volume::new::(0.); self.volume_to_res_accumulator = Volume::new::(0.); } @@ -107,9 +107,9 @@ impl Actuator for BrakeActuator { } } -// 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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). -// So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position +/// 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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). +/// So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position pub struct BrakeCircuit { _id: String, id_left_press: String, @@ -127,39 +127,18 @@ pub struct BrakeCircuit { pressure_limitation: Pressure, pressure_limitation_active: bool, - // Brake accumulator variables. Accumulator can have 0 volume if no accumulator + /// 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 + /// 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 + /// Fluid pressure in brake circuit filtered for cockpit gauges accumulator_fluid_pressure_sensor_filtered: Pressure, } - -impl SimulationElement for BrakeCircuit { - fn accept(&mut self, visitor: &mut T) { - visitor.visit(self); - } - - 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::()); - } - } -} - impl BrakeCircuit { const ACCUMULATOR_GAS_PRE_CHARGE: f64 = 1000.0; // Nitrogen PSI const ACCUMULATOR_PRESS_BREAKPTS: [f64; 10] = [ @@ -220,7 +199,7 @@ impl BrakeCircuit { self.pressure_limitation = pressure_limit; } - pub fn set_brake_limit_ena(&mut self, is_pressure_limit_active: bool) { + pub fn set_brake_limit_active(&mut self, is_pressure_limit_active: bool) { self.pressure_limitation_active = is_pressure_limit_active; } @@ -254,8 +233,8 @@ impl BrakeCircuit { self.update_brake_actuators(context, actual_pressure_available); - let delta_vol = self.left_brake_actuator.used_volume() - + self.right_brake_actuator.used_volume(); + 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.); @@ -300,6 +279,7 @@ impl BrakeCircuit { 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); } @@ -307,11 +287,12 @@ impl BrakeCircuit { pub fn left_brake_pressure(&self) -> Pressure { self.pressure_applied_left } + pub fn right_brake_pressure(&self) -> Pressure { self.pressure_applied_right } - pub fn accumulator_pressure(&self) -> Pressure { + fn accumulator_pressure(&self) -> Pressure { self.accumulator_fluid_pressure_sensor_filtered } @@ -324,7 +305,6 @@ impl BrakeCircuit { self.volume_to_actuator_accumulator = Volume::new::(0.); } } - impl Actuator for BrakeCircuit { fn used_volume(&self) -> Volume { self.volume_to_actuator_accumulator @@ -333,8 +313,20 @@ impl Actuator for BrakeCircuit { 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::()); + } + } +} -pub struct AutoBrakeController { +struct AutoBrakeController { accel_targets: Vec, num_of_modes: usize, @@ -347,11 +339,10 @@ pub struct AutoBrakeController { current_integral_term: f64, // Controller brake demand to satisfy autobrake mode [0:1] - current_brake_dmnd: f64, + current_brake_demand: f64, is_enabled: bool, } - impl AutoBrakeController { const LONG_ACC_FILTER_TIMECONST: f64 = 0.1; @@ -359,7 +350,7 @@ impl AutoBrakeController { const CONTROLLER_I_GAIN: f64 = 0.001; const CONTROLLER_D_GAIN: f64 = 0.01; - pub fn new(accel_targets: Vec) -> AutoBrakeController { + fn new(accel_targets: Vec) -> AutoBrakeController { let num = accel_targets.len(); assert!(num > 0); AutoBrakeController { @@ -370,12 +361,12 @@ impl AutoBrakeController { current_accel_error: Acceleration::new::(0.0), accel_error_prev: Acceleration::new::(0.0), current_integral_term: 0., - current_brake_dmnd: 0., + current_brake_demand: 0., is_enabled: false, } } - pub fn update(&mut self, delta_time: &Duration, context: &UpdateContext) { + fn update(&mut self, delta_time: &Duration, context: &UpdateContext) { self.current_filtered_accel = self.current_filtered_accel + (context.long_accel() - self.current_filtered_accel) * (1. @@ -396,23 +387,19 @@ impl AutoBrakeController { * AutoBrakeController::CONTROLLER_I_GAIN; let current_brake_dmnd = pterm + self.current_integral_term + dterm; - self.current_brake_dmnd = current_brake_dmnd.min(1.).max(0.); + self.current_brake_demand = current_brake_dmnd.min(1.).max(0.); } else { - self.current_brake_dmnd = 0.0; + self.current_brake_demand = 0.0; self.current_integral_term = 0.0; } } - pub fn brake_command(&self) -> f64 { - self.current_brake_dmnd + fn brake_command(&self) -> f64 { + self.current_brake_demand } - pub fn set_mode(&mut self, selected_mode: &usize) { - self.current_selected_mode = *selected_mode.min(&self.num_of_modes); - } - - pub fn set_enable(&mut self, ena: bool) { - self.is_enabled = ena; + fn enable(&mut self) { + self.is_enabled = true; } } @@ -552,9 +539,7 @@ mod tests { < 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.fluid_volume() == Volume::new::(0.0)); assert!(brake_circuit_unprimed.accumulator.gas_volume == init_max_vol); let brake_circuit_primed = BrakeCircuit::new( @@ -691,7 +676,7 @@ mod tests { 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_ena(true); + 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 @@ -719,7 +704,7 @@ mod tests { controller.update(&context.delta(), &context); assert!(controller.brake_command() <= 0.0); - controller.set_enable(true); + controller.enable(); controller.update(&context.delta(), &context); assert!(controller.brake_command() >= 0.0); } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 07de473bdc0..702270b0aab 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -14,8 +14,8 @@ use crate::shared::interpolation; use crate::simulation::{SimulationElement, SimulationElementVisitor, SimulatorWriter}; use crate::{engine::Engine, simulation::UpdateContext}; -pub mod brakecircuit; -use crate::hydraulic::brakecircuit::Actuator; +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 @@ -34,9 +34,7 @@ pub struct Fluid { } impl Fluid { pub fn new(bulk: Pressure) -> Self { - Self { - current_bulk: bulk, - } + Self { current_bulk: bulk } } pub fn bulk_mod(&self) -> Pressure { @@ -111,10 +109,6 @@ impl PowerTransferUnit { self.last_flow } - pub fn is_active(&self) -> bool { - self.is_active_right || self.is_active_left - } - pub fn is_enabled(&self) -> bool { self.is_enabled } @@ -198,10 +192,7 @@ 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_f64("HYD_PTU_MOTOR_FLOW", self.flow().get::()); writer.write_bool("HYD_PTU_VALVE_OPENED", self.is_enabled()); } } @@ -230,7 +221,7 @@ struct Accumulator { impl Accumulator { const FLOW_DYNAMIC_LOW_PASS: f64 = 0.7; - pub fn new( + fn new( gas_precharge: Pressure, total_volume: Volume, fluid_vol_at_init: Volume, @@ -296,7 +287,7 @@ impl Accumulator { (self.gas_init_precharge * self.total_volume) / (self.total_volume - self.fluid_volume); } - pub fn get_delta_vol(&mut self, required_delta_vol: Volume) -> 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); @@ -312,11 +303,11 @@ impl Accumulator { volume_from_acc } - pub fn fluid_volume(&self) -> Volume { + fn fluid_volume(&self) -> Volume { self.fluid_volume } - pub fn raw_gas_press(&self) -> Pressure { + fn raw_gas_press(&self) -> Pressure { self.gas_pressure } } @@ -448,21 +439,13 @@ impl HydraulicLoop { self.accumulator.gas_volume } - pub fn get_usable_reservoir_fluid(&self, amount: Volume) -> Volume { - let mut drawn = amount; - if amount > self.reservoir_volume { - drawn = self.reservoir_volume; - } - drawn - } - 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 - pub fn get_usable_reservoir_flow(&self, amount: VolumeRate, delta_time: Time) -> VolumeRate { + /// 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; @@ -472,16 +455,15 @@ impl HydraulicLoop { drawn } - // Method to update pressure of a loop. The more delta volume is added, the more pressure rises - // directly from bulk modulus equation - pub fn delta_pressure_from_delta_volume(&self, delta_vol: Volume) -> Pressure { + /// 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 - pub fn vol_to_target(&self, target_press: Pressure) -> Volume { - (target_press - self.loop_pressure) * (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 { @@ -655,10 +637,7 @@ impl HydraulicLoop { 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::(), - ); + 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()); } @@ -798,10 +777,6 @@ impl ElectricPump { self.pump.update(context, line, self.rpm, controller); self.is_active = controller.should_pressurise(); } - - pub fn is_active(&self) -> bool { - self.is_active - } } impl PressureSource for ElectricPump { fn delta_vol_max(&self) -> Volume { @@ -813,7 +788,7 @@ impl PressureSource for ElectricPump { } impl SimulationElement for ElectricPump { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool(&self.active_id, self.is_active()); + writer.write_bool(&self.active_id, self.is_active); } } @@ -863,10 +838,6 @@ impl EngineDrivenPump { self.pump.update(context, line, pump_rpm, controller); self.is_active = controller.should_pressurise(); } - - fn is_active(&self) -> bool { - self.is_active - } } impl PressureSource for EngineDrivenPump { fn delta_vol_min(&self) -> Volume { @@ -878,11 +849,11 @@ impl PressureSource for EngineDrivenPump { } impl SimulationElement for EngineDrivenPump { fn write(&self, writer: &mut SimulatorWriter) { - writer.write_bool(&self.active_id, self.is_active()); + writer.write_bool(&self.active_id, self.is_active); } } -pub struct WindTurbine { +struct WindTurbine { position: f64, speed: f64, acceleration: f64, @@ -899,7 +870,7 @@ impl WindTurbine { ]; const PROP_ALPHA_MAP: [f64; 9] = [45., 45., 45., 45., 35., 25., 1., 1., 1.]; - pub fn new() -> Self { + fn new() -> Self { Self { position: Self::STOWED_ANGLE, speed: 0., @@ -909,7 +880,7 @@ impl WindTurbine { } } - pub fn rpm(&self) -> f64 { + fn rpm(&self) -> f64 { self.rpm } @@ -953,7 +924,7 @@ impl WindTurbine { self.torque_sum = 0.; } - pub fn update( + fn update( &mut self, delta_time: &Duration, indicated_speed: &Velocity, From 7208b4bdb6f0c14bb68c860b28b483bf8e18d880 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 6 May 2021 14:45:31 +0200 Subject: [PATCH 113/122] refactor: only compile AutoBrakeController in test --- src/systems/systems/src/hydraulic/brake_circuit.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/systems/systems/src/hydraulic/brake_circuit.rs b/src/systems/systems/src/hydraulic/brake_circuit.rs index a8be8a769ce..c972a8b273e 100644 --- a/src/systems/systems/src/hydraulic/brake_circuit.rs +++ b/src/systems/systems/src/hydraulic/brake_circuit.rs @@ -5,9 +5,13 @@ use crate::{ use std::f64::consts::E; use std::string::String; + +#[cfg(test)] use std::time::Duration; +#[cfg(test)] +use uom::si::{acceleration::foot_per_second_squared}; -use uom::si::{acceleration::foot_per_second_squared, f64::*, pressure::psi, volume::gallon}; +use uom::si::{f64::*, pressure::psi, volume::gallon}; use super::Accumulator; @@ -326,6 +330,7 @@ impl SimulationElement for BrakeCircuit { } } +#[cfg(test)] struct AutoBrakeController { accel_targets: Vec, num_of_modes: usize, @@ -343,6 +348,7 @@ struct AutoBrakeController { is_enabled: bool, } +#[cfg(test)] impl AutoBrakeController { const LONG_ACC_FILTER_TIMECONST: f64 = 0.1; @@ -394,10 +400,12 @@ impl AutoBrakeController { } } + #[cfg(test)] fn brake_command(&self) -> f64 { self.current_brake_demand } + #[cfg(test)] fn enable(&mut self) { self.is_enabled = true; } From 4a080a9f40022b11ccf9f12e53d9e027f8c56d50 Mon Sep 17 00:00:00 2001 From: David Walschots Date: Thu, 6 May 2021 15:23:23 +0200 Subject: [PATCH 114/122] refactor: decrease visibility --- src/systems/a320_systems/src/hydraulic.rs | 40 +++++++++++++---------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index a78b34ac718..e4adc1a93dc 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -883,7 +883,7 @@ impl SimulationElement for A320RamAirTurbineController { } } -pub struct A320HydraulicBrakingLogic { +struct A320HydraulicBrakingLogic { parking_brake_demand: bool, weight_on_wheels: bool, is_gear_lever_down: bool, @@ -898,7 +898,7 @@ pub struct A320HydraulicBrakingLogic { anti_skid_activated: bool, autobrakes_setting: u8, } -// Implements brakes computers logic +/// 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 @@ -911,17 +911,22 @@ impl A320HydraulicBrakingLogic { const AUTOBRAKE_GEAR_RETRACTION_DURATION_S: f64 = 3.; - pub fn new() -> A320HydraulicBrakingLogic { + fn new() -> A320HydraulicBrakingLogic { A320HydraulicBrakingLogic { - parking_brake_demand: true, // Position of parking brake lever + // 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, - left_brake_green_output: 0.0, // Actual command sent to left green circuit - left_brake_yellow_output: 1.0, // Actual command sent to left yellow circuit. Init 1 as considering park brake on on init - right_brake_green_output: 0.0, // Actual command sent to right green circuit - right_brake_yellow_output: 1.0, // Actual command sent to right yellow circuit. Init 1 as considering park brake on on init + // 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), @@ -942,7 +947,7 @@ impl A320HydraulicBrakingLogic { } } - pub fn update_brake_pressure_limitation( + fn update_brake_pressure_limitation( &mut self, norm_brk: &mut BrakeCircuit, altn_brk: &mut BrakeCircuit, @@ -975,8 +980,8 @@ impl A320HydraulicBrakingLogic { } } - // Updates final brake demands per hydraulic loop based on pilot pedal demands - pub fn update_brake_demands( + /// Updates final brake demands per hydraulic loop based on pilot pedal demands + fn update_brake_demands( &mut self, context: &UpdateContext, green_loop: &HydraulicLoop, @@ -998,8 +1003,9 @@ impl A320HydraulicBrakingLogic { self.left_brake_yellow_output = 0.; self.right_brake_yellow_output = 0.; } else { - self.left_brake_green_output = 0.2; // Slight brake pressure to stop the spinning wheels - self.right_brake_green_output = 0.2; // Slight brake pressure to stop the spinning wheels + // Slight brake pressure to stop the spinning wheels + self.left_brake_green_output = 0.2; + self.right_brake_green_output = 0.2; } } else { let green_used_for_brakes = self.normal_brakes_available @@ -1043,7 +1049,7 @@ impl A320HydraulicBrakingLogic { self.right_brake_green_output = self.right_brake_green_output.min(1.).max(0.); } - pub fn send_brake_demands(&mut self, norm: &mut BrakeCircuit, altn: &mut BrakeCircuit) { + 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); @@ -1112,7 +1118,7 @@ impl PushbackTug { } } - pub fn update(&mut self) { + 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 { @@ -1217,12 +1223,12 @@ impl SimulationElement for A320HydraulicOverheadPanel { } } -pub struct A320EngineFireOverheadPanel { +pub(super) struct A320EngineFireOverheadPanel { eng1_fire_pb: FirePushButton, eng2_fire_pb: FirePushButton, } impl A320EngineFireOverheadPanel { - pub fn new() -> Self { + pub(super) fn new() -> Self { Self { eng1_fire_pb: FirePushButton::new("ENG1"), eng2_fire_pb: FirePushButton::new("ENG2"), From 646f603ee7927c1e3eac6b13a376adf990b5bc28 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 6 May 2021 16:23:39 +0200 Subject: [PATCH 115/122] Updated in flight brake logic Yellow value would have be frozen until end of autobrake by a bold pilot braking while rotating --- src/systems/a320_systems/src/hydraulic.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index e2a57b30d50..ea01d5cb603 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -989,12 +989,14 @@ impl A320HydraulicBrakingLogic { if self.should_disable_auto_brake_when_retracting.output() { self.left_brake_green_output = 0.; self.right_brake_green_output = 0.; - self.left_brake_yellow_output = 0.; - self.right_brake_yellow_output = 0.; } else { - self.left_brake_green_output = 0.2; // Slight brake pressure to stop the spinning wheels - self.right_brake_green_output = 0.2; // Slight brake pressure to stop the spinning wheels + // 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 From 63359e6ac53e46afa6776374c9111d2480a31c88 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 6 May 2021 16:46:23 +0200 Subject: [PATCH 116/122] Removed number of modes of autobrake --- src/systems/systems/src/hydraulic/brake_circuit.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/systems/systems/src/hydraulic/brake_circuit.rs b/src/systems/systems/src/hydraulic/brake_circuit.rs index c972a8b273e..28b21845e17 100644 --- a/src/systems/systems/src/hydraulic/brake_circuit.rs +++ b/src/systems/systems/src/hydraulic/brake_circuit.rs @@ -9,7 +9,7 @@ use std::string::String; #[cfg(test)] use std::time::Duration; #[cfg(test)] -use uom::si::{acceleration::foot_per_second_squared}; +use uom::si::acceleration::foot_per_second_squared; use uom::si::{f64::*, pressure::psi, volume::gallon}; @@ -333,7 +333,6 @@ impl SimulationElement for BrakeCircuit { #[cfg(test)] struct AutoBrakeController { accel_targets: Vec, - num_of_modes: usize, current_selected_mode: usize, @@ -361,7 +360,6 @@ impl AutoBrakeController { assert!(num > 0); AutoBrakeController { accel_targets, - num_of_modes: num, current_selected_mode: 0, current_filtered_accel: Acceleration::new::(0.0), current_accel_error: Acceleration::new::(0.0), From 08a1fd4b39d9a246802e07849327781b819e10f1 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Thu, 6 May 2021 17:09:56 +0200 Subject: [PATCH 117/122] Auto brake sent to a git feature branch --- .github/CHANGELOG.md | 1 + .../systems/src/hydraulic/brake_circuit.rs | 108 +----------------- src/systems/systems/src/hydraulic/mod.rs | 2 +- 3 files changed, 5 insertions(+), 106 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 56931988ee0..2ee1c6d2459 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -6,6 +6,7 @@ ## 0.7.0 1. [HYD] First hydraulics systems integration - @crocket63 (crocket) + First building block, more to come. Hydraulics do not impact the sim YET 1. [MCDU] Fixed input and display issues on PERF/W&B and INIT pages - @felixharnstrom (Felix Härnström) 1. [MCDU] Progress page only shows GPS Primary when it should - @Username23-23 (NONAmr2433 #8777) 1. [ND] Add VOR/ADF needles to ILS arc display - @tracernz (Mike) diff --git a/src/systems/systems/src/hydraulic/brake_circuit.rs b/src/systems/systems/src/hydraulic/brake_circuit.rs index 28b21845e17..a57bf44ec34 100644 --- a/src/systems/systems/src/hydraulic/brake_circuit.rs +++ b/src/systems/systems/src/hydraulic/brake_circuit.rs @@ -6,11 +6,6 @@ use crate::{ use std::f64::consts::E; use std::string::String; -#[cfg(test)] -use std::time::Duration; -#[cfg(test)] -use uom::si::acceleration::foot_per_second_squared; - use uom::si::{f64::*, pressure::psi, volume::gallon}; use super::Accumulator; @@ -112,8 +107,8 @@ impl Actuator for BrakeActuator { } /// 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 position from 0 to 1 and take conrresponding fluid volume (vol = max_displacement * brake_position). -/// So it's fairly simplified as we just end up with brake pressure = hyd_pressure * current_position +/// 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, @@ -330,85 +325,6 @@ impl SimulationElement for BrakeCircuit { } } -#[cfg(test)] -struct AutoBrakeController { - accel_targets: Vec, - - current_selected_mode: usize, - - pub current_filtered_accel: Acceleration, - - pub current_accel_error: Acceleration, - accel_error_prev: Acceleration, - current_integral_term: f64, - - // Controller brake demand to satisfy autobrake mode [0:1] - current_brake_demand: f64, - - is_enabled: bool, -} -#[cfg(test)] -impl AutoBrakeController { - const LONG_ACC_FILTER_TIMECONST: f64 = 0.1; - - const CONTROLLER_P_GAIN: f64 = 0.05; - const CONTROLLER_I_GAIN: f64 = 0.001; - const CONTROLLER_D_GAIN: f64 = 0.01; - - fn new(accel_targets: Vec) -> AutoBrakeController { - let num = accel_targets.len(); - assert!(num > 0); - AutoBrakeController { - accel_targets, - current_selected_mode: 0, - current_filtered_accel: Acceleration::new::(0.0), - current_accel_error: Acceleration::new::(0.0), - accel_error_prev: Acceleration::new::(0.0), - current_integral_term: 0., - current_brake_demand: 0., - is_enabled: false, - } - } - - fn update(&mut self, delta_time: &Duration, context: &UpdateContext) { - self.current_filtered_accel = self.current_filtered_accel - + (context.long_accel() - self.current_filtered_accel) - * (1. - - E.powf( - -delta_time.as_secs_f64() / AutoBrakeController::LONG_ACC_FILTER_TIMECONST, - )); - - self.current_accel_error = - self.current_filtered_accel - self.accel_targets[self.current_selected_mode]; - - if self.is_enabled && context.is_on_ground() { - let pterm = self.current_accel_error.get::() - * AutoBrakeController::CONTROLLER_P_GAIN; - let dterm = (self.current_accel_error - self.accel_error_prev) - .get::() - * AutoBrakeController::CONTROLLER_D_GAIN; - self.current_integral_term += self.current_accel_error.get::() - * AutoBrakeController::CONTROLLER_I_GAIN; - - let current_brake_dmnd = pterm + self.current_integral_term + dterm; - self.current_brake_demand = current_brake_dmnd.min(1.).max(0.); - } else { - self.current_brake_demand = 0.0; - self.current_integral_term = 0.0; - } - } - - #[cfg(test)] - fn brake_command(&self) -> f64 { - self.current_brake_demand - } - - #[cfg(test)] - fn enable(&mut self) { - self.is_enabled = true; - } -} - #[cfg(test)] mod tests { use super::*; @@ -416,6 +332,7 @@ mod tests { hydraulic::{Fluid, HydraulicLoop}, simulation::UpdateContext, }; + use std::time::Duration; use uom::si::{ acceleration::foot_per_second_squared, length::foot, @@ -696,25 +613,6 @@ mod tests { assert!(brake_circuit_primed.right_brake_pressure() <= pressure_limit); } - #[test] - fn auto_brake_controller() { - let mut controller = AutoBrakeController::new(vec![ - Acceleration::new::(-1.5), - Acceleration::new::(-3.), - Acceleration::new::(-15.), - ]); - let context = context(Duration::from_secs_f64(0.)); - - assert!(controller.brake_command() <= 0.0); - - controller.update(&context.delta(), &context); - assert!(controller.brake_command() <= 0.0); - - controller.enable(); - controller.update(&context.delta(), &context); - assert!(controller.brake_command() >= 0.0); - } - fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { match loop_color { "GREEN" => HydraulicLoop::new( diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 702270b0aab..4a9411b8519 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -985,7 +985,7 @@ impl RamAirTurbine { ]; 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. !!Warning, this will be affected by a different delta time + // 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 From 5a16e685556c74bd36e6f50417946ff5375ac192 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 7 May 2021 14:58:46 +0200 Subject: [PATCH 118/122] Refactored Engine so it computes pump speeds --- .../src/main.rs | 42 +---- src/systems/a320_systems/src/hydraulic.rs | 171 ++++++++++++------ src/systems/a320_systems/src/lib.rs | 10 +- src/systems/systems/src/engine/leap_engine.rs | 62 +++++++ src/systems/systems/src/engine/mod.rs | 30 +-- src/systems/systems/src/hydraulic/mod.rs | 64 +++---- 6 files changed, 223 insertions(+), 156 deletions(-) create mode 100644 src/systems/systems/src/engine/leap_engine.rs diff --git a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs index 2d68229e869..4bc6c0afa3e 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/src/main.rs +++ b/src/systems/a320_hydraulic_simulation_graphs/src/main.rs @@ -1,4 +1,3 @@ -use systems::engine::Engine; use systems::simulation::UpdateContext; pub use systems::hydraulic::*; @@ -9,7 +8,6 @@ use uom::si::{ f64::*, length::foot, pressure::{pascal, psi}, - ratio::percent, thermodynamic_temperature::degree_celsius, velocity::knot, volume::{gallon, liter}, @@ -229,7 +227,7 @@ fn green_loop_edp_simulation(path: &str) { ]; let mut green_loop_history = History::new(green_loop_var_names); - let edp1_var_names = vec!["Delta Vol Max".to_string(), "n2 ratio".to_string()]; + 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(); @@ -238,8 +236,7 @@ fn green_loop_edp_simulation(path: &str) { let mut green_loop = hydraulic_loop("GREEN"); let green_loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - let init_n2 = Ratio::new::(55.0); - let engine1 = engine(init_n2); + let edp_rpm = 3000.; let context = context(Duration::from_millis(100)); let green_acc_var_names = vec![ @@ -259,13 +256,7 @@ fn green_loop_edp_simulation(path: &str) { green_loop.current_flow().get::(), ], ); - edp1_history.init( - 0.0, - vec![ - edp1.delta_vol_max().get::(), - engine1.corrected_n2.get::() as f64, - ], - ); + edp1_history.init(0.0, vec![edp1.delta_vol_max().get::(), edp_rpm]); accu_green_history.init( 0.0, vec![ @@ -289,7 +280,7 @@ fn green_loop_edp_simulation(path: &str) { assert!(green_loop.pressure() <= Pressure::new::(250.0)); } - edp1.update(&context, &green_loop, &engine1, &edp1_controller); + edp1.update(&context, &green_loop, edp_rpm, &edp1_controller); green_loop.update( &context, Vec::new(), @@ -335,10 +326,7 @@ fn green_loop_edp_simulation(path: &str) { ); edp1_history.update( context.delta_as_secs_f64(), - vec![ - edp1.delta_vol_max().get::(), - engine1.corrected_n2.get::() as f64, - ], + vec![edp1.delta_vol_max().get::(), edp_rpm], ); accu_green_history.update( context.delta_as_secs_f64(), @@ -398,8 +386,6 @@ fn yellow_green_ptu_loop_simulation(path: &str) { let mut edp1 = engine_driven_pump(); let mut edp1_controller = TestPumpController::commanding_depressurise(); - let mut engine1 = engine(Ratio::new::(0.0)); - let mut green_loop = hydraulic_loop("GREEN"); let loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); @@ -451,7 +437,7 @@ fn yellow_green_ptu_loop_simulation(path: &str) { let yellow_res_at_start = yellow_loop.reservoir_volume(); let green_res_at_start = green_loop.reservoir_volume(); - engine1.corrected_n2 = Ratio::new::(100.0); + let edp_rpm = 3300.; for x in 0..800 { if x == 10 { // After 1s powering electric pump @@ -525,7 +511,7 @@ fn yellow_green_ptu_loop_simulation(path: &str) { } ptu.update(&green_loop, &yellow_loop, &ptu_controller); - edp1.update(&context, &green_loop, &engine1, &edp1_controller); + edp1.update(&context, &green_loop, edp_rpm, &edp1_controller); epump.update(&context, &yellow_loop, &epump_controller); yellow_loop.update( @@ -596,7 +582,7 @@ fn yellow_green_ptu_loop_simulation(path: &str) { yellow_loop.max_volume().get::() ); println!("---PSI GREEN: {}", green_loop.pressure().get::()); - println!("---N2 GREEN: {}", engine1.corrected_n2.get::()); + println!("---EDP RPM GREEN: {}", edp_rpm); println!( "---Priming State: {}/{}", green_loop.loop_fluid_volume().get::(), @@ -654,7 +640,7 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { let mut edp2 = engine_driven_pump(); let mut edp2_controller = TestPumpController::commanding_depressurise(); - let mut engine2 = engine(Ratio::new::(100.0)); + let edp_rpm = 3300.; let mut green_loop = hydraulic_loop("GREEN"); @@ -704,7 +690,6 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { ], ); - engine2.corrected_n2 = Ratio::new::(100.0); for x in 0..800 { if x == 10 { // After 1s powering electric pump @@ -720,7 +705,7 @@ fn yellow_epump_plus_edp2_with_ptu(path: &str) { println!("Gpress={}", green_loop.pressure().get::()); } ptu.update(&green_loop, &yellow_loop, &ptu_controller); - edp2.update(&context, &yellow_loop, &engine2, &edp2_controller); + edp2.update(&context, &yellow_loop, edp_rpm, &edp2_controller); epump.update(&context, &yellow_loop, &epump_controller); yellow_loop.update( @@ -840,13 +825,6 @@ fn engine_driven_pump() -> EngineDrivenPump { EngineDrivenPump::new("DEFAULT") } -fn engine(n2: Ratio) -> Engine { - let mut engine = Engine::new(1); - engine.corrected_n2 = n2; - - engine -} - fn context(delta_time: Duration) -> UpdateContext { UpdateContext::new( delta_time, diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 1a1e4c54118..041d31247c1 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1,6 +1,7 @@ use std::time::Duration; use uom::si::{ - f64::*, pressure::pascal, pressure::psi, ratio::percent, velocity::knot, volume::gallon, + angular_velocity::revolution_per_minute, f64::*, pressure::pascal, pressure::psi, + ratio::percent, velocity::knot, volume::gallon, }; use systems::hydraulic::{ @@ -14,6 +15,7 @@ use systems::overhead::{ use systems::simulation::{ SimulationElement, SimulationElementVisitor, SimulatorReader, SimulatorWriter, UpdateContext, }; + use systems::{engine::Engine, landing_gear::LandingGear}; use systems::{ hydraulic::brake_circuit::BrakeCircuit, shared::DelayedFalseLogicGate, @@ -165,11 +167,11 @@ impl A320Hydraulic { } } - pub(super) fn update( + pub(super) fn update( &mut self, context: &UpdateContext, - engine1: &Engine, - engine2: &Engine, + engine1: &T, + engine2: &T, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, landing_gear: &LandingGear, @@ -321,11 +323,11 @@ impl A320Hydraulic { 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( + fn update_fixed_step( &mut self, context: &UpdateContext, - engine1: &Engine, - engine2: &Engine, + engine1: &T, + engine2: &T, overhead_panel: &A320HydraulicOverheadPanel, engine_fire_overhead: &A320EngineFireOverheadPanel, landing_gear: &LandingGear, @@ -365,13 +367,16 @@ impl A320Hydraulic { 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, + engine1 + .hydraulic_pump_output_speed() + .get::(), &self.engine_driven_pump_1_controller, ); @@ -381,21 +386,24 @@ impl A320Hydraulic { 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, + 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.corrected_n2(), - engine2.corrected_n2(), + engine1.oil_pressure(), + engine2.oil_pressure(), ); self.blue_electric_pump.update( context, @@ -538,12 +546,17 @@ impl A320EngineDrivenPumpController { } } - fn update_low_pressure_state(&mut self, engine_n2: Ratio, pressure_switch_state: bool) { + 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.; - // Faking low engine oil pressure using N2 slow level. TODO Get real oil pressure (treshold is 18psi) - let faked_is_engine_low_oil_pressure = engine_n2.get::() < 25.; + // Engine off state uses oil pressure threshold (treshold is 18psi) + let is_engine_low_oil_pressure = engine_oil_pressure.get::() < 18.; // 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() @@ -551,7 +564,7 @@ impl A320EngineDrivenPumpController { // Fault inhibited if on ground AND engine oil pressure is low (11KS1 elec relay) self.has_pressure_low_fault = - self.is_pressure_low && (!faked_is_engine_low_oil_pressure || !self.weight_on_wheels); + self.is_pressure_low && (!is_engine_low_oil_pressure || !self.weight_on_wheels); } fn update( @@ -559,6 +572,7 @@ impl A320EngineDrivenPumpController { 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) @@ -571,7 +585,7 @@ impl A320EngineDrivenPumpController { self.should_pressurise = false; } - self.update_low_pressure_state(engine_n2, pressure_switch_state); + self.update_low_pressure_state(engine_n2, engine_oil_pressure, pressure_switch_state); } fn has_pressure_low_fault(&self) -> bool { @@ -624,8 +638,8 @@ impl A320BlueElectricPumpController { &mut self, overhead_panel: &A320HydraulicOverheadPanel, pressure_switch_state: bool, - engine1_n2: Ratio, - engine2_n2: Ratio, + engine1_oil_pressure: Pressure, + engine2_oil_pressure: Pressure, ) { if overhead_panel.blue_epump_push_button.is_auto() { if self.engine_1_master_on @@ -643,8 +657,8 @@ impl A320BlueElectricPumpController { self.update_low_pressure_state( overhead_panel, pressure_switch_state, - engine1_n2, - engine2_n2, + engine1_oil_pressure, + engine2_oil_pressure, ); } @@ -652,17 +666,17 @@ impl A320BlueElectricPumpController { &mut self, overhead_panel: &A320HydraulicOverheadPanel, pressure_switch_state: bool, - engine1_n2: Ratio, - engine2_n2: Ratio, + engine1_oil_pressure: Pressure, + engine2_oil_pressure: Pressure, ) { - // Faking low engine oil pressure using N2 slow level. TODO Get real oil pressure - let faked_is_engine_low_oil_pressure = - engine1_n2.get::() < 25. && engine2_n2.get::() < 25.; + // Low engine oil pressure inhibits fault under 18psi level + let is_engine_low_oil_pressure = + engine1_oil_pressure.get::() < 18. && engine2_oil_pressure.get::() < 18.; self.is_pressure_low = self.should_pressurise() && !pressure_switch_state; self.has_pressure_low_fault = self.is_pressure_low - && ((!faked_is_engine_low_oil_pressure || !self.weight_on_wheels) + && ((!is_engine_low_oil_pressure || !self.weight_on_wheels) || overhead_panel.blue_epump_override_push_button_is_on()); } @@ -1260,6 +1274,7 @@ mod tests { 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, @@ -1267,8 +1282,8 @@ mod tests { }; struct A320HydraulicsTestAircraft { - engine_1: Engine, - engine_2: Engine, + engine_1: LeapEngine, + engine_2: LeapEngine, hydraulics: A320Hydraulic, overhead: A320HydraulicOverheadPanel, engine_fire_overhead: A320EngineFireOverheadPanel, @@ -1277,8 +1292,8 @@ mod tests { impl A320HydraulicsTestAircraft { fn new() -> Self { Self { - engine_1: Engine::new(1), - engine_2: Engine::new(2), + engine_1: LeapEngine::new(1), + engine_2: LeapEngine::new(2), hydraulics: A320Hydraulic::new(), overhead: A320HydraulicOverheadPanel::new(), engine_fire_overhead: A320EngineFireOverheadPanel::new(), @@ -1328,13 +1343,6 @@ mod tests { fn is_yellow_pressurised(&self) -> bool { self.hydraulics.is_yellow_pressurised() } - - fn set_engine_1_n2(&mut self, n2: Ratio) { - self.engine_1.corrected_n2 = n2; - } - fn set_engine_2_n2(&mut self, n2: Ratio) { - self.engine_2.corrected_n2 = n2; - } } impl Aircraft for A320HydraulicsTestAircraft { @@ -1353,6 +1361,8 @@ mod tests { } 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); @@ -1594,7 +1604,8 @@ mod tests { fn start_eng1(mut self, n2: Ratio) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:1", true); - self.aircraft.set_engine_1_n2(n2); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:1", n2.get::()); self } @@ -1602,7 +1613,8 @@ mod tests { fn start_eng2(mut self, n2: Ratio) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:2", true); - self.aircraft.set_engine_2_n2(n2); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:2", n2.get::()); self } @@ -1610,7 +1622,8 @@ mod tests { fn stop_eng1(mut self) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:1", false); - self.aircraft.set_engine_1_n2(Ratio::new::(0.)); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:1", 0.); self } @@ -1618,7 +1631,8 @@ mod tests { fn stopping_eng1(mut self) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:1", false); - self.aircraft.set_engine_1_n2(Ratio::new::(25.)); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:1", 25.); self } @@ -1626,7 +1640,8 @@ mod tests { fn stop_eng2(mut self) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:2", false); - self.aircraft.set_engine_2_n2(Ratio::new::(0.)); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:2", 0.); self } @@ -1634,7 +1649,8 @@ mod tests { fn stopping_eng2(mut self) -> Self { self.simulation_test_bed .write_bool("GENERAL ENG STARTER ACTIVE:2", false); - self.aircraft.set_engine_2_n2(Ratio::new::(25.)); + self.simulation_test_bed + .write_f64("TURB ENG CORRECTED N2:2", 25.); self } @@ -3307,31 +3323,46 @@ mod tests { #[test] fn controller_blue_epump_split_master_switch() { - let engine_on_n2 = Ratio::new::(50.); - let engine_off_n2 = Ratio::new::(1.); + 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(); - blue_epump_controller.update(&overhead_panel, true, engine_off_n2, engine_off_n2); + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_off_oil_pressure, + ); assert!(!blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = false; - blue_epump_controller.update(&overhead_panel, true, engine_on_n2, engine_off_n2); + blue_epump_controller.update( + &overhead_panel, + true, + engine_on_oil_pressure, + engine_off_oil_pressure, + ); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = true; - blue_epump_controller.update(&overhead_panel, true, engine_off_n2, engine_on_n2); + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_on_oil_pressure, + ); assert!(blue_epump_controller.should_pressurise()); } #[test] fn controller_blue_epump_on_off_master_switch() { - let engine_on_n2 = Ratio::new::(50.); - let engine_off_n2 = Ratio::new::(1.); + 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(); @@ -3339,31 +3370,51 @@ mod tests { blue_epump_controller.engine_1_master_on = true; blue_epump_controller.engine_2_master_on = true; - blue_epump_controller.update(&overhead_panel, true, engine_on_n2, engine_on_n2); + blue_epump_controller.update( + &overhead_panel, + true, + engine_on_oil_pressure, + engine_on_oil_pressure, + ); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; - blue_epump_controller.update(&overhead_panel, false, engine_off_n2, engine_off_n2); + blue_epump_controller.update( + &overhead_panel, + false, + engine_off_oil_pressure, + engine_off_oil_pressure, + ); assert!(!blue_epump_controller.should_pressurise()); } #[test] fn controller_blue_epump_override() { - let engine_off_n2 = Ratio::new::(1.); + let engine_off_oil_pressure = Pressure::new::(10.); let mut overhead_panel = A320HydraulicOverheadPanel::new(); let mut blue_epump_controller = A320BlueElectricPumpController::new(); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_on(); - blue_epump_controller.update(&overhead_panel, true, engine_off_n2, engine_off_n2); + blue_epump_controller.update( + &overhead_panel, + true, + engine_off_oil_pressure, + engine_off_oil_pressure, + ); assert!(blue_epump_controller.should_pressurise()); blue_epump_controller.engine_1_master_on = false; blue_epump_controller.engine_2_master_on = false; overhead_panel.blue_epump_override_push_button.push_off(); - blue_epump_controller.update(&overhead_panel, false, engine_off_n2, engine_off_n2); + blue_epump_controller.update( + &overhead_panel, + false, + engine_off_oil_pressure, + engine_off_oil_pressure, + ); assert!(!blue_epump_controller.should_pressurise()); } @@ -3432,6 +3483,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(edp1_controller.should_pressurise()); @@ -3441,6 +3493,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(!edp1_controller.should_pressurise()); @@ -3450,6 +3503,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(edp1_controller.should_pressurise()); @@ -3469,6 +3523,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(edp1_controller.should_pressurise()); @@ -3478,6 +3533,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(10.), true, ); assert!(!edp1_controller.should_pressurise()); @@ -3496,6 +3552,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(edp2_controller.should_pressurise()); @@ -3505,6 +3562,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(!edp2_controller.should_pressurise()); @@ -3514,6 +3572,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(edp2_controller.should_pressurise()); @@ -3533,6 +3592,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(30.), true, ); assert!(edp2_controller.should_pressurise()); @@ -3542,6 +3602,7 @@ mod tests { &overhead_panel, &fire_overhead_panel, Ratio::new::(50.), + Pressure::new::(5.), true, ); assert!(!edp2_controller.should_pressurise()); diff --git a/src/systems/a320_systems/src/lib.rs b/src/systems/a320_systems/src/lib.rs index 726fd05fc64..68d0b313a0c 100644 --- a/src/systems/a320_systems/src/lib.rs +++ b/src/systems/a320_systems/src/lib.rs @@ -20,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}, }; @@ -33,8 +33,8 @@ 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, @@ -53,8 +53,8 @@ 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(), 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..1ea7fa972db --- /dev/null +++ b/src/systems/systems/src/engine/leap_engine.rs @@ -0,0 +1,62 @@ +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; + + 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 + } +} diff --git a/src/systems/systems/src/engine/mod.rs b/src/systems/systems/src/engine/mod.rs index 1f2112b1c45..e2ccd780fe1 100644 --- a/src/systems/systems/src/engine/mod.rs +++ b/src/systems/systems/src/engine/mod.rs @@ -1,27 +1,9 @@ -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, - pub 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; } diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 4a9411b8519..734be417310 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -4,15 +4,14 @@ use std::time::Duration; use uom::si::{ f64::*, pressure::psi, - ratio::percent, 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}; -use crate::{engine::Engine, simulation::UpdateContext}; pub mod brake_circuit; use crate::hydraulic::brake_circuit::Actuator; @@ -799,11 +798,12 @@ pub struct EngineDrivenPump { pump: Pump, } impl EngineDrivenPump { - // 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; - const PUMP_N2_GEAR_RATIO: f64 = 0.211; + // // 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 DISPLACEMENT_BREAKPTS: [f64; 9] = [ 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3020.0, 3500.0, @@ -829,12 +829,9 @@ impl EngineDrivenPump { &mut self, context: &UpdateContext, line: &HydraulicLoop, - engine: &Engine, + pump_rpm: f64, controller: &T, ) { - let n2_rpm = engine.corrected_n2().get::() * Self::LEAP_1A26_MAX_N2_RPM / 100.; - let pump_rpm = n2_rpm * Self::PUMP_N2_GEAR_RATIO; - self.pump.update(context, line, pump_rpm, controller); self.is_active = controller.should_pressurise(); } @@ -1190,8 +1187,8 @@ mod tests { let green_loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - let init_n2 = Ratio::new::(55.0); - let engine1 = engine(init_n2); + let init_rpm = 1900.; + let context = context(Duration::from_millis(100)); let mut pump_controller = TestPumpController::commanding_pressurise(); @@ -1209,7 +1206,7 @@ mod tests { assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); } - edp1.update(&context, &green_loop, &engine1, &pump_controller); + edp1.update(&context, &green_loop, init_rpm, &pump_controller); green_loop.update( &context, Vec::new(), @@ -1442,8 +1439,6 @@ mod tests { let mut engine_driven_pump = engine_driven_pump(); let mut engine_driven_pump_controller = TestPumpController::commanding_depressurise(); - let mut engine1 = engine(Ratio::new::(0.0)); - let mut green_loop = hydraulic_loop("GREEN"); let green_loop_controller = TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); @@ -1456,7 +1451,7 @@ mod tests { let yellow_res_at_start = yellow_loop.reservoir_volume; let green_res_at_start = green_loop.reservoir_volume; - engine1.corrected_n2 = Ratio::new::(100.0); + let edp_rpm = 3300.; for x in 0..800 { if x == 10 { // After 1s powering electric pump @@ -1534,7 +1529,7 @@ mod tests { engine_driven_pump.update( &context, &green_loop, - &engine1, + edp_rpm, &engine_driven_pump_controller, ); electric_pump.update(&context, &yellow_loop, &electric_pump_controller); @@ -1567,7 +1562,7 @@ mod tests { yellow_loop.max_loop_volume.get::() ); println!("---PSI GREEN: {}", green_loop.loop_pressure.get::()); - println!("---N2 GREEN: {}", engine1.corrected_n2.get::()); + println!("---N2 GREEN: {}", edp_rpm); println!( "---Priming State: {}/{}", green_loop.loop_volume.get::(), @@ -1629,13 +1624,6 @@ mod tests { EngineDrivenPump::new("DEFAULT") } - fn engine(n2: Ratio) -> Engine { - let mut engine = Engine::new(1); - engine.corrected_n2 = n2; - - engine - } - fn context(delta_time: Duration) -> UpdateContext { UpdateContext::new( delta_time, @@ -1650,7 +1638,6 @@ mod tests { #[cfg(test)] mod edp_tests { use super::*; - use uom::si::ratio::percent; #[test] fn starts_inactive() { @@ -1659,12 +1646,12 @@ mod tests { #[test] fn zero_flow_above_3000_psi_after_25ms() { - let n2 = Ratio::new::(60.0); + 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( - n2, + pump_rpm, displacement, pressure, &context @@ -1672,24 +1659,23 @@ mod tests { } fn delta_vol_equality_check( - n2: Ratio, + pump_rpm: f64, displacement: Volume, pressure: Pressure, context: &UpdateContext, ) -> bool { - let actual = get_edp_actual_delta_vol_when(n2, pressure, context); - let predicted = get_edp_predicted_delta_vol_when(n2, displacement, context); + 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( - n2: Ratio, + pump_rpm: f64, pressure: Pressure, context: &UpdateContext, ) -> Volume { - let eng = engine(n2); let mut edp = engine_driven_pump(); let mut line = hydraulic_loop("GREEN"); @@ -1698,22 +1684,20 @@ mod tests { edp.update( &context.with_delta(Duration::from_secs(1)), &line, - &eng, + pump_rpm, &engine_driven_pump_controller, ); // Update 10 times to stabilize displacement - edp.update(context, &line, &eng, &engine_driven_pump_controller); + edp.update(context, &line, pump_rpm, &engine_driven_pump_controller); edp.delta_vol_max() } fn get_edp_predicted_delta_vol_when( - n2: Ratio, + pump_rpm: f64, displacement: Volume, context: &UpdateContext, ) -> Volume { - let n2_rpm = n2.get::() * EngineDrivenPump::LEAP_1A26_MAX_N2_RPM / 100.; - let edp_rpm = n2_rpm * EngineDrivenPump::PUMP_N2_GEAR_RATIO; - let expected_flow = Pump::calculate_flow(edp_rpm, displacement); + let expected_flow = Pump::calculate_flow(pump_rpm, displacement); expected_flow * context.delta_as_time() } } From 7c3fe5331adce14fdc55576f5c4cf1f2052aff08 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 7 May 2021 15:43:27 +0200 Subject: [PATCH 119/122] wrong comment --- src/systems/systems/src/hydraulic/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 734be417310..74346ad05ce 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -798,13 +798,6 @@ pub struct EngineDrivenPump { pump: Pump, } impl EngineDrivenPump { - // // 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 DISPLACEMENT_BREAKPTS: [f64; 9] = [ 0.0, 500.0, 1000.0, 1500.0, 2800.0, 2900.0, 3000.0, 3020.0, 3500.0, ]; From f8a8047b0052f4d06bf1c09524ccbdf1866c55f8 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 7 May 2021 16:39:54 +0200 Subject: [PATCH 120/122] Removed duplicated tests in core hyd part --- src/systems/a320_systems/src/hydraulic.rs | 32 ++ src/systems/systems/src/hydraulic/mod.rs | 352 +--------------------- 2 files changed, 33 insertions(+), 351 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index 041d31247c1..b85aefb7db6 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -1519,6 +1519,14 @@ mod tests { 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 @@ -3687,6 +3695,30 @@ mod tests { 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::(50.)) + .start_eng2(Ratio::new::(50.)) + .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, diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 74346ad05ce..9ae84009759 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -1082,7 +1082,7 @@ mod tests { length::foot, pressure::{pascal, psi}, thermodynamic_temperature::degree_celsius, - volume::{gallon, liter}, + volume::gallon, }; struct TestHydraulicLoopController { @@ -1110,20 +1110,6 @@ mod tests { 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 { @@ -1131,26 +1117,6 @@ mod tests { } } - struct TestPowerTransferUnitController { - should_enable: bool, - } - impl TestPowerTransferUnitController { - fn commanding_disabled() -> Self { - Self { - should_enable: false, - } - } - - fn command_enable(&mut self) { - self.should_enable = true; - } - } - impl PowerTransferUnitController for TestPowerTransferUnitController { - fn should_enable(&self) -> bool { - self.should_enable - } - } - struct TestRamAirTurbineController { should_deploy: bool, } @@ -1172,170 +1138,6 @@ mod tests { } use super::*; - #[test] - /// Runs engine driven pump, checks pressure OK, shut it down, check drop of pressure after 20s - fn green_loop_edp_simulation() { - let mut edp1 = engine_driven_pump(); - let mut green_loop = hydraulic_loop("GREEN"); - let green_loop_controller = - TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - - let init_rpm = 1900.; - - let context = context(Duration::from_millis(100)); - let mut pump_controller = TestPumpController::commanding_pressurise(); - - for x in 0..600 { - if x == 50 { - // After 5s - assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); - } - if x == 200 { - assert!(green_loop.loop_pressure >= Pressure::new::(2850.0)); - pump_controller.command_depressurise(); - } - if x >= 500 { - // Shutdown + 30s - assert!(green_loop.loop_pressure <= Pressure::new::(250.0)); - } - - edp1.update(&context, &green_loop, init_rpm, &pump_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.loop_pressure.get::()); - println!( - "--------Reservoir Volume (g): {}", - green_loop.reservoir_volume.get::() - ); - println!( - "--------Loop Volume (g): {}", - green_loop.loop_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.raw_gas_press().get::() - ); - } - } - } - - #[test] - /// Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s - fn yellow_loop_epump_simulation() { - let mut epump = electric_pump(); - let mut yellow_loop = hydraulic_loop("YELLOW"); - let yellow_loop_controller = - TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - let mut pump_controller = TestPumpController::commanding_pressurise(); - - let context = context(Duration::from_millis(100)); - for x in 0..800 { - if x == 400 { - assert!(yellow_loop.loop_pressure >= Pressure::new::(2800.0)); - pump_controller.command_depressurise(); - } - - if x >= 600 { - // X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low - assert!(yellow_loop.loop_pressure <= Pressure::new::(200.0)); - } - epump.update(&context, &yellow_loop, &pump_controller); - yellow_loop.update( - &context, - vec![&epump], - Vec::new(), - Vec::new(), - Vec::new(), - &yellow_loop_controller, - ); - if x % 20 == 0 { - println!("Iteration {}", x); - println!("-------------------------------------------"); - println!("---PSI: {}", yellow_loop.loop_pressure.get::()); - println!("---RPM: {}", epump.rpm); - println!( - "--------Reservoir Volume (g): {}", - yellow_loop.reservoir_volume.get::() - ); - println!( - "--------Loop Volume (g): {}", - yellow_loop.loop_volume.get::() - ); - println!( - "--------Acc Volume (g): {}", - yellow_loop.accumulator.gas_volume.get::() - ); - } - } - } - - #[test] - /// Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s - fn blue_loop_epump_simulation() { - let mut epump = electric_pump(); - let mut blue_loop = hydraulic_loop("BLUE"); - let blue_loop_controller = - TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - let mut pump_controller = TestPumpController::commanding_pressurise(); - - let context = context(Duration::from_millis(100)); - for x in 0..800 { - if x == 400 { - assert!(blue_loop.loop_pressure >= Pressure::new::(2800.0)); - pump_controller.command_depressurise(); - } - - if x >= 600 { - // X+200 after shutoff = X + 20seconds @ 100ms, so pressure shall be low - assert!(blue_loop.loop_pressure <= Pressure::new::(100.0)); - } - epump.update(&context, &blue_loop, &pump_controller); - blue_loop.update( - &context, - vec![&epump], - Vec::new(), - Vec::new(), - Vec::new(), - &blue_loop_controller, - ); - if x % 20 == 0 { - println!("Iteration {}", x); - println!("-------------------------------------------"); - println!("---PSI: {}", blue_loop.loop_pressure.get::()); - println!("---RPM: {}", epump.rpm); - println!( - "--------Reservoir Volume (g): {}", - blue_loop.reservoir_volume.get::() - ); - println!( - "--------Loop Volume (g): {}", - blue_loop.loop_volume.get::() - ); - println!( - "--------Acc Volume (g): {}", - blue_loop.accumulator.gas_volume.get::() - ); - } - } - } - #[test] /// Runs electric pump, checks pressure OK, shut it down, check drop of pressure after 20s fn blue_loop_rat_deploy_simulation() { @@ -1417,154 +1219,6 @@ mod tests { } } - #[test] - /// Runs green edp and yellow epump, checks pressure OK, - /// shut green edp off, check drop of pressure and ptu effect - /// shut yellow epump, check drop of pressure in both loops - fn yellow_green_ptu_loop_simulation() { - let mut yellow_loop = hydraulic_loop("YELLOW"); - let yellow_loop_controller = - TestHydraulicLoopController::commanding_open_fire_shutoff_valve(); - - let mut electric_pump = electric_pump(); - let mut electric_pump_controller = TestPumpController::commanding_depressurise(); - - let mut engine_driven_pump = engine_driven_pump(); - let mut engine_driven_pump_controller = TestPumpController::commanding_depressurise(); - - let mut green_loop = hydraulic_loop("GREEN"); - let green_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)); - - 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.loop_pressure <= Pressure::new::(50.0)); - assert!(yellow_loop.reservoir_volume == yellow_res_at_start); - - assert!(green_loop.loop_pressure <= Pressure::new::(50.0)); - assert!(green_loop.reservoir_volume == green_res_at_start); - - electric_pump_controller.command_pressurise(); - } - - if x == 110 { - // 10s later enabling ptu - println!("--------------PTU ENABLED--------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2950.0)); - assert!(yellow_loop.reservoir_volume <= yellow_res_at_start); - - assert!(green_loop.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.loop_pressure >= Pressure::new::(2400.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2400.0)); - } - - if x == 400 { - // @40s enabling edp - println!("------------GREEN EDP1 ON------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2600.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2000.0)); - engine_driven_pump_controller.command_pressurise(); - } - - if (500..=600).contains(&x) { - // 10s later and during 10s, ptu should stay inactive - println!("------------IS PTU ACTIVE??------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(!ptu.is_active_left && !ptu.is_active_right); - } - - if x == 600 { - // @60s diabling edp and epump - println!("-------------ALL PUMPS OFF------------"); - assert!(yellow_loop.loop_pressure >= Pressure::new::(2900.0)); - assert!(green_loop.loop_pressure >= Pressure::new::(2900.0)); - engine_driven_pump_controller.command_depressurise(); - electric_pump_controller.command_depressurise(); - } - - if x == 800 { - // @80s disabling edp and epump - println!("-----------IS PRESSURE OFF?-----------"); - assert!(yellow_loop.loop_pressure < Pressure::new::(50.0)); - assert!(green_loop.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); - engine_driven_pump.update( - &context, - &green_loop, - edp_rpm, - &engine_driven_pump_controller, - ); - electric_pump.update(&context, &yellow_loop, &electric_pump_controller); - - yellow_loop.update( - &context, - vec![&electric_pump], - Vec::new(), - Vec::new(), - vec![&ptu], - &yellow_loop_controller, - ); - green_loop.update( - &context, - Vec::new(), - vec![&engine_driven_pump], - Vec::new(), - vec![&ptu], - &green_loop_controller, - ); - - if x % 20 == 0 { - println!("Iteration {}", x); - println!("-------------------------------------------"); - println!("---PSI YELLOW: {}", yellow_loop.loop_pressure.get::()); - println!("---RPM YELLOW: {}", electric_pump.rpm); - println!( - "---Priming State: {}/{}", - yellow_loop.loop_volume.get::(), - yellow_loop.max_loop_volume.get::() - ); - println!("---PSI GREEN: {}", green_loop.loop_pressure.get::()); - println!("---N2 GREEN: {}", edp_rpm); - println!( - "---Priming State: {}/{}", - green_loop.loop_volume.get::(), - green_loop.max_loop_volume.get::() - ); - } - } - } - fn hydraulic_loop(loop_color: &str) -> HydraulicLoop { match loop_color { "GREEN" => HydraulicLoop::new( @@ -1609,10 +1263,6 @@ mod tests { } } - fn electric_pump() -> ElectricPump { - ElectricPump::new("DEFAULT") - } - fn engine_driven_pump() -> EngineDrivenPump { EngineDrivenPump::new("DEFAULT") } From 7e6ac7ae659f8bde0b85564b1875cd9d9d36148f Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 7 May 2021 17:00:00 +0200 Subject: [PATCH 121/122] Update CHANGELOG.md --- .github/CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2ee1c6d2459..f2edff9c878 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,8 +5,7 @@ ## 0.7.0 -1. [HYD] First hydraulics systems integration - @crocket63 (crocket) - First building block, more to come. Hydraulics do not impact the sim YET +1. [HYD] First building block, more to come. Hydraulics do not impact the sim YET. - @crocket6 (crocket) 1. [MCDU] Fixed input and display issues on PERF/W&B and INIT pages - @felixharnstrom (Felix Härnström) 1. [MCDU] Progress page only shows GPS Primary when it should - @Username23-23 (NONAmr2433 #8777) 1. [ND] Add VOR/ADF needles to ILS arc display - @tracernz (Mike) From 368f54cf74e9b75bc9809a2bd5ef04546e3ce3a9 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Fri, 7 May 2021 20:36:46 +0200 Subject: [PATCH 122/122] Updated blue epump auto run logic --- src/systems/a320_systems/src/hydraulic.rs | 162 ++++++++++++------ src/systems/systems/src/engine/leap_engine.rs | 6 + src/systems/systems/src/engine/mod.rs | 1 + 3 files changed, 117 insertions(+), 52 deletions(-) diff --git a/src/systems/a320_systems/src/hydraulic.rs b/src/systems/a320_systems/src/hydraulic.rs index b85aefb7db6..721c50687f3 100644 --- a/src/systems/a320_systems/src/hydraulic.rs +++ b/src/systems/a320_systems/src/hydraulic.rs @@ -404,6 +404,8 @@ impl A320Hydraulic { 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, @@ -534,6 +536,8 @@ struct A320EngineDrivenPumpController { 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, @@ -556,7 +560,8 @@ impl A320EngineDrivenPumpController { 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::() < 18.; + 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() @@ -618,18 +623,16 @@ struct A320BlueElectricPumpController { should_pressurise: bool, has_pressure_low_fault: bool, is_pressure_low: bool, - engine_1_master_on: bool, - engine_2_master_on: 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, - engine_1_master_on: false, - engine_2_master_on: false, weight_on_wheels: true, } } @@ -640,10 +643,13 @@ impl A320BlueElectricPumpController { 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.engine_1_master_on - || self.engine_2_master_on + 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; @@ -670,8 +676,10 @@ impl A320BlueElectricPumpController { engine2_oil_pressure: Pressure, ) { // Low engine oil pressure inhibits fault under 18psi level - let is_engine_low_oil_pressure = - engine1_oil_pressure.get::() < 18. && engine2_oil_pressure.get::() < 18.; + 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; @@ -693,8 +701,6 @@ impl PumpController for A320BlueElectricPumpController { impl SimulationElement for A320BlueElectricPumpController { fn read(&mut self, state: &mut SimulatorReader) { - self.engine_1_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:1"); - self.engine_2_master_on = state.read_bool("GENERAL ENG STARTER ACTIVE:2"); self.weight_on_wheels = state.read_bool("SIM ON GROUND"); } @@ -1568,8 +1574,8 @@ mod tests { .set_indicated_altitude(Length::new::(2500.)); self.simulation_test_bed .set_indicated_airspeed(Velocity::new::(180.)); - self.start_eng1(Ratio::new::(60.)) - .start_eng2(Ratio::new::(60.)) + self.start_eng1(Ratio::new::(80.)) + .start_eng2(Ratio::new::(80.)) .set_gear_up() .set_park_brake(false) } @@ -1880,7 +1886,7 @@ mod tests { // Not all engines on or off should disable ptu if on ground and park brake on test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .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(); @@ -2041,7 +2047,7 @@ mod tests { // Starting eng 1 test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) .run_one_tick(); // ALMOST No pressure @@ -2056,7 +2062,7 @@ mod tests { // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(5)); assert!(test_bed.is_green_pressurised()); @@ -2130,7 +2136,7 @@ mod tests { assert!(!test_bed.is_green_edp_press_low_fault()); test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) .run_one_tick(); assert!(!test_bed.is_green_pressurised()); @@ -2194,7 +2200,7 @@ mod tests { assert!(!test_bed.is_yellow_edp_press_low_fault()); test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_one_tick(); assert!(!test_bed.is_yellow_pressurised()); @@ -2225,7 +2231,7 @@ mod tests { assert!(!test_bed.is_blue_epump_press_low_fault()); test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_one_tick(); assert!(!test_bed.is_blue_pressurised()); @@ -2285,7 +2291,7 @@ mod tests { // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(5)); // No more fault LOW expected @@ -2386,7 +2392,7 @@ mod tests { // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(5)); // No more fault LOW expected @@ -2435,7 +2441,7 @@ mod tests { // Waiting for 5s pressure should be at 3000 psi in EDP section test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(5)); // No more fault LOW expected @@ -2450,7 +2456,7 @@ mod tests { .on_the_ground() .set_cold_dark_inputs() .set_park_brake(false) - .start_eng2(Ratio::new::(60.)) + .start_eng2(Ratio::new::(80.)) .run_one_tick(); // EDP should be LOW pressure state @@ -2476,7 +2482,7 @@ mod tests { // Waiting for 5s pressure should be at 3000 psi in EDP section test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(5)); // No more fault LOW expected @@ -2558,10 +2564,10 @@ mod tests { .set_ptu_state(false) .run_one_tick(); - // Starting eng 1 + // Starting eng 1 and eng 2 test_bed = test_bed - .start_eng1(Ratio::new::(50.)) - .start_eng2(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) + .start_eng2(Ratio::new::(80.)) .run_one_tick(); // ALMOST No pressure @@ -2601,7 +2607,7 @@ mod tests { // Starting eng 1 test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_one_tick(); // ALMOST No pressure assert!(!test_bed.is_green_pressurised()); @@ -2615,7 +2621,7 @@ mod tests { // Waiting for 5s pressure should be at 3000 psi test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(5)); assert!(!test_bed.is_green_pressurised()); @@ -2674,7 +2680,7 @@ mod tests { // Now doing cycles of pressurisation on EDP and ePump for _ in 1..6 { test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(50)); assert!(test_bed.yellow_pressure() < Pressure::new::(3100.)); @@ -2728,7 +2734,7 @@ mod tests { // Starting EDP wait for pressure rise to make sure system is primed test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .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.)); @@ -2747,7 +2753,7 @@ mod tests { // Now doing cycles of pressurisation on EDP for _ in 1..6 { test_bed = test_bed - .start_eng1(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(50)); assert!(test_bed.green_pressure() < Pressure::new::(3500.)); @@ -2800,7 +2806,7 @@ mod tests { // 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::(50.)) + .start_eng1(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(50)); assert!(test_bed.blue_pressure() < Pressure::new::(3500.)); @@ -2817,7 +2823,7 @@ mod tests { // Now engine 2 is used test_bed = test_bed - .start_eng2(Ratio::new::(50.)) + .start_eng2(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(50)); assert!(test_bed.blue_pressure() < Pressure::new::(3500.)); @@ -2857,8 +2863,8 @@ mod tests { // Starting eng 1 test_bed = test_bed - .start_eng2(Ratio::new::(50.)) - .start_eng1(Ratio::new::(50.)) + .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 @@ -3330,7 +3336,42 @@ mod tests { } #[test] - fn controller_blue_epump_split_master_switch() { + 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(); @@ -3338,37 +3379,45 @@ mod tests { 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()); - blue_epump_controller.engine_1_master_on = true; - blue_epump_controller.engine_2_master_on = false; + 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()); - blue_epump_controller.engine_1_master_on = false; - blue_epump_controller.engine_2_master_on = true; + 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_master_switch() { + 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(); @@ -3376,23 +3425,27 @@ mod tests { let mut blue_epump_controller = A320BlueElectricPumpController::new(); - blue_epump_controller.engine_1_master_on = true; - blue_epump_controller.engine_2_master_on = true; + 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()); - blue_epump_controller.engine_1_master_on = false; - blue_epump_controller.engine_2_master_on = false; + 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()); } @@ -3400,28 +3453,33 @@ mod tests { #[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(); - blue_epump_controller.engine_1_master_on = false; - blue_epump_controller.engine_2_master_on = false; + 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()); - blue_epump_controller.engine_1_master_on = false; - blue_epump_controller.engine_2_master_on = false; + 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()); } @@ -3700,8 +3758,8 @@ mod tests { let mut test_bed = test_bed_with() .set_cold_dark_inputs() .on_the_ground() - .start_eng1(Ratio::new::(50.)) - .start_eng2(Ratio::new::(50.)) + .start_eng1(Ratio::new::(80.)) + .start_eng2(Ratio::new::(80.)) .run_waiting_for(Duration::from_secs(10)); assert!(test_bed.is_blue_pressurised()); diff --git a/src/systems/systems/src/engine/leap_engine.rs b/src/systems/systems/src/engine/leap_engine.rs index 1ea7fa972db..703d6fd55a5 100644 --- a/src/systems/systems/src/engine/leap_engine.rs +++ b/src/systems/systems/src/engine/leap_engine.rs @@ -18,6 +18,8 @@ impl LeapEngine { // 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), @@ -59,4 +61,8 @@ impl Engine for LeapEngine { 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 e2ccd780fe1..cc6e015f60b 100644 --- a/src/systems/systems/src/engine/mod.rs +++ b/src/systems/systems/src/engine/mod.rs @@ -6,4 +6,5 @@ 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; }

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 R#T5Lbp>}wGrmBVi{{SEKrNsaM literal 0 HcmV?d00001 diff --git a/src/systems/systems/yellow_green_ptu_loop_simulation()_PTU.png b/src/systems/systems/yellow_green_ptu_loop_simulation()_PTU.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc6af6fc4ea2bbce84b608a578e144c0e73d1c8 GIT binary patch literal 51333 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 Date: Tue, 9 Mar 2021 10:15:35 +0100 Subject: [PATCH 016/122] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d2096e2ec6d..92571846bc3 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ node_modules /target /src/instruments/src/EFB/web/ /src/systems/target/ +/src/systems/systems/*.png From 757203510d6e8f6b6b327e96222b00e655643334 Mon Sep 17 00:00:00 2001 From: davydecorps <38904654+crocket63@users.noreply.github.com> Date: Tue, 9 Mar 2021 10:36:16 +0100 Subject: [PATCH 017/122] rm test outputs --- src/systems/systems/Eng_Driv_pump_carac.png | Bin 39713 -> 0 bytes src/systems/systems/Epump_carac.png | Bin 33830 -> 0 bytes .../green_loop_edp_simulation_EDP1 data.png | Bin 21320 -> 0 bytes ...een_loop_edp_simulation_Green Accum data.png | Bin 32671 -> 0 bytes .../systems/green_loop_edp_simulation_press.png | Bin 35346 -> 0 bytes ...ow_green_ptu_loop_simulation()_Green_acc.png | Bin 48578 -> 0 bytes ...w_green_ptu_loop_simulation()_Loop_press.png | Bin 58763 -> 0 bytes .../yellow_green_ptu_loop_simulation()_PTU.png | Bin 51333 -> 0 bytes ...w_green_ptu_loop_simulation()_Yellow_acc.png | Bin 34334 -> 0 bytes 9 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/systems/systems/Eng_Driv_pump_carac.png delete mode 100644 src/systems/systems/Epump_carac.png delete mode 100644 src/systems/systems/green_loop_edp_simulation_EDP1 data.png delete mode 100644 src/systems/systems/green_loop_edp_simulation_Green Accum data.png delete mode 100644 src/systems/systems/green_loop_edp_simulation_press.png delete mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_Green_acc.png delete mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_Loop_press.png delete mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_PTU.png delete mode 100644 src/systems/systems/yellow_green_ptu_loop_simulation()_Yellow_acc.png diff --git a/src/systems/systems/Eng_Driv_pump_carac.png b/src/systems/systems/Eng_Driv_pump_carac.png deleted file mode 100644 index 2b6e70d2d60f00b6d30537647ade15dfac75fc0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39713 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 diff --git a/src/systems/systems/Epump_carac.png b/src/systems/systems/Epump_carac.png deleted file mode 100644 index cb2ff1db3d4949b422c720ff19c1fb293ce6fe8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33830 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 diff --git a/src/systems/systems/green_loop_edp_simulation_EDP1 data.png b/src/systems/systems/green_loop_edp_simulation_EDP1 data.png deleted file mode 100644 index 7fef1c05397b123a8e820958f711155fbec038f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21320 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 diff --git a/src/systems/systems/green_loop_edp_simulation_press.png b/src/systems/systems/green_loop_edp_simulation_press.png deleted file mode 100644 index e6764f3bdab7f6471ec6e3704ba1b5edc0ee8f08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35346 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 JOZL|N{{byB{ki}E diff --git a/src/systems/systems/yellow_green_ptu_loop_simulation()_Loop_press.png b/src/systems/systems/yellow_green_ptu_loop_simulation()_Loop_press.png deleted file mode 100644 index 097f7407ab27ba79cc6bc22ed300255a44b25471..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58763 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