diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2f7e41fbcd7..6f4bdfbba8c 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -33,15 +33,29 @@ 1. [SD] Improve F/CTL and WHEEL SD pages visuals - @lukecologne (luke) 1. [MCDU] Added formatter to improve text alignment and ease integration - @derl30n (Leon) 1. [EFB] Fix default unit to match UI and other consumers - @tracernz (Mike) +1. [HYD] Added a new THS mechanism simulation - @Crocket63 (crocket) 1. [HYD] More prox sensors and gear actuator failures - @Crocket63 (crocket) 1. [MCDU] Added 4:3 aspect ratio compatibility to remote mcdu client - @tyler58546 (tyler58546) 1. [HYD] Fixed too slow leak measurement valves operation - @Crocket63 (crocket) 1. [ISIS] Added temporary ISIS font with arrows - @aweissoertel (Alexibexi#7550) 1. [PFD] Improve PFD barberpole rendering and behaviour - @lukecologne (luke) +1. [HYD] Fixed actuator position control demand consistency - @Crocket63 (crocket) 1. [FMGC] Only emit decel point when an approach is selected - @tracernz (Mike) +1. [HYD] Fixed SFCC computer failing to send FPPU commands - @Crocket63 (crocket) 1. [MCDU] Fix padding of arc radii on F-PLN - @tracernz (Mike) 1. [FMGC] Allow stringing of STARs with non-runway approaches - @tracernz (Mike) 1. [SD] Improve COND page visuals, fix some visual bugs on the SD - @lukecologne (luke) +1. [HYD] Fixed Lgciu state machine when reverting gravity extension - @Crocket63 (crocket) +1. [FMGC] Fix unnatural turn direction for path capture transition - @tracernz (Mike) +1. [FMGC] Fix unnatural turn direction for course capture transition - @tracernz (Mike) +1. [FMGC] Update spoiler CD for MSFS SDK clarification - @donstim (donbikes#4084) +1. [FMGC] Implement procedure turns (PI leg) - @tracernz (Mike) +1. [ECAM] Move EWD to correct AC bus - @tracernz (Mike) +1. [FMGC] Fix inbound leg time for holds - @tracernz (Mike) +1. [MCDU] Improved visuals of Init-A and Init-B page - @derl30n (Leon) +1. [MODEL] Added new animated gear gravity extension handle- @tyler58546 (tyler58546), @MoreRightRudder (Mike), @Crocket63 (crocket), @Lantarius +1. [MCDU] Hide stored elements on A/C Status when there are none - @tracernz (Mike) +1. [FMGC] Fix ident for CD legs - @tracernz (Mike) ## 0.8.0 diff --git a/Cargo.lock b/Cargo.lock index dfa483647f9..141f23906c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ "rand", "rustplotlib", "systems", - "uom 0.32.0", + "uom", ] [[package]] @@ -26,7 +26,7 @@ dependencies = [ "rand", "rstest", "systems", - "uom 0.32.0", + "uom", ] [[package]] @@ -37,7 +37,7 @@ dependencies = [ "msfs", "systems", "systems_wasm", - "uom 0.32.0", + "uom", ] [[package]] @@ -101,9 +101,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" dependencies = [ "addr2line", "cc", @@ -178,9 +178,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", @@ -415,9 +415,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.119" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libloading" @@ -437,9 +437,9 @@ checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] @@ -455,9 +455,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "minimal-lexical" @@ -467,18 +467,17 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", - "autocfg", ] [[package]] name = "msfs" version = "0.1.0" -source = "git+https://github.com/flybywiresim/msfs-rs?branch=main#4f6290f95c1dcbada65f541dc8c61e1e895e09e9" +source = "git+https://github.com/flybywiresim/msfs-rs?branch=main#f5ebb3d75f5a64fcd4e542129584ca0ee1ef0ebe" dependencies = [ "bindgen", "cc", @@ -490,7 +489,7 @@ dependencies = [ [[package]] name = "msfs_derive" version = "0.1.0" -source = "git+https://github.com/flybywiresim/msfs-rs?branch=main#4f6290f95c1dcbada65f541dc8c61e1e895e09e9" +source = "git+https://github.com/flybywiresim/msfs-rs?branch=main#f5ebb3d75f5a64fcd4e542129584ca0ee1ef0ebe" dependencies = [ "msfs_sdk", "quote", @@ -500,7 +499,7 @@ dependencies = [ [[package]] name = "msfs_sdk" version = "0.1.0" -source = "git+https://github.com/flybywiresim/msfs-rs?branch=main#4f6290f95c1dcbada65f541dc8c61e1e895e09e9" +source = "git+https://github.com/flybywiresim/msfs-rs?branch=main#f5ebb3d75f5a64fcd4e542129584ca0ee1ef0ebe" [[package]] name = "nalgebra" @@ -521,13 +520,12 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ "memchr", "minimal-lexical", - "version_check", ] [[package]] @@ -593,9 +591,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -614,9 +612,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", "libm", @@ -624,24 +622,24 @@ dependencies = [ [[package]] name = "object" -version = "0.27.1" +version = "0.28.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "paste" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "peeking_take_while" @@ -660,9 +658,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -698,18 +696,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] @@ -762,9 +760,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "regex" -version = "1.5.5" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -773,9 +771,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "rstest" @@ -837,18 +835,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -875,9 +873,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "strsim" @@ -893,13 +891,13 @@ checksum = "3685c82a045a6af0c488f0550b0f52b4c77d2a52b0ca8aba719f9d268fa96965" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -927,7 +925,7 @@ dependencies = [ "rand", "rand_distr", "rstest", - "uom 0.32.0", + "uom", ] [[package]] @@ -938,7 +936,7 @@ dependencies = [ "fxhash", "msfs", "systems", - "uom 0.30.0", + "uom", ] [[package]] @@ -961,18 +959,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", @@ -981,9 +979,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -1000,6 +998,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + [[package]] name = "unicode-width" version = "0.1.9" @@ -1008,25 +1012,15 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "uom" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e76503e636584f1e10b9b3b9498538279561adcef5412927ba00c2b32c4ce5ed" -dependencies = [ - "num-traits", - "typenum", -] +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "uom" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffcb443c899d2adc34f4ae4c64d6e9867b6d3ead2fad03979a8732a4ab6d396f" +checksum = "53e68fe0bfdacf0a6cef0efec5dcc295b836cde69b01ad93feb18488fa82050d" dependencies = [ "num-traits", "typenum", @@ -1046,15 +1040,15 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "which" -version = "4.2.4" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" dependencies = [ "either", "lazy_static", diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index 13f2af5de70..e0a2a5f1139 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -1114,26 +1114,6 @@ - Percent - Indicates the angle of the right slats out of 27 degrees -- A32NX_LEFT_FLAPS_TARGET_ANGLE - - Degrees - - Indicates the target angle of the left flaps - according to the configuration. - -- A32NX_RIGHT_FLAPS_TARGET_ANGLE - - Degrees - - Indicates the target angle of the right flaps - according to the configuration. - -- A32NX_LEFT_SLATS_TARGET_ANGLE - - Degrees - - Indicates the target angle of the left slats - according to the configuration. - -- A32NX_RIGHT_SLATS_TARGET_ANGLE - - Degrees - - Indicates the target angle of the right slats - according to the configuration. - - A32NX_LEFT_FLAPS_ANGLE - Degrees - The actual angle of the left flaps @@ -1546,6 +1526,26 @@ 0.0 | neutral 1.0 | full up +- A32NX_THS_{number}_ACTIVE_MODE_COMMANDED + - Boolean + - Trim electric motor {number} is commanded active + - {number} + - 1 + - 2 + - 3 + +- A32NX_THS_{number}_COMMANDED_POSITION + - Degree + - Trim electric motor {number} position demand in trim surface deflection angle + +- A32NX_HYD_THS_TRIM_MANUAL_OVERRIDE + - Boolean + - Feedback signal from the trim actuator system. True if pilot is moving or holding trim wheel + +- A32NX_HYD_TRIM_WHEEL_PERCENT + - Percent + - Trim wheel position in percent + - A32NX_ELEVATOR_DEFLECTION_DEMAND - Number - Provides the elevator position demand to hydraulics @@ -2894,13 +2894,9 @@ In the variables below, {number} should be replaced with one item in the set: { - LEFT - RIGHT -- A32NX_GEAR_EMERGENCY_EXTENSION_CLICKED - - Indicates the emergency extension handle is clicked in cockpit. - - Bool - -- A32NX_GEAR_EMERGENCY_EXTENSION_IS_TURNED - - Indicates the emergency extension handle is currently turning. - - Bool +- A32NX_GRAVITYGEAR_ROTATE_PCT + - Indicates the position of the gear emergency extension crank handle from 0 to 300 (3 turns) + - Percent ## ATC (ATA 34) diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_ALBEDO.PNG.DDS b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_ALBEDO.PNG.DDS new file mode 100644 index 00000000000..2b91bd69f8e Binary files /dev/null and b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_ALBEDO.PNG.DDS differ diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_ALBEDO.PNG.DDS.json b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_ALBEDO.PNG.DDS.json new file mode 100644 index 00000000000..d4088b4937b --- /dev/null +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_ALBEDO.PNG.DDS.json @@ -0,0 +1 @@ +{"Version":2,"SourceFileDate":133032791159350567,"Flags":["FL_BITMAP_COMPRESSION","FL_BITMAP_MIPMAP"]} \ No newline at end of file diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_COMP.PNG.DDS b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_COMP.PNG.DDS new file mode 100644 index 00000000000..bc8b787dd0b Binary files /dev/null and b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_COMP.PNG.DDS differ diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_COMP.PNG.DDS.json b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_COMP.PNG.DDS.json new file mode 100644 index 00000000000..1052c92a6b1 --- /dev/null +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_COMP.PNG.DDS.json @@ -0,0 +1 @@ +{"Version":2,"SourceFileDate":133032791160260571,"Flags":["FL_BITMAP_COMPRESSION","FL_BITMAP_MIPMAP","FL_BITMAP_NO_GAMMA_CORRECTION","FL_BITMAP_METAL_ROUGH_AO_DATA"]} \ No newline at end of file diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_NORMAL.PNG.DDS b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_NORMAL.PNG.DDS new file mode 100644 index 00000000000..f80d234e966 Binary files /dev/null and b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_NORMAL.PNG.DDS differ diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_NORMAL.PNG.DDS.json b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_NORMAL.PNG.DDS.json new file mode 100644 index 00000000000..57a580634d0 --- /dev/null +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/TEXTURE/EMERG_GEAR_EXT_NORMAL.PNG.DDS.json @@ -0,0 +1 @@ +{"Version":2,"SourceFileDate":133032791163051170,"Flags":["FL_BITMAP_COMPRESSION","FL_BITMAP_MIPMAP","FL_BITMAP_TANGENT_DXT5N","FL_BITMAP_NO_GAMMA_CORRECTION"]} \ No newline at end of file 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 93d071d3b35..86f6b10d518 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 @@ -279,15 +279,13 @@ -4854 - + LEVER_ELEVATORTRIM_DECAL - -4 - 13.5 - -4 + 0 0 - 13 - 94 - 13.5 + 97.143 + 94.2 + 100 100 2 @@ -2143,18 +2141,9 @@ - - - BUTTON - FBW_Push_Toggle - GEAR_EXTN_Decal - 1 (>L:A32NX_GEAR_EMERGENCY_EXTENSION_CLICKED) - 0 (>L:A32NX_GEAR_EMERGENCY_EXTENSION_CLICKED) - Emergency Extension (click and hold to turn) - - - - + + + LEVER_GRAVITYGEAR diff --git a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Field.js b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Field.js index 923c3a40dbd..6bd299d15c7 100644 --- a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Field.js +++ b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Field.js @@ -20,6 +20,41 @@ class CDU_Field { onSelect(value) { this.selectedCallback(this.currentValue); } + + getFieldAsColumnParameters() { + const text = this.getValue(); + let color = Column.white; + + if (text.includes("[color]amber")) { + color = Column.amber; + } else if (text.includes("[color]red")) { + color = Column.red; + } else if (text.includes("[color]green")) { + color = Column.green; + } else if (text.includes("[color]cyan")) { + color = Column.cyan; + } else if (text.includes("[color]magenta")) { + color = Column.magenta; + } else if (text.includes("[color]yellow")) { + color = Column.yellow; + } else if (text.includes("[color]inop")) { + color = Column.inop; + } + + return [ + this.onSelect.bind(this), + text + .replace("[color]white", "") + .replace("[color]amber", "") + .replace("[color]red", "") + .replace("[color]green", "") + .replace("[color]cyan", "") + .replace("[color]magenta", "") + .replace("[color]yellow", "") + .replace("[color]inop", "") + , color + ]; + } } /** diff --git a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js index 0f1d9cc8653..c7aa5e7456a 100644 --- a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js +++ b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js @@ -239,6 +239,7 @@ class CDUFlightPlanPage { break; case 16: // PI fixAnnotation = `PROC ${wp.turnDirection === 1 ? 'L' : 'R'}`; + ident = "INTCPT"; break; case 17: // RF fixAnnotation = `${("" + Math.round(wp.additionalData.radius)).padStart(2, "\xa0")}\xa0ARC`; @@ -829,6 +830,8 @@ function legTypeIsCourseReversal(wp) { } function legTurnIsForced(wp) { - // left || right - return wp.turnDirection === 1 || wp.turnDirection === 2; + // forced turns are only for straight legs + return (wp.turnDirection === 1 /* Left */ || wp.turnDirection === 2 /* Right */) + // eslint-disable-next-line semi-spacing + && wp.additionalData.legType !== 1 /* AF */ && wp.additionalData.legType !== 17 /* RF */; } diff --git a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Format.js b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Format.js index 3c31b1ab437..dde4be8b212 100644 --- a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Format.js +++ b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_Format.js @@ -6,16 +6,52 @@ class Column { * @param {...({ color: string } | { size: string } | { align: bool })} att - attributes of the text, e.g. text size, color and/or alignment */ constructor(index, text, ...att) { + this.index = index; this.raw = text; - this.color = (att.find(e => e.color) || Column.white).color; + this.color = att.find(e => e.color) || Column.white; this.length = text.length; this.anchorPos = !!att.find(e => e.align) ? index - this.length + 1 : index; const size = att.find(e => e.size); this.size = !!size ? [`{${size["size"]}}`, "{end}"] : ["", ""]; } + /** + * Returns a styled/formatted string. + * @returns {string} + */ get text() { - return `${this.size[0]}{${this.color}}${this.raw}{end}${this.size[1]}`; + return `${this.size[0]}{${this.color.color}}${this.raw}{end}${this.size[1]}`; + } + + /** + * @param {string} text - text to be displayed + */ + set text(text) { + this.raw = text; + this.length = text.length; + + // if text is right aligned => update anchor position + if (this.index !== this.anchorPos) { + this.anchorPos = this.index - this.length + 1; + } + } + + /** + * @param {...({ color: string } | { size: string })} att - attributes of the text, e.g. text size and/or color + */ + updateAttributes(...att) { + this.color = att.find(e => e.color) || this.color; + const size = att.find(e => e.size); + this.size = !!size ? [`{${size["size"]}}`, "{end}"] : this.size; + } + + /** + * @param {string} text - text to be displayed + * @param {...({ color: string } | { size: string })} att - attributes of the text, e.g. text size and/or color + */ + update(text, ...att) { + this.text = text; + this.updateAttributes(...att); } } @@ -75,5 +111,6 @@ function FormatLine(...columns) { pos = newEnd; } - return [line.replace(/\s/g, '{sp}')]; + // '{small}{end}{big}{end}' fixes the lines "jumping" (line moves up or down a few pixels) when entering small and large content into the same line. + return [line.replace(/\s/g, '{sp}') + "{small}{end}{big}{end}"]; } diff --git a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_IdentPage.js b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_IdentPage.js index 125f03537e5..1b8b61db00f 100644 --- a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_IdentPage.js +++ b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_IdentPage.js @@ -67,11 +67,37 @@ function calculateSecDate(date) { class CDUIdentPage { static ShowPage(mcdu, confirmDeleteAll = false) { - const date = mcdu.getNavDataDateRange(); - const stored = mcdu.dataManager.numberOfStoredElements(); mcdu.clearDisplay(); mcdu.page.Current = mcdu.page.IdentPage; mcdu.activeSystem = 'FMGC'; + + const date = mcdu.getNavDataDateRange(); + const stored = mcdu.dataManager.numberOfStoredElements(); + + let storedTitleCell = ""; + let storedRoutesRunwaysCell = ""; + let storedWaypointsNavaidsCell = ""; + let storedDeleteCell = ""; + if ((stored.routes + stored.runways + stored.waypoints + stored.navaids) > 0) { + storedTitleCell = "STORED\xa0\xa0\xa0\xa0"; + storedRoutesRunwaysCell = `{green}${stored.routes.toFixed(0).padStart(2, '0')}{end}{small}RTES{end}\xa0{green}${stored.runways.toFixed(0).padStart(2, '0')}{end}{small}RWYS{end}`; + storedWaypointsNavaidsCell = `{green}{big}${stored.waypoints.toFixed(0).padStart(2, '0')}{end}{end}{small}WPTS{end}\xa0{green}{big}${stored.navaids.toFixed(0).padStart(2, '0')}{end}{end}{small}NAVS{end}`; + storedDeleteCell = confirmDeleteAll ? '{amber}CONFIRM DEL*{end}' : '{cyan}DELETE ALL}{end}'; + + // DELETE ALL + mcdu.onRightInput[4] = () => { + if (confirmDeleteAll) { + const allDeleted = mcdu.dataManager.deleteAllStoredWaypoints(); + if (!allDeleted) { + mcdu.setScratchpadMessage(NXSystemMessages.fplnElementRetained); + } + CDUIdentPage.ShowPage(mcdu); + } else { + CDUIdentPage.ShowPage(mcdu, true); + } + }; + } + mcdu.setTemplate([ ["A320-200"],//This aircraft code is correct and does not include the engine type. ["\xa0ENG"], @@ -80,25 +106,12 @@ class CDUIdentPage { ["\xa0" + calculateActiveDate(date) + "[color]cyan", "AIRAC[color]green"], ["\xa0SECOND NAV DATA BASE"], ["{small}{" + calculateSecDate(date) + "{end}[color]inop"], - ["", "STORED\xa0\xa0\xa0\xa0"], - ["", `{green}${stored.routes.toFixed(0).padStart(2, '0')}{end}{small}RTES{end}\xa0{green}${stored.runways.toFixed(0).padStart(2, '0')}{end}{small}RWYS{end}`], - ["CHG CODE", `{green}{big}${stored.waypoints.toFixed(0).padStart(2, '0')}{end}{end}{small}WPTS{end}\xa0{green}{big}${stored.navaids.toFixed(0).padStart(2, '0')}{end}{end}{small}NAVS{end}`], - ["{small}[ ]{end}[color]inop", confirmDeleteAll ? '{amber}CONFIRM DEL*{end}' : '{cyan}DELETE ALL}{end}'], + ["", storedTitleCell], + ["", storedRoutesRunwaysCell], + ["CHG CODE", storedWaypointsNavaidsCell], + ["{small}[ ]{end}[color]inop", storedDeleteCell], ["IDLE/PERF", "SOFTWARE"], ["+0.0/+0.0[color]green", "STATUS/XLOAD>[color]inop"] ]); - - // DELETE ALL - mcdu.onRightInput[4] = () => { - if (confirmDeleteAll) { - const allDeleted = mcdu.dataManager.deleteAllStoredWaypoints(); - if (!allDeleted) { - mcdu.setScratchpadMessage(NXSystemMessages.fplnElementRetained); - } - CDUIdentPage.ShowPage(mcdu); - } else { - CDUIdentPage.ShowPage(mcdu, true); - } - }; } } diff --git a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_InitPage.js b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_InitPage.js index a782dfc7ede..0adcdfd163d 100644 --- a/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_InitPage.js +++ b/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_InitPage.js @@ -5,9 +5,9 @@ class CDUInitPage { mcdu.pageRedrawCallback = () => CDUInitPage.ShowPage1(mcdu); mcdu.activeSystem = 'FMGC'; - let fromTo = "____|____[color]amber"; - let coRoute = "__________[color]amber"; - const flightNo = new CDU_SingleValueField(mcdu, + const fromTo = new Column(23, "____|____", Column.amber, Column.right); + const coRoute = new Column(0, "__________", Column.amber); + const [flightNoAction, flightNoText, flightNoColor] = new CDU_SingleValueField(mcdu, "string", mcdu.flightNumber, { @@ -24,28 +24,34 @@ class CDUInitPage { } }); } - ); + ).getFieldAsColumnParameters(); //; - let altDest = "----|----------"; - let costIndex = "---"; - let cruiseFlTemp = "-----\xa0|---°"; + const altDest = new Column(0, "----|----------"); + let costIndexText = "---"; + let costIndexAction; + let costIndexColor = Column.white; + + const cruiseFl = new Column(0, "-----"); + const cruiseTemp = new Column (10, "---°", Column.right); + const cruiseFlTempSeparator = new Column(6, "/"); + let alignOption; - let tropo = "{small}36090{end}[color]cyan"; - let requestButton = "REQUEST*[color]amber"; - let requestButtonLabel = "INIT [color]amber"; + const tropo = new Column(23, "36090", Column.small, Column.cyan, Column.right); + let requestButton = "REQUEST*"; + let requestButtonLabel = "INIT"; let requestEnable = true; if (mcdu.simbrief.sendStatus === "REQUESTING") { requestEnable = false; - requestButton = "REQUEST [color]amber"; + requestButton = "REQUEST "; } if (mcdu.flightPlanManager.getPersistentOrigin() && mcdu.flightPlanManager.getPersistentOrigin().ident) { if (mcdu.flightPlanManager.getDestination() && mcdu.flightPlanManager.getDestination().ident) { - fromTo = mcdu.flightPlanManager.getPersistentOrigin().ident + "/" + mcdu.flightPlanManager.getDestination().ident + "[color]cyan"; - if (coRoute.includes("__________[color]amber")) { - coRoute = ""; + fromTo.update(mcdu.flightPlanManager.getPersistentOrigin().ident + "/" + mcdu.flightPlanManager.getDestination().ident, Column.cyan); + if (coRoute.raw.includes("__________")) { + coRoute.text = ""; } // If an active SimBrief OFP matches the FP, hide the request option @@ -58,7 +64,7 @@ class CDUInitPage { } // Cost index - costIndex = new CDU_SingleValueField(mcdu, + [costIndexAction, costIndexText, costIndexColor] = new CDU_SingleValueField(mcdu, "int", mcdu.costIndexSet ? mcdu.costIndex : null, { @@ -78,15 +84,24 @@ class CDUInitPage { } CDUInitPage.ShowPage1(mcdu); } - ); + ).getFieldAsColumnParameters(); + + mcdu.onLeftInput[4] = costIndexAction; + + cruiseFl.update("_____", Column.amber); + cruiseTemp.update("|___°", Column.amber); + cruiseFlTempSeparator.updateAttributes(Column.amber); - cruiseFlTemp = "_____\xa0|___°[color]amber"; //This is done so pilot enters a FL first, rather than using the computed one if (mcdu._cruiseEntered && mcdu._cruiseFlightLevel) { - cruiseFlTemp = - "{cyan}FL" + mcdu._cruiseFlightLevel.toFixed(0).padStart(3, "0") + "\xa0" + - (mcdu.cruiseTemperature ? "/" + mcdu.cruiseTemperature.toFixed(0) + "°" : "{small}/" + mcdu.tempCurve.evaluate(mcdu._cruiseFlightLevel).toFixed(0) + "°{end}") + - "{end}"; + cruiseFl.update("FL" + mcdu._cruiseFlightLevel.toFixed(0).padStart(3, "0"), Column.cyan); + if (mcdu.cruiseTemperature) { + cruiseTemp.update(mcdu.cruiseTemperature.toFixed(0) + "°", Column.cyan); + cruiseFlTempSeparator.updateAttributes(Column.cyan); + } else { + cruiseTemp.update(mcdu.tempCurve.evaluate(mcdu._cruiseFlightLevel).toFixed(0) + "°", Column.cyan, Column.small); + cruiseFlTempSeparator.updateAttributes(Column.cyan, Column.small); + } } // CRZ FL / FLX TEMP @@ -103,14 +118,10 @@ class CDUInitPage { } // Since CoRte isn't implemented, AltDest defaults to None Ref: Ares's documents - altDest = "NONE[color]cyan"; - if (mcdu.altDestination) { - altDest = mcdu.altDestination.ident + "[color]cyan"; - } else { - altDest = "NONE" + "[color]cyan"; - } + altDest.update(mcdu.altDestination ? mcdu.altDestination.ident : "NONE", Column.cyan); + mcdu.onLeftInput[1] = async (value, scratchpadCallback) => { - switch (altDest) { + switch (altDest.raw) { case "NONE": if (value === "") { CDUAvailableFlightPlanPage.ShowPage(mcdu); @@ -139,7 +150,7 @@ class CDUInitPage { } if (mcdu.coRoute) { - coRoute = mcdu.coRoute + "[color]cyan"; + coRoute.update(mcdu.coRoute, Column.cyan); } mcdu.onLeftInput[0] = (value, scratchpadCallback) => { mcdu.updateCoRoute(value, (result) => { @@ -152,7 +163,7 @@ class CDUInitPage { }; if (mcdu.tropo) { - tropo = `{cyan}${mcdu.tropo.toFixed(0).padStart(5, "\xa0")}`; + tropo.update("" + mcdu.tropo, Column.big); } mcdu.onRightInput[4] = (value, scratchpadCallback) => { if (mcdu.tryUpdateTropo(value)) { @@ -206,9 +217,9 @@ class CDUInitPage { } }; - let groundTemp = '---'; + const groundTemp = new Column(23, "---°", Column.right); if (mcdu.groundTemp !== undefined) { - groundTemp = `{${mcdu.groundTempPilot !== undefined ? 'big' : 'small'}}${mcdu.groundTemp.toFixed(0)}{end}`; + groundTemp.update(mcdu.groundTemp.toFixed(0) + "°", Column.cyan, (mcdu.groundTempPilot !== undefined ? Column.big : Column.small)); } mcdu.onRightInput[5] = (scratchpadValue, scratchpadCallback) => { @@ -225,23 +236,60 @@ class CDUInitPage { } }; + mcdu.onLeftInput[2] = flightNoAction; + mcdu.setArrows(false, false, true, true); - mcdu.setTemplate([ - ["INIT"], - ["\xa0CO RTE", "FROM/TO\xa0\xa0"], - [coRoute, fromTo], - ["ALTN/CO RTE", requestButtonLabel], - [altDest, requestButton], - ["FLT NBR"], - [flightNo, alignOption], + mcdu.setTemplate(FormatTemplate([ + [ + new Column(10, "INIT") + ], + [ + new Column(1, "CO RTE"), + new Column(21, "FROM/TO", Column.right) + ], + [ + coRoute, + fromTo + ], + [ + new Column(0, "ALTN/CO RTE"), + new Column(22, requestButtonLabel, Column.amber, Column.right) + ], + [ + altDest, + new Column(23, requestButton, Column.amber, Column.right) + ], + [ + new Column(0, "FLT NBR") + ], + [ + new Column(0, flightNoText, flightNoColor), + new Column(23, alignOption || "", Column.right) + ], [""], - ["", "WIND/TEMP>"], - ["COST INDEX", "TROPO"], - [costIndex, tropo], - ["CRZ FL/TEMP", "GND TEMP"], - [cruiseFlTemp, `{cyan}${groundTemp}°{end}`], - ]); + [ + new Column(23, "WIND/TEMP>", Column.right) + ], + [ + new Column(0, "COST INDEX"), + new Column(23, "TROPO", Column.right) + ], + [ + new Column(0, costIndexText, costIndexColor), + tropo + ], + [ + new Column(0, "CRZ FL/TEMP"), + new Column(23, "GND TEMP", Column.right) + ], + [ + cruiseFl, + cruiseFlTempSeparator, + cruiseTemp, + groundTemp + ] + ])); mcdu.onPrevPage = () => { mcdu.goToFuelPredPage(); @@ -299,19 +347,19 @@ class CDUInitPage { mcdu.activeSystem = 'FMGC'; mcdu.pageRedrawCallback = () => CDUInitPage.ShowPage2(mcdu); - let zfwColor = "[color]amber"; - let zfwCell = "___._"; - let zfwCgCell = "__._"; + const zfwCell = new Column(17, "___._", Column.amber, Column.right); + const zfwCgCell = new Column(22, "__._", Column.amber, Column.right); + const zfwCgCellDivider = new Column(18, "|", Column.amber, Column.right); + if (mcdu._zeroFuelWeightZFWCGEntered) { if (isFinite(mcdu.zeroFuelWeight)) { - zfwCell = (NXUnits.kgToUser(mcdu.zeroFuelWeight)).toFixed(1); - zfwColor = "[color]cyan"; + zfwCell.update(NXUnits.kgToUser(mcdu.zeroFuelWeight).toFixed(1), Column.cyan); } if (isFinite(mcdu.zeroFuelWeightMassCenter)) { - zfwCgCell = mcdu.zeroFuelWeightMassCenter.toFixed(1); + zfwCgCell.update(mcdu.zeroFuelWeightMassCenter.toFixed(1), Column.cyan); } if (isFinite(mcdu.zeroFuelWeight) && isFinite(mcdu.zeroFuelWeightMassCenter)) { - zfwColor = "[color]cyan"; + zfwCgCellDivider.updateAttributes(Column.cyan); } } mcdu.onRightInput[0] = async (value, scratchpadCallback) => { @@ -331,12 +379,10 @@ class CDUInitPage { } }; - let blockFuel = "__._"; - let blockFuelColor = "[color]amber"; + const blockFuel = new Column(23, "__._", Column.amber, Column.right); if (mcdu._blockFuelEntered || mcdu._fuelPlanningPhase === mcdu._fuelPlanningPhases.IN_PROGRESS) { if (isFinite(mcdu.blockFuel)) { - blockFuel = (NXUnits.kgToUser(mcdu.blockFuel)).toFixed(1); - blockFuelColor = "[color]cyan"; + blockFuel.update(NXUnits.kgToUser(mcdu.blockFuel).toFixed(1), Column.cyan); } } mcdu.onRightInput[1] = async (value, scratchpadCallback) => { @@ -359,12 +405,11 @@ class CDUInitPage { }; - let fuelPlanTopTitle = ""; - let fuelPlanBottomTitle = ""; - let fuelPlanColor = "[color]amber"; + const fuelPlanTopTitle = new Column(23, "", Column.amber, Column.right); + const fuelPlanBottomTitle = new Column(23, "", Column.amber, Column.right); if (mcdu._zeroFuelWeightZFWCGEntered && !mcdu._blockFuelEntered) { - fuelPlanTopTitle = "FUEL "; - fuelPlanBottomTitle = "PLANNING }"; + fuelPlanTopTitle.text = "FUEL "; + fuelPlanBottomTitle.text = "PLANNING }"; mcdu.onRightInput[2] = async () => { if (await mcdu.tryFuelPlanning()) { CDUInitPage.updateTowIfNeeded(mcdu); @@ -373,9 +418,8 @@ class CDUInitPage { }; } if (mcdu._fuelPlanningPhase === mcdu._fuelPlanningPhases.IN_PROGRESS) { - fuelPlanTopTitle = "BLOCK "; - fuelPlanBottomTitle = "CONFIRM"; - fuelPlanColor = "[color]green"; + fuelPlanTopTitle.update("BLOCK ", Column.green); + fuelPlanBottomTitle.update("CONFIRM", Column.green); mcdu.onRightInput[2] = async () => { if (await mcdu.tryFuelPlanning()) { CDUInitPage.updateTowIfNeeded(mcdu); @@ -385,16 +429,16 @@ class CDUInitPage { }; } - let towCell = "---.-"; - let lwCell = "---.-"; - let towLwColor = "[color]white"; + const towCell = new Column(17, "---.-", Column.right); + const lwCell = new Column(23, "---.-", Column.right); + const towLwCellDivider = new Column(18, "/"); + const taxiFuelCell = new Column(0, "0.4", Column.cyan, Column.small); - let taxiFuelCell = "{small}0.4{end}"; if (isFinite(mcdu.taxiFuelWeight)) { if (mcdu._taxiEntered) { - taxiFuelCell = (NXUnits.kgToUser(mcdu.taxiFuelWeight)).toFixed(1); + taxiFuelCell.update(NXUnits.kgToUser(mcdu.taxiFuelWeight).toFixed(1), Column.big); } else { - taxiFuelCell = "{small}" + (NXUnits.kgToUser(mcdu.taxiFuelWeight)).toFixed(1) + "{end}"; + taxiFuelCell.text = NXUnits.kgToUser(mcdu.taxiFuelWeight).toFixed(1); } } mcdu.onLeftInput[0] = async (value, scratchpadCallback) => { @@ -419,17 +463,15 @@ class CDUInitPage { } }; - let tripWeightCell = "---.-"; - let tripTimeCell = "----"; - let tripColor = "[color]white"; + const tripWeightCell = new Column(4, "---.-", Column.right); + const tripTimeCell = new Column(9, "----", Column.right); + const tripCellDivider = new Column(5, "/"); + const rteRsvWeightCell = new Column(4, "---.-", Column.right); + const rteRsvPercentCell = new Column(6, "5.0", Column.cyan); + const rteRsvCellDivider = new Column(5, "/", Column.cyan); - let rteRsvWeightCell = "---.-"; - let rteRsvPercentCell = "5.0"; - let rteRsvColor = "[color]white"; - let rteRsvPctColor = "{cyan}"; if (isFinite(mcdu.getRouteReservedPercent())) { - rteRsvPercentCell = mcdu.getRouteReservedPercent().toFixed(1); - rteRsvPctColor = "{cyan}"; + rteRsvPercentCell.text = mcdu.getRouteReservedPercent().toFixed(1); } mcdu.onLeftInput[2] = async (value, scratchpadCallback) => { if (await mcdu.trySetRouteReservedPercent(value)) { @@ -439,16 +481,16 @@ class CDUInitPage { } }; - let altnWeightCell = "---.-"; - let altnTimeCell = "----"; - let altnColor = "[color]white"; - let altnTimeColor = "{white}"; + const altnWeightCell = new Column(4, "---.-", Column.right); + const altnTimeCell = new Column(9, "----", Column.right); + const altnCellDivider = new Column(5, "/"); + const finalWeightCell = new Column(4, "---.-", Column.right); + const finalTimeCell = new Column(9, "----", Column.right); + const finalCellDivider = new Column(5, "/"); - let finalWeightCell = "---.-"; - let finalTimeCell = "----"; - let finalColor = "[color]white"; if (mcdu.getRouteFinalFuelTime() > 0) { - finalTimeCell = "{cyan}" + FMCMainDisplay.minutesTohhmm(mcdu.getRouteFinalFuelTime()) + "{end}"; + finalTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu.getRouteFinalFuelTime()), Column.cyan); + finalCellDivider.updateAttributes(Column.cyan); } mcdu.onLeftInput[4] = async (value, scratchpadCallback) => { if (await mcdu.trySetRouteFinalTime(value)) { @@ -458,31 +500,36 @@ class CDUInitPage { } }; - let extraWeightCell = "---.-"; - let extraTimeCell = "----"; - let extraColor = "[color]white"; - let extraTimeColor = "{white}"; - - let minDestFob = "---.-"; - let minDestFobColor = "[color]white"; - - let tripWindCell = `{small}${mcdu._windDir}{end}{big}${mcdu.averageWind.toFixed(0).padStart(3, "0")}{end}`; - mcdu.onRightInput[4] = async (value, scratchpadCallback) => { - if (await mcdu.trySetAverageWind(value)) { - CDUInitPage.ShowPage2(mcdu); - } else { - scratchpadCallback(); - } - }; + const extraWeightCell = new Column(18, "---.-", Column.right); + const extraTimeCell = new Column(23, "----", Column.right); + const extraCellDivider = new Column(19, "/"); + const minDestFob = new Column(4, "---.-", Column.right); + const tripWindDirCell = new Column(19, "--"); + const tripWindAvgCell = new Column(21, "---"); + + if ( + mcdu.flightPlanManager.getPersistentOrigin() && mcdu.flightPlanManager.getPersistentOrigin().ident + && mcdu.flightPlanManager.getDestination() && mcdu.flightPlanManager.getDestination().ident + ) { + tripWindDirCell.update(mcdu._windDir, Column.cyan, Column.small); + tripWindAvgCell.update(mcdu.averageWind.toFixed(0).padStart(3, "0"), Column.cyan); + + mcdu.onRightInput[4] = async (value, scratchpadCallback) => { + if (await mcdu.trySetAverageWind(value)) { + CDUInitPage.ShowPage2(mcdu); + } else { + scratchpadCallback(); + } + }; + } if (CDUInitPage.fuelPredConditionsMet(mcdu)) { - fuelPlanTopTitle = ""; - fuelPlanBottomTitle = ""; + fuelPlanTopTitle.text = ""; + fuelPlanBottomTitle.text = ""; mcdu.tryUpdateTOW(); if (isFinite(mcdu.takeOffWeight)) { - towCell = "{small}" + (NXUnits.kgToUser(mcdu.takeOffWeight)).toFixed(1); - towLwColor = "[color]green"; + towCell.update(NXUnits.kgToUser(mcdu.takeOffWeight).toFixed(1), Column.green, Column.small); } if (mcdu._fuelPredDone) { @@ -491,16 +538,17 @@ class CDUInitPage { } if (isFinite(mcdu.getRouteFinalFuelWeight()) && isFinite(mcdu.getRouteFinalFuelTime())) { if (mcdu._rteFinalWeightEntered) { - finalWeightCell = "{sp}{sp}" + (NXUnits.kgToUser(mcdu.getRouteFinalFuelWeight())).toFixed(1); + finalWeightCell.update(NXUnits.kgToUser(mcdu.getRouteFinalFuelWeight()).toFixed(1), Column.cyan); } else { - finalWeightCell = "{sp}{sp}{small}" + (NXUnits.kgToUser(mcdu.getRouteFinalFuelWeight())).toFixed(1) + "{end}"; + finalWeightCell.update(NXUnits.kgToUser(mcdu.getRouteFinalFuelWeight()).toFixed(1), Column.cyan, Column.small); } if (mcdu._rteFinalTimeEntered || !mcdu.routeFinalEntered()) { - finalTimeCell = FMCMainDisplay.minutesTohhmm(mcdu.getRouteFinalFuelTime()); + finalTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu.getRouteFinalFuelTime()), Column.cyan); } else { - finalTimeCell = "{small}" + FMCMainDisplay.minutesTohhmm(mcdu.getRouteFinalFuelTime()) + "{end}"; + finalTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu.getRouteFinalFuelTime()), Column.cyan, Column.small); + finalCellDivider.updateAttributes(Column.small); } - finalColor = "[color]cyan"; + finalCellDivider.updateAttributes(Column.cyan); } mcdu.onLeftInput[4] = async (value, scratchpadCallback) => { setTimeout(async () => { @@ -517,25 +565,19 @@ class CDUInitPage { if (mcdu.altDestination) { if (mcdu._routeAltFuelEntered) { if (isFinite(mcdu.getRouteAltFuelWeight())) { - altnWeightCell = "{sp}{sp}" + (NXUnits.kgToUser(mcdu.getRouteAltFuelWeight())).toFixed(1); - altnTimeCell = "{small}" + FMCMainDisplay.minutesTohhmm(mcdu.getRouteAltFuelTime()) + "{end}"; - altnTimeColor = "{green}"; - altnColor = "[color]cyan"; + altnWeightCell.update(NXUnits.kgToUser(mcdu.getRouteAltFuelWeight()).toFixed(1), Column.cyan); + altnTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu.getRouteAltFuelTime()), Column.green, Column.small); } } else { mcdu.tryUpdateRouteAlternate(); if (isFinite(mcdu.getRouteAltFuelWeight())) { - altnWeightCell = "{sp}{sp}{small}" + (NXUnits.kgToUser(mcdu.getRouteAltFuelWeight())).toFixed(1); - altnTimeCell = FMCMainDisplay.minutesTohhmm(mcdu.getRouteAltFuelTime()) + "{end}"; - altnTimeColor = "{green}"; - altnColor = "[color]cyan"; + altnWeightCell.update(NXUnits.kgToUser(mcdu.getRouteAltFuelWeight()).toFixed(1), Column.cyan, Column.small); + altnTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu.getRouteAltFuelTime()), Column.green, Column.small); } } + altnCellDivider.updateAttributes(Column.green, Column.small); } else { - altnWeightCell = "{sp}{sp}{small}0.0{end}"; - altnTimeCell = "----"; - altnColor = "[color]green"; - altnTimeColor = "{white}"; + altnWeightCell.update("0.0", Column.green, Column.small); } mcdu.onLeftInput[3] = async (value, scratchpadCallback) => { @@ -552,31 +594,29 @@ class CDUInitPage { mcdu.tryUpdateRouteTrip(); if (isFinite(mcdu.getTotalTripFuelCons()) && isFinite(mcdu.getTotalTripTime())) { - tripWeightCell = "{sp}{sp}{small}" + (NXUnits.kgToUser(mcdu.getTotalTripFuelCons())).toFixed(1); - tripTimeCell = FMCMainDisplay.minutesTohhmm(mcdu._routeTripTime); - tripColor = "[color]green"; + tripWeightCell.update(NXUnits.kgToUser(mcdu.getTotalTripFuelCons()).toFixed(1), Column.green, Column.small); + tripTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu._routeTripTime), Column.green, Column.small); + tripCellDivider.updateAttributes(Column.green, Column.small); } if (isFinite(mcdu.getRouteReservedWeight())) { if (mcdu._rteReservedWeightEntered) { - rteRsvWeightCell = "{sp}{sp}" + (NXUnits.kgToUser(mcdu.getRouteReservedWeight())).toFixed(1); + rteRsvWeightCell.update(NXUnits.kgToUser(mcdu.getRouteReservedWeight()).toFixed(1), Column.cyan); } else { - rteRsvWeightCell = "{sp}{sp}{small}" + (NXUnits.kgToUser(mcdu.getRouteReservedWeight())).toFixed(1) + "{end}"; + rteRsvWeightCell.update(NXUnits.kgToUser(mcdu.getRouteReservedWeight()).toFixed(1), Column.cyan, Column.small); } - rteRsvColor = "[color]cyan"; } if (mcdu._rteRsvPercentOOR) { - rteRsvPercentCell = "--.-"; - rteRsvPctColor = "{white}"; + rteRsvPercentCell.update("--.-", Column.white); + rteRsvCellDivider.updateAttributes(Column.white); } else if (isFinite(mcdu.getRouteReservedPercent())) { if (mcdu._rteReservedPctEntered || !mcdu.routeReservedEntered()) { - rteRsvPercentCell = mcdu.getRouteReservedPercent().toFixed(1); + rteRsvPercentCell.update(mcdu.getRouteReservedPercent().toFixed(1), Column.cyan); } else { - rteRsvPercentCell = "{small}" + mcdu.getRouteReservedPercent().toFixed(1) + "{end}"; + rteRsvPercentCell.update(mcdu.getRouteReservedPercent().toFixed(1), Column.cyan, Column.small); + rteRsvCellDivider.updateAttributes(Column.small); } - rteRsvColor = "[color]cyan"; - rteRsvPctColor = "{cyan}"; } mcdu.onLeftInput[2] = async (value, scratchpadCallback) => { @@ -592,12 +632,15 @@ class CDUInitPage { }; mcdu.tryUpdateLW(); - lwCell = (NXUnits.kgToUser(mcdu.landingWeight)).toFixed(1); - lwCell = lwCell.length <= 4 ? "{sp}" + lwCell : lwCell; + lwCell.update(NXUnits.kgToUser(mcdu.landingWeight).toFixed(1), Column.green, Column.small); + towLwCellDivider.updateAttributes(Column.green, Column.small); + + tripWindDirCell.update(mcdu._windDir, Column.small); + tripWindAvgCell.update("000", Column.small); - tripWindCell = "{small}" + mcdu._windDir + "000" + "{end}"; if (isFinite(mcdu.averageWind)) { - tripWindCell = `{small}${mcdu._windDir}{end}{big}${mcdu.averageWind.toFixed(0).padStart(3, "0")}{end}`; + tripWindDirCell.update(mcdu._windDir, Column.small); + tripWindAvgCell.update(mcdu.averageWind.toFixed(0).padStart(3, "0"), Column.big); } mcdu.onRightInput[4] = async (value, scratchpadCallback) => { setTimeout(async () => { @@ -612,12 +655,10 @@ class CDUInitPage { }; if (mcdu._minDestFobEntered) { - minDestFob = "{sp}{sp}" + (NXUnits.kgToUser(mcdu._minDestFob)).toFixed(1); - minDestFobColor = "[color]cyan"; + minDestFob.update(NXUnits.kgToUser(mcdu._minDestFob).toFixed(1), Column.cyan); } else { mcdu.tryUpdateMinDestFob(); - minDestFob = "{sp}{sp}{small}" + (NXUnits.kgToUser(mcdu._minDestFob)).toFixed(1) + "{end}"; - minDestFobColor = "[color]cyan"; + minDestFob.update(NXUnits.kgToUser(mcdu._minDestFob).toFixed(1), Column.cyan, Column.small); } mcdu.onLeftInput[5] = async (value, scratchpadCallback) => { setTimeout(async () => { @@ -632,35 +673,87 @@ class CDUInitPage { }; mcdu.checkEFOBBelowMin(); - extraWeightCell = "{small}" + (NXUnits.kgToUser(mcdu.tryGetExtraFuel())).toFixed(1); - if (mcdu.tryGetExtraFuel() < 0) { - extraTimeCell = "----{end}"; - extraTimeColor = "{white}"; - } else { - extraTimeCell = FMCMainDisplay.minutesTohhmm(mcdu.tryGetExtraTime()) + "{end}"; - extraTimeColor = "{green}"; + extraWeightCell.update(NXUnits.kgToUser(mcdu.tryGetExtraFuel()).toFixed(1), Column.green, Column.small); + if (mcdu.tryGetExtraFuel() >= 0) { + extraTimeCell.update(FMCMainDisplay.minutesTohhmm(mcdu.tryGetExtraTime()), Column.green, Column.small); + extraCellDivider.updateAttributes(Column.green, Column.small); } - extraColor = "[color]green"; } } mcdu.setArrows(false, false, true, true); - mcdu.setTemplate([ - ["INIT FUEL PRED"], - ["TAXI", "ZFW/ZFWCG"], - [taxiFuelCell + "[color]cyan", zfwCell + "|" + zfwCgCell + zfwColor], - ["TRIP\xa0\xa0/TIME", "BLOCK"], - [tripWeightCell + "/" + tripTimeCell + tripColor, blockFuel + blockFuelColor], - ["RTE RSV/%", fuelPlanTopTitle + fuelPlanColor], - [rteRsvWeightCell + rteRsvPctColor + "/" + rteRsvPercentCell + "{end}" + rteRsvColor, fuelPlanBottomTitle + fuelPlanColor], - ["ALTN\xa0\xa0/TIME", "TOW/\xa0\xa0\xa0\xa0LW"], - [altnWeightCell + altnTimeColor + "/" + altnTimeCell + "{end}" + altnColor, towCell + "/" + lwCell + towLwColor], - ["FINAL\xa0/TIME", "TRIP WIND"], - [finalWeightCell + "/" + finalTimeCell + finalColor, `{cyan}${tripWindCell}{end}`], - ["MIN DEST FOB", "EXTRA/\xa0TIME"], - [minDestFob + minDestFobColor, extraWeightCell + extraTimeColor + "/" + extraTimeCell + "{end}" + extraColor], - ]); + mcdu.setTemplate(FormatTemplate([ + [ + new Column(5, "INIT FUEL PRED") + ], + [ + new Column(0, "TAXI"), + new Column(15, "ZFW/ZFWCG") + ], + [ + taxiFuelCell, + zfwCell, + zfwCgCellDivider, + zfwCgCell + ], + [ + new Column(0, "TRIP"), + new Column(5, "/TIME"), + new Column(19, "BLOCK") + ], + [ + tripWeightCell, + tripCellDivider, + tripTimeCell, + blockFuel + ], + [ + new Column(0, "RTE RSV/%"), + fuelPlanTopTitle + ], + [ + rteRsvWeightCell, + rteRsvCellDivider, + rteRsvPercentCell, + fuelPlanBottomTitle + ], + [ + new Column(0, "ALTN"), + new Column(5, "/TIME"), + new Column(15, "TOW/"), + new Column(22, "LW") + ], + [ + altnWeightCell, + altnCellDivider, + altnTimeCell, + towCell, + towLwCellDivider, + lwCell + ], + [ + new Column(0, "FINAL/TIME"), + new Column(15, "TRIP WIND") + ], + [ + finalWeightCell, + finalCellDivider, + finalTimeCell, + tripWindDirCell, + tripWindAvgCell + ], + [ + new Column(0, "MIN DEST FOB"), + new Column(14, "EXTRA/TIME") + ], + [ + minDestFob, + extraWeightCell, + extraCellDivider, + extraTimeCell + ] + ])); mcdu.onPrevPage = () => { CDUInitPage.ShowPage1(mcdu); diff --git a/flybywire-aircraft-a320-neo/html_ui/Pages/VLivery/Liveries/Printer/Printer.js b/flybywire-aircraft-a320-neo/html_ui/Pages/VLivery/Liveries/Printer/Printer.js index c0734d7fc91..492b8bf2333 100644 --- a/flybywire-aircraft-a320-neo/html_ui/Pages/VLivery/Liveries/Printer/Printer.js +++ b/flybywire-aircraft-a320-neo/html_ui/Pages/VLivery/Liveries/Printer/Printer.js @@ -19,15 +19,9 @@ class LiveryPrinter extends TemplateElement { return; } this.Update(); - requestAnimationFrame(updateLoop); }; this._isConnected = true; - requestAnimationFrame(updateLoop); - } - disconnectedCallback() { - } - Update() { - + setInterval(updateLoop, 100); Coherent.on('A32NX_PRINT', (lines) => { const currentPageID = SimVar.GetSimVarValue("L:A32NX_PAGE_ID", "number") - 1; if (currentPageID >= 0 && this.pages[currentPageID] == null) { @@ -37,7 +31,10 @@ class LiveryPrinter extends TemplateElement { this.pages[currentPageID] = lines; } }); - + } + disconnectedCallback() { + } + Update() { if (this.pages == null) { return; } diff --git a/src/behavior/src/A32NX_Interior_Handling.xml b/src/behavior/src/A32NX_Interior_Handling.xml index 43ac6243d44..59f42599835 100644 --- a/src/behavior/src/A32NX_Interior_Handling.xml +++ b/src/behavior/src/A32NX_Interior_Handling.xml @@ -496,32 +496,84 @@ HANDLING_Wheel_ElevatorTrim HANDLING_Wheel_ElevatorTrim HANDLING_Wheel_ElevatorTrim - -16384 + 0 + 100 HANDLING_Wheel_ElevatorTrim_Pitch_2 - 100 - 0.001 + 0.001 + 0.03 + + (L:A32NX_HYD_TRIM_WHEEL_PERCENT) (>O:Position) + 1 (>O:IsDragging) + + + 0 (>O:IsDragging) + + + (K:AXIS_ELEV_TRIM_SET) = ((O:Position, number) / 100 * 16383 * 2) - 16383; + + - - 200 - (A:ELEVATOR TRIM PCT, Percent) 100 + - ELEVATOR TRIM PCT - percent - ELEVATOR_TRIM_SET + + 100 + (L:A32NX_HYD_TRIM_WHEEL_PERCENT) + #DRAG_CODE# + #DRAG_CODE# + + if (O:IsDragging, number) == 1 { + #DRAG_CODE#; + } + - - 200 - (A:ELEVATOR TRIM PCT, Percent) 100 + - ELEVATOR TRIM PCT - percent - ELEVATOR_TRIM_SET + + 100 + (L:A32NX_HYD_TRIM_WHEEL_PERCENT) + #DRAG_CODE# + #DRAG_CODE# + + if (O:IsDragging, number) == 1 { + #DRAG_CODE#; + } + + + + + diff --git a/src/fbw/src/FlyByWireInterface.cpp b/src/fbw/src/FlyByWireInterface.cpp index d34e746c4e7..7c5ece086a2 100644 --- a/src/fbw/src/FlyByWireInterface.cpp +++ b/src/fbw/src/FlyByWireInterface.cpp @@ -446,6 +446,9 @@ void FlyByWireInterface::setupLocalVariables() { idElevatorPosition = make_unique("A32NX_ELEVATOR_DEFLECTION_DEMAND"); + idThs1MotorActive = make_unique("A32NX_THS_1_ACTIVE_MODE_COMMANDED"); + idThs1MotorCommand = make_unique("A32NX_THS_1_COMMANDED_POSITION"); + idRudderPosition = make_unique("A32NX_RUDDER_DEFLECTION_DEMAND"); idRadioReceiverUsageEnabled = make_unique("A32NX_RADIO_RECEIVER_USAGE_ENABLED"); @@ -1506,19 +1509,12 @@ bool FlyByWireInterface::updateFlyByWire(double sampleTime) { } // set trim values - SimOutputEtaTrim outputEtaTrim = {}; - if (flyByWireOutput.output.eta_trim_deg_should_write && !idDevelopmentUseDirectLaw->get()) { - outputEtaTrim.eta_trim_deg = flyByWireOutput.output.eta_trim_deg; - elevatorTrimHandler->synchronizeValue(outputEtaTrim.eta_trim_deg); - } else { - outputEtaTrim.eta_trim_deg = elevatorTrimHandler->getPosition(); - } - if (!flyByWireOutput.sim.data_computed.tracking_mode_on) { - if (!simConnectInterface.sendData(outputEtaTrim)) { - cout << "WASM: Write data failed!" << endl; - return false; - } - } + idThs1MotorCommand->set(flyByWireOutput.output.eta_trim_deg); + if (flyByWireOutput.output.eta_trim_deg_should_write) { + idThs1MotorActive->set(1); + } else { + idThs1MotorActive->set(0); + } SimOutputZetaTrim outputZetaTrim = {}; rudderTrimHandler->update(sampleTime); diff --git a/src/fbw/src/FlyByWireInterface.h b/src/fbw/src/FlyByWireInterface.h index 3aabfb98b23..9f456d876c1 100644 --- a/src/fbw/src/FlyByWireInterface.h +++ b/src/fbw/src/FlyByWireInterface.h @@ -321,8 +321,9 @@ class FlyByWireInterface { std::unique_ptr idAileronPositionRight; std::shared_ptr animationAileronHandler; + std::unique_ptr idThs1MotorActive; + std::unique_ptr idThs1MotorCommand; std::unique_ptr idElevatorPosition; - std::unique_ptr idRudderPosition; std::unique_ptr idRadioReceiverUsageEnabled; diff --git a/src/fbw/src/interface/SimConnectData.h b/src/fbw/src/interface/SimConnectData.h index c3141b26a97..8d6bde56728 100644 --- a/src/fbw/src/interface/SimConnectData.h +++ b/src/fbw/src/interface/SimConnectData.h @@ -158,10 +158,6 @@ struct SimInputThrottles { double ATHR_reset_disable; }; -struct SimOutputEtaTrim { - double eta_trim_deg; -}; - struct SimOutputZetaTrim { double zeta_trim_pos; }; diff --git a/src/fbw/src/interface/SimConnectInterface.cpp b/src/fbw/src/interface/SimConnectInterface.cpp index 934e1a55c40..045cfdff5be 100644 --- a/src/fbw/src/interface/SimConnectInterface.cpp +++ b/src/fbw/src/interface/SimConnectInterface.cpp @@ -279,11 +279,6 @@ bool SimConnectInterface::prepareSimInputSimConnectDataDefinitions() { result &= addInputDataDefinition(hSimConnect, 0, Events::ELEV_DOWN, "ELEV_DOWN", true); result &= addInputDataDefinition(hSimConnect, 0, Events::ELEV_UP, "ELEV_UP", true); - result &= addInputDataDefinition(hSimConnect, 0, Events::ELEV_TRIM_DN, "ELEV_TRIM_DN", true); - result &= addInputDataDefinition(hSimConnect, 0, Events::ELEV_TRIM_UP, "ELEV_TRIM_UP", true); - result &= addInputDataDefinition(hSimConnect, 0, Events::ELEVATOR_TRIM_SET, "ELEVATOR_TRIM_SET", true); - result &= addInputDataDefinition(hSimConnect, 0, Events::AXIS_ELEV_TRIM_SET, "AXIS_ELEV_TRIM_SET", true); - result &= addInputDataDefinition(hSimConnect, 0, Events::AP_MASTER, "AP_MASTER", true); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTOPILOT_OFF, "AUTOPILOT_OFF", false); result &= addInputDataDefinition(hSimConnect, 0, Events::AUTOPILOT_ON, "AUTOPILOT_ON", true); @@ -860,10 +855,6 @@ bool SimConnectInterface::readData() { return true; } -bool SimConnectInterface::sendData(SimOutputEtaTrim output) { - // write data and return result - return sendData(2, sizeof(output), &output); -} bool SimConnectInterface::sendData(SimOutputZetaTrim output) { // write data and return result @@ -1354,54 +1345,6 @@ void SimConnectInterface::simConnectProcessEvent(const SIMCONNECT_RECV_EVENT* ev break; } - case Events::ELEV_TRIM_DN: { - elevatorTrimHandler->onEventElevatorTrimDown(); - if (loggingFlightControlsEnabled) { - cout << "WASM: ELEV_TRIM_DN: "; - cout << "(no data)"; - cout << " -> "; - cout << elevatorTrimHandler->getPosition(); - cout << endl; - } - break; - } - - case Events::ELEV_TRIM_UP: { - elevatorTrimHandler->onEventElevatorTrimUp(); - if (loggingFlightControlsEnabled) { - cout << "WASM: ELEV_TRIM_UP: "; - cout << "(no data)"; - cout << " -> "; - cout << elevatorTrimHandler->getPosition(); - cout << endl; - } - break; - } - - case Events::ELEVATOR_TRIM_SET: { - elevatorTrimHandler->onEventElevatorTrimSet(static_cast(event->dwData)); - if (loggingFlightControlsEnabled) { - cout << "WASM: ELEVATOR_TRIM_SET: "; - cout << static_cast(event->dwData); - cout << " -> "; - cout << elevatorTrimHandler->getPosition(); - cout << endl; - } - break; - } - - case Events::AXIS_ELEV_TRIM_SET: { - elevatorTrimHandler->onEventElevatorTrimAxisSet(static_cast(event->dwData)); - if (loggingFlightControlsEnabled) { - cout << "WASM: AXIS_ELEV_TRIM_SET: "; - cout << static_cast(event->dwData); - cout << " -> "; - cout << elevatorTrimHandler->getPosition(); - cout << endl; - } - break; - } - case Events::AUTOPILOT_OFF: { simInputAutopilot.AP_disconnect = 1; cout << "WASM: event triggered: AUTOPILOT_OFF" << endl; diff --git a/src/fbw/src/interface/SimConnectInterface.h b/src/fbw/src/interface/SimConnectInterface.h index f76f6a6ef68..bf5588a7ac9 100644 --- a/src/fbw/src/interface/SimConnectInterface.h +++ b/src/fbw/src/interface/SimConnectInterface.h @@ -36,10 +36,6 @@ class SimConnectInterface { ELEVATOR_SET, ELEV_DOWN, ELEV_UP, - ELEV_TRIM_DN, - ELEV_TRIM_UP, - ELEVATOR_TRIM_SET, - AXIS_ELEV_TRIM_SET, AP_MASTER, AUTOPILOT_OFF, AUTOPILOT_ON, @@ -209,8 +205,6 @@ class SimConnectInterface { bool readData(); - bool sendData(SimOutputEtaTrim output); - bool sendData(SimOutputZetaTrim output); bool sendData(SimOutputThrottles output); diff --git a/src/fmgc/src/components/fms-messages/FmsMessages.ts b/src/fmgc/src/components/fms-messages/FmsMessages.ts index 01e95ab28d3..51b9c96b8ee 100644 --- a/src/fmgc/src/components/fms-messages/FmsMessages.ts +++ b/src/fmgc/src/components/fms-messages/FmsMessages.ts @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: GPL-3.0 +import { TurnAreaExceedanceLeft, TurnAreaExceedanceRight } from '@fmgc/components/fms-messages/TurnAreaExceedance'; +import { FlightPlanManager } from '@shared/flightplan'; import { FMMessage, FMMessageTriggers } from '@shared/FmMessages'; import { FmgcComponent } from '../FmgcComponent'; import { GpsPrimary } from './GpsPrimary'; @@ -23,6 +25,8 @@ import { MapPartlyDisplayedLeft, MapPartlyDisplayedRight } from './MapPartlyDisp export class FmsMessages implements FmgcComponent { private listener = RegisterViewListener('JS_LISTENER_SIMVARS', null, true); + private baseInstrument: BaseInstrument; + private ndMessageFlags: Record<'L' | 'R', number> = { L: 0, R: 0, @@ -33,10 +37,18 @@ export class FmsMessages implements FmgcComponent { new GpsPrimaryLost(), new MapPartlyDisplayedLeft(), new MapPartlyDisplayedRight(), + new TurnAreaExceedanceLeft(), + new TurnAreaExceedanceRight(), ]; - init(): void { - // Do nothing + init(baseInstrument: BaseInstrument, _flightPlanManager: FlightPlanManager): void { + this.baseInstrument = baseInstrument; + + for (const selector of this.messageSelectors) { + if (selector.init) { + selector.init(this.baseInstrument); + } + } } update(deltaTime: number): void { @@ -173,6 +185,8 @@ export interface FMMessageSelector { efisSide?: 'L' | 'R'; + init?(baseInstrument: BaseInstrument): void; + /** * Optionally triggers a message when there isn't any other system or Redux update triggering it. */ diff --git a/src/fmgc/src/components/fms-messages/TurnAreaExceedance.ts b/src/fmgc/src/components/fms-messages/TurnAreaExceedance.ts new file mode 100644 index 00000000000..f65428226d6 --- /dev/null +++ b/src/fmgc/src/components/fms-messages/TurnAreaExceedance.ts @@ -0,0 +1,59 @@ +import { GuidanceController } from '@fmgc/guidance/GuidanceController'; +import { PILeg } from '@fmgc/guidance/lnav/legs/PI'; +import { Navigation } from '@fmgc/navigation/Navigation'; +import { FMMessage, FMMessageTypes } from '@shared/FmMessages'; +import { Trigger } from '@shared/logic'; +import { FMMessageSelector, FMMessageUpdate } from './FmsMessages'; + +abstract class TurnAreaExceedance implements FMMessageSelector { + message: FMMessage = FMMessageTypes.TurnAreaExceedance; + + abstract efisSide: 'L' | 'R'; + + private trigRising = new Trigger(true); + + private trigFalling = new Trigger(true); + + private guidanceController: GuidanceController; + + private navigation: Navigation; + + init(baseInstrument: BaseInstrument): void { + this.guidanceController = baseInstrument.guidanceController; + this.navigation = baseInstrument.navigation; + } + + process(deltaTime: number): FMMessageUpdate { + const gs = this.navigation.groundSpeed; + const dtg = this.guidanceController.activeLegDtg ?? Infinity; + const ttg = gs > 10 ? 3600 * dtg / gs : Infinity; + const nextLeg = this.guidanceController.activeGeometry.legs.get(this.guidanceController.activeLegIndex + 1); + + // if within 1.5 min of PI and it's path goes outside the coded distance limit + const turnAreaExceeded = ttg <= 90 && nextLeg instanceof PILeg && nextLeg.turnAreaExceeded; + + this.trigRising.input = turnAreaExceeded; + this.trigRising.update(deltaTime); + + this.trigFalling.input = !turnAreaExceeded; + this.trigFalling.update(deltaTime); + + if (this.trigRising.output) { + return FMMessageUpdate.SEND; + } + + if (this.trigFalling.output) { + return FMMessageUpdate.RECALL; + } + + return FMMessageUpdate.NO_ACTION; + } +} + +export class TurnAreaExceedanceLeft extends TurnAreaExceedance { + efisSide: 'L' | 'R' = 'L'; +} + +export class TurnAreaExceedanceRight extends TurnAreaExceedance { + efisSide: 'L' | 'R' = 'R'; +} diff --git a/src/fmgc/src/efis/EfisSymbols.ts b/src/fmgc/src/efis/EfisSymbols.ts index 18240b9b551..f604d4848e9 100644 --- a/src/fmgc/src/efis/EfisSymbols.ts +++ b/src/fmgc/src/efis/EfisSymbols.ts @@ -13,6 +13,7 @@ import { PathVector, PathVectorType } from '@fmgc/guidance/lnav/PathVector'; import { SegmentType } from '@fmgc/wtsdk'; import { distanceTo } from 'msfs-geo'; import { FlowEventSync } from '@shared/FlowEventSync'; +import { LnavConfig } from '@fmgc/guidance/LnavConfig'; import { LegType, RunwaySurface, TurnDirection, VorType } from '../types/fstypes/FSEnums'; import { NearbyFacilities } from './NearbyFacilities'; @@ -306,7 +307,7 @@ export class EfisSymbols { const legType = wp.additionalData.legType; if ( legType === LegType.CA || legType === LegType.CR || legType === LegType.CI - || legType === LegType.FM + || legType === LegType.FM || legType === LegType.PI || legType === LegType.VA || legType === LegType.VI || legType === LegType.VM ) { continue; @@ -337,12 +338,14 @@ export class EfisSymbols { const constraints = []; let direction; - // TODO PI leg - const isCourseReversal = wp.additionalData.legType === LegType.HA || wp.additionalData.legType === LegType.HF || wp.additionalData.legType === LegType.HM; + const isCourseReversal = wp.additionalData.legType === LegType.HA + || wp.additionalData.legType === LegType.HF + || wp.additionalData.legType === LegType.HM + || wp.additionalData.legType === LegType.PI; if (i === activeFp.activeWaypointIndex) { type |= NdSymbolTypeFlags.ActiveLegTermination; - } else if (isCourseReversal && i > (activeFp.activeWaypointIndex + 1) && range <= 80) { + } else if (isCourseReversal && i > (activeFp.activeWaypointIndex + 1) && range <= 80 && !LnavConfig.DEBUG_FORCE_INCLUDE_COURSE_REVERSAL_VECTORS) { if (wp.turnDirection === TurnDirection.Left) { type |= NdSymbolTypeFlags.CourseReversalLeft; } else { diff --git a/src/fmgc/src/flightplanning/FlightPlanManager.ts b/src/fmgc/src/flightplanning/FlightPlanManager.ts index 864d72c0475..dff13612a96 100644 --- a/src/fmgc/src/flightplanning/FlightPlanManager.ts +++ b/src/fmgc/src/flightplanning/FlightPlanManager.ts @@ -1897,14 +1897,14 @@ export class FlightPlanManager { public isWaypointInUse(icao: string): boolean { for (const fp of this._flightPlans) { - for (let i = 0; i < fp.waypoints.length; i++) { + for (let i = 0; i < fp?.waypoints.length; i++) { if (fp.getWaypoint(i).icao === icao) { return true; } } } for (const fixInfo of this._fixInfos) { - if (fixInfo.getRefFix()?.infos.icao === icao) { + if (fixInfo?.getRefFix()?.infos.icao === icao) { return true; } } diff --git a/src/fmgc/src/flightplanning/LegsProcedure.ts b/src/fmgc/src/flightplanning/LegsProcedure.ts index 2ff0d911eb9..7165a588d8e 100644 --- a/src/fmgc/src/flightplanning/LegsProcedure.ts +++ b/src/fmgc/src/flightplanning/LegsProcedure.ts @@ -136,6 +136,7 @@ export class LegsProcedure { try { switch (currentLeg.type) { case LegType.AF: + case LegType.PI: mappedLeg = this.mapExactFix(currentLeg); break; case LegType.CD: @@ -353,7 +354,7 @@ export class LegsProcedure { legDistance * LegsProcedure.distanceNormalFactorNM, prevLeg.infos.coordinates.lat, prevLeg.infos.coordinates.long, ); - const waypoint = this.buildWaypoint(`${originIdent}${Math.trunc(legDistance * LegsProcedure.distanceNormalFactorNM)}`, coordinates); + const waypoint = this.buildWaypoint(`${originIdent.substring(0, 3)}/${Math.round(leg.distance / 1852).toString().padStart(2, '0')}`, coordinates); return waypoint; } diff --git a/src/fmgc/src/flightplanning/ManagedFlightPlan.ts b/src/fmgc/src/flightplanning/ManagedFlightPlan.ts index dbec3e00694..b947263d6a7 100644 --- a/src/fmgc/src/flightplanning/ManagedFlightPlan.ts +++ b/src/fmgc/src/flightplanning/ManagedFlightPlan.ts @@ -1105,9 +1105,21 @@ export class ManagedFlightPlan { } if (approachIndex !== -1) { + const finalLegs = [...approach.finalLegs]; + // PI legs can only occur in approach vias + // if the via ends in one, we must omit the IF leg at the start of the approach + const viaLastLegType = legs[legs.length - 1]?.type; + if (viaLastLegType === LegType.PI && finalLegs[0]?.type === LegType.IF) { + finalLegs.splice(0, 1); + // @ts-expect-error (ts compiler doesn't see that splice mutates finalLegs) + if (finalLegs[0]?.type !== LegType.CF) { + console.error('PI must be followed by CF!'); + } + } + this.procedureDetails.approachType = approach.approachType; - legs.push(...approach.finalLegs); - legAnnotations.push(...approach.finalLegs.map(_ => approachName)); + legs.push(...finalLegs); + legAnnotations.push(...finalLegs.map(_ => approachName)); missedLegs.push(...approach.missedLegs); } diff --git a/src/fmgc/src/guidance/GuidanceManager.ts b/src/fmgc/src/guidance/GuidanceManager.ts index 21cdabea1f2..e07631d4c7e 100644 --- a/src/fmgc/src/guidance/GuidanceManager.ts +++ b/src/fmgc/src/guidance/GuidanceManager.ts @@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0 import { HALeg, HFLeg, HMLeg } from '@fmgc/guidance/lnav/legs/HX'; +import { PILeg } from '@fmgc/guidance/lnav/legs/PI'; import { RFLeg } from '@fmgc/guidance/lnav/legs/RF'; import { TFLeg } from '@fmgc/guidance/lnav/legs/TF'; import { VMLeg } from '@fmgc/guidance/lnav/legs/VM'; @@ -137,6 +138,10 @@ export class GuidanceManager { if (to.additionalData?.legType === LegType.HM) { return new HMLeg(to, metadata, segment); } + + if (to.additionalData.legType === LegType.PI) { + return new PILeg(to, nextLeg as CFLeg, metadata, segment); + } } if (to.isVectors) { diff --git a/src/fmgc/src/guidance/lnav/TransitionPicker.ts b/src/fmgc/src/guidance/lnav/TransitionPicker.ts index b2bbd75f5e3..3665b01a07a 100644 --- a/src/fmgc/src/guidance/lnav/TransitionPicker.ts +++ b/src/fmgc/src/guidance/lnav/TransitionPicker.ts @@ -21,6 +21,7 @@ import { CRLeg } from '@fmgc/guidance/lnav/legs/CR'; import { CILeg } from '@fmgc/guidance/lnav/legs/CI'; import { AFLeg } from '@fmgc/guidance/lnav/legs/AF'; import { DmeArcTransition } from '@fmgc/guidance/lnav/transitions/DmeArcTransition'; +import { PILeg } from '@fmgc/guidance/lnav/legs/PI'; export class TransitionPicker { static forLegs(from: Leg, to: Leg): Transition | null { @@ -39,6 +40,9 @@ export class TransitionPicker { if (from instanceof HALeg || from instanceof HFLeg || from instanceof HMLeg) { return TransitionPicker.fromHX(from, to); } + if (from instanceof PILeg) { + return TransitionPicker.fromPI(from, to); + } if (from instanceof RFLeg) { return TransitionPicker.fromRF(from, to); } @@ -140,6 +144,9 @@ export class TransitionPicker { if (to instanceof HALeg || to instanceof HFLeg || to instanceof HMLeg) { return new HoldEntryTransition(from, to); } + if (to instanceof PILeg) { + return new FixedRadiusTransition(from, to); + } if (to instanceof TFLeg) { return new FixedRadiusTransition(from, to); } @@ -191,6 +198,9 @@ export class TransitionPicker { if (to instanceof HALeg || to instanceof HFLeg || to instanceof HMLeg) { return new HoldEntryTransition(from, to); } + if (to instanceof PILeg) { + return new FixedRadiusTransition(from, to); + } if (to instanceof CILeg) { return new CourseCaptureTransition(from, to); } @@ -244,6 +254,14 @@ export class TransitionPicker { return null; } + private static fromPI(from: PILeg, to: Leg): Transition | null { + if (!(to instanceof CFLeg)) { + console.error('PI -> !CF', from, to); + } + + return null; + } + private static fromRF(from: RFLeg, to: Leg): Transition | null { if (to instanceof CALeg) { return new CourseCaptureTransition(from, to); @@ -279,6 +297,9 @@ export class TransitionPicker { if (to instanceof HALeg || to instanceof HFLeg || to instanceof HMLeg) { return new HoldEntryTransition(from, to); } + if (to instanceof PILeg) { + return new FixedRadiusTransition(from, to); + } if (to instanceof TFLeg) { return new FixedRadiusTransition(from, to); } diff --git a/src/fmgc/src/guidance/lnav/legs/HX.ts b/src/fmgc/src/guidance/lnav/legs/HX.ts index 6ab88320f93..f2dbe4a4ef8 100644 --- a/src/fmgc/src/guidance/lnav/legs/HX.ts +++ b/src/fmgc/src/guidance/lnav/legs/HX.ts @@ -9,7 +9,7 @@ import { GuidanceParameters, LateralPathGuidance } from '@fmgc/guidance/ControlL import { Geometry } from '@fmgc/guidance/Geometry'; import { AltitudeDescriptor, TurnDirection } from '@fmgc/types/fstypes/FSEnums'; import { SegmentType } from '@fmgc/wtsdk'; -import { arcDistanceToGo, arcGuidance, courseToFixDistanceToGo, courseToFixGuidance, maxBank } from '@fmgc/guidance/lnav/CommonGeometry'; +import { arcDistanceToGo, arcGuidance, courseToFixDistanceToGo, courseToFixGuidance, maxBank, reciprocal } from '@fmgc/guidance/lnav/CommonGeometry'; import { XFLeg } from '@fmgc/guidance/lnav/legs/XF'; import { LegMetadata } from '@fmgc/guidance/lnav/legs/index'; import { EntryState, HoldEntryTransition } from '@fmgc/guidance/lnav/transitions/HoldEntryTransition'; @@ -394,7 +394,7 @@ abstract class HXLeg extends XFLeg { updatePrediction() { const windDirection = SimVar.GetSimVarValue('AMBIENT WIND DIRECTION', 'Degrees'); const windSpeed = SimVar.GetSimVarValue('AMBIENT WIND VELOCITY', 'Knots'); - const windAngleToInbound = Math.abs(Avionics.Utils.diffAngle(windDirection, this.inboundCourse)); + const windAngleToInbound = Math.abs(Avionics.Utils.diffAngle(reciprocal(windDirection), this.inboundLegCourse)); this.inboundWindSpeed = Math.cos(windAngleToInbound * Math.PI / 180) * windSpeed; this.currentPredictedTas = this.nextPredictedTas; diff --git a/src/fmgc/src/guidance/lnav/legs/Leg.ts b/src/fmgc/src/guidance/lnav/legs/Leg.ts index 1f50d1d7031..e838f499ece 100644 --- a/src/fmgc/src/guidance/lnav/legs/Leg.ts +++ b/src/fmgc/src/guidance/lnav/legs/Leg.ts @@ -61,4 +61,8 @@ export abstract class Leg extends Guidable { get overflyTermFix(): boolean { return false; } + + get initialLegTermPoint(): Coordinates { + return this.getPathEndPoint(); + } } diff --git a/src/fmgc/src/guidance/lnav/legs/PI.ts b/src/fmgc/src/guidance/lnav/legs/PI.ts new file mode 100644 index 00000000000..ec1895eeb33 --- /dev/null +++ b/src/fmgc/src/guidance/lnav/legs/PI.ts @@ -0,0 +1,440 @@ +// Copyright (c) 2022 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { Coordinates } from '@fmgc/flightplanning/data/geo'; +import { GuidanceParameters } from '@fmgc/guidance/ControlLaws'; +import { Geometry } from '@fmgc/guidance/Geometry'; +import { arcDistanceToGo, arcGuidance, courseToFixDistanceToGo, courseToFixGuidance, maxBank } from '@fmgc/guidance/lnav/CommonGeometry'; +import { LegMetadata } from '@fmgc/guidance/lnav/legs'; +import { CFLeg } from '@fmgc/guidance/lnav/legs/CF'; +import { Leg } from '@fmgc/guidance/lnav/legs/Leg'; +import { DebugPointColour, PathVector, PathVectorType } from '@fmgc/guidance/lnav/PathVector'; +import { LnavConfig } from '@fmgc/guidance/LnavConfig'; +import { TurnDirection } from '@fmgc/types/fstypes/FSEnums'; +import { SegmentType } from '@fmgc/wtsdk'; +import { bearingTo, distanceTo, smallCircleGreatCircleIntersection } from 'msfs-geo'; + +interface Segment { + itp?: Coordinates, + arcCentre?: Coordinates, + ftp?: Coordinates, + course?: DegreesTrue, + sweepAngle?: Degrees, + length?: NauticalMiles, +} + +enum PiState { + Straight, + Turn1, + Outbound, + Turn2, + Intercept, +} + +export class PILeg extends Leg { + private radius: NauticalMiles = 1; + + private straight: Segment = {}; + + private turn1: Segment = {}; + + private outbound: Segment = {}; + + private turn2: Segment = {}; + + private intercept: Segment = {}; + + private state: PiState = PiState.Straight; + + private debugPoints: PathVector[] = []; + + constructor( + public fix: WayPoint, + private nextLeg: CFLeg, + public metadata: LegMetadata, + public segment: SegmentType, + ) { + super(); + + this.recomputeWithParameters(false, 220, 220, { lat: 0, long: 0 }, 0); + } + + recomputeWithParameters(isActive: boolean, tas: number, gs: number, _ppos: Coordinates, _trueTrack: number): void { + if (isActive) { + return; + } + + if (this.nextLeg && !(this.nextLeg instanceof CFLeg)) { + throw new Error('PI must be followed by CF!'); + } else if (!this.nextLeg) { + return; + } + + this.debugPoints.length = 0; + + const turn1Sign = this.fix.turnDirection === TurnDirection.Left ? 1 : -1; + const turn2Sign = -1 * turn1Sign; + + const gsMs = gs / 1.94384; + this.radius = (gsMs ** 2 / (9.81 * Math.tan(maxBank(tas, true) * Math.PI / 180)) / 1852); + + const minStraightDist = this.radius * 2; + + const brgToCf = Avionics.Utils.computeGreatCircleHeading( + this.fix.infos.coordinates, + this.nextLeg.fix.infos.coordinates, + ); + + const distToCf = Avionics.Utils.computeGreatCircleDistance( + this.fix.infos.coordinates, + this.nextLeg.fix.infos.coordinates, + ); + + const cfInverseCrs = (this.nextLeg.course + 180) % 360; + this.outbound.course = this.fix.additionalData.course; + + this.straight.itp = this.fix.infos.coordinates; + this.straight.course = cfInverseCrs; + + let tp: Coordinates; + if (Math.abs(Avionics.Utils.diffAngle(cfInverseCrs, brgToCf)) < 90 && distToCf > minStraightDist) { + tp = this.nextLeg.fix.infos.coordinates; + } else { + // find an intercept on the CF at min dist + [tp] = smallCircleGreatCircleIntersection( + this.fix.infos.coordinates, + minStraightDist, + this.nextLeg.fix.infos.coordinates, + cfInverseCrs, + ).filter((p) => Math.abs(Avionics.Utils.diffAngle(cfInverseCrs, bearingTo(this.nextLeg.fix.infos.coordinates, p))) < 90); + + this.straight.course = Avionics.Utils.computeGreatCircleHeading( + this.fix.infos.coordinates, + tp, + ); + } + + this.turn1.sweepAngle = turn1Sign * Math.abs(Avionics.Utils.diffAngle(this.straight.course, this.outbound.course)); + const tpT1FtpDist = this.radius * Math.tan(Math.abs(this.turn1.sweepAngle) * Math.PI / 360); + this.turn1.ftp = Avionics.Utils.bearingDistanceToCoordinates( + this.outbound.course, + tpT1FtpDist, + tp.lat, + tp.long, + ); + this.turn1.arcCentre = Avionics.Utils.bearingDistanceToCoordinates( + (360 + this.outbound.course + turn1Sign * 90) % 360, + this.radius, + this.turn1.ftp.lat, + this.turn1.ftp.long, + ); + this.turn1.itp = Avionics.Utils.bearingDistanceToCoordinates( + (this.straight.course + 180) % 360, + this.radius * (1 - Math.cos(this.turn1.sweepAngle * Math.PI / 180)), + tp.lat, + tp.long, + ); + this.turn1.length = Math.abs(this.turn1.sweepAngle / 180 * this.radius); + + this.straight.ftp = this.turn1.itp; + this.straight.length = Avionics.Utils.computeGreatCircleDistance( + this.fix.infos.coordinates, + this.turn1.itp, + ); + + if (LnavConfig.DEBUG_PREDICTED_PATH) { + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: tp, + annotation: 'TP', + colour: DebugPointColour.Yellow, + }); + + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: this.turn1.itp, + annotation: 'ITP1', + colour: DebugPointColour.Magenta, + }); + + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: this.turn1.arcCentre, + annotation: 'AC1', + colour: DebugPointColour.Magenta, + }); + + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: this.turn1.ftp, + annotation: 'FTP1', + colour: DebugPointColour.Magenta, + }); + } + + const theta = Math.abs(Avionics.Utils.diffAngle(this.outbound.course, (this.nextLeg.course + 180) % 360)) * Math.PI / 180; + this.outbound.length = this.radius * (1 / Math.tan(theta / 2)); + this.outbound.itp = this.turn1.ftp; + + this.turn2.itp = Avionics.Utils.bearingDistanceToCoordinates( + this.outbound.course, + this.outbound.length + tpT1FtpDist, + tp.lat, + tp.long, + ); + this.turn2.arcCentre = Avionics.Utils.bearingDistanceToCoordinates( + (360 + this.outbound.course + turn2Sign * 90) % 360, + this.radius, + this.turn2.itp.lat, + this.turn2.itp.long, + ); + this.turn2.sweepAngle = turn2Sign * 180; + this.turn2.ftp = Avionics.Utils.bearingDistanceToCoordinates( + (360 + this.outbound.course + turn2Sign * 90) % 360, + this.radius, + this.turn2.arcCentre.lat, + this.turn2.arcCentre.long, + ); + this.turn2.length = Math.abs(this.turn2.sweepAngle / 180 * this.radius); + + this.outbound.ftp = this.turn2.itp; + + if (LnavConfig.DEBUG_PREDICTED_PATH) { + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: this.turn2.itp, + annotation: 'ITP2', + colour: DebugPointColour.Cyan, + }); + + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: this.turn2.arcCentre, + annotation: 'AC2', + colour: DebugPointColour.Cyan, + }); + + this.debugPoints.push({ + type: PathVectorType.DebugPoint, + startPoint: this.turn2.ftp, + annotation: 'FTP2', + colour: DebugPointColour.Cyan, + }); + } + + this.intercept.itp = this.turn2.ftp; + this.intercept.ftp = A32NX_Util.greatCircleIntersection( + this.turn2.ftp, + (this.outbound.course + 180) % 360, + tp, + cfInverseCrs, + ); + this.intercept.length = Avionics.Utils.computeGreatCircleDistance( + this.intercept.itp, + this.intercept.ftp, + ); + this.intercept.course = Avionics.Utils.computeGreatCircleHeading( + this.intercept.itp, + this.intercept.ftp, + ); + + this.isComputed = true; + } + + get initialLegTermPoint(): Coordinates { + return this.turn1.itp; + } + + get distanceToTermination(): NauticalMiles { + return this.straight.length; + } + + get distance(): NauticalMiles { + return this.intercept.length + this.turn2.length + this.outbound.length + this.turn1.length + this.straight.length; + } + + /** + * Do we end up further away from the fix than the coded limit + */ + get turnAreaExceeded(): boolean { + if (!this.turn2) { + return false; + } + + const maxExcursion = distanceTo( + this.fix.infos.coordinates, + this.turn2.arcCentre, + ) + this.radius; + + return maxExcursion > this.fix.additionalData.distance; + } + + getDistanceToGo(ppos: Coordinates): NauticalMiles { + switch (this.state) { + case PiState.Intercept: + return courseToFixDistanceToGo(ppos, this.intercept.course, this.intercept.ftp); + case PiState.Turn2: + return this.intercept.length + arcDistanceToGo(ppos, this.turn2.itp, this.turn2.arcCentre, this.turn2.sweepAngle); + case PiState.Outbound: + return this.intercept.length + this.turn2.length + courseToFixDistanceToGo(ppos, this.outbound.course, this.outbound.ftp); + case PiState.Turn1: + return this.intercept.length + this.turn2.length + this.outbound.length + arcDistanceToGo(ppos, this.turn1.itp, this.turn1.arcCentre, this.turn1.sweepAngle); + case PiState.Straight: + return this.intercept.length + this.turn2.length + this.outbound.length + this.turn1.length + courseToFixDistanceToGo(ppos, this.straight.course, this.straight.ftp); + default: + return 1; + } + } + + private dtgCurrentSegment(ppos: Coordinates): NauticalMiles { + switch (this.state) { + case PiState.Intercept: + return courseToFixDistanceToGo(ppos, this.intercept.course, this.intercept.ftp); + case PiState.Turn2: + return arcDistanceToGo(ppos, this.turn2.itp, this.turn2.arcCentre, this.turn2.sweepAngle); + case PiState.Outbound: + return courseToFixDistanceToGo(ppos, this.outbound.course, this.outbound.ftp); + case PiState.Turn1: + return arcDistanceToGo(ppos, this.turn1.itp, this.turn1.arcCentre, this.turn1.sweepAngle); + case PiState.Straight: + return courseToFixDistanceToGo(ppos, this.straight.course, this.straight.ftp); + default: + return 0; + } + } + + private radCurrentSegment(tas: Knots, gs: Knots): [NauticalMiles, Degrees] { + const turn1Sign = this.fix.turnDirection === TurnDirection.Left ? 1 : -1; + const turn2Sign = -1 * turn1Sign; + + let currentBank; + let nextBank; + switch (this.state) { + case PiState.Turn1: + currentBank = turn1Sign * maxBank(tas, true); + nextBank = 0; + break; + case PiState.Turn2: + currentBank = turn2Sign * maxBank(tas, true); + nextBank = 0; + break; + case PiState.Straight: + currentBank = 0; + nextBank = turn1Sign * maxBank(tas, true); + break; + case PiState.Outbound: + currentBank = 0; + nextBank = turn2Sign * maxBank(tas, true); + break; + default: + return [0, 0]; + } + + return [Geometry.getRollAnticipationDistance(gs, currentBank, nextBank), nextBank]; + } + + getGuidanceParameters(ppos: Coordinates, trueTrack: number, tas: number, gs: number): GuidanceParameters { + let dtg = this.dtgCurrentSegment(ppos); + if (dtg <= 0 && this.state < PiState.Intercept) { + this.state++; + dtg = this.dtgCurrentSegment(ppos); + } + + let params; + switch (this.state) { + case PiState.Intercept: + return this.nextLeg?.getGuidanceParameters(ppos, trueTrack, tas); + case PiState.Turn2: + return arcGuidance(ppos, trueTrack, this.turn2.itp, this.turn2.arcCentre, this.turn2.sweepAngle); + case PiState.Outbound: + params = courseToFixGuidance(ppos, trueTrack, this.outbound.course, this.outbound.ftp); + break; + case PiState.Turn1: + params = arcGuidance(ppos, trueTrack, this.turn1.itp, this.turn1.arcCentre, this.turn1.sweepAngle); + break; + case PiState.Straight: + params = courseToFixGuidance(ppos, trueTrack, this.straight.course, this.straight.ftp); + break; + default: + } + + const [rad, nextBank] = this.radCurrentSegment(tas, gs); + + if (params && rad > 0 && dtg <= rad) { + params.phiCommand = nextBank; + } + + return params; + } + + getNominalRollAngle(_gs: number): number { + return 0; + } + + getPathStartPoint(): Coordinates { + return this.inboundGuidable?.isComputed ? this.inboundGuidable.getPathEndPoint() : this.fix.infos.coordinates; + } + + getPathEndPoint(): Coordinates { + return this.intercept.ftp; + } + + get terminationWaypoint(): WayPoint | Coordinates { + return this.intercept.ftp; + } + + get inboundCourse(): number { + return this.straight.course ?? 0; + } + + get outboundCourse(): number { + return this.nextLeg?.course ?? 0; + } + + isAbeam(_ppos: Coordinates): boolean { + return true; // TODO y needed + } + + get predictedPath(): PathVector[] { + return [ + { + type: PathVectorType.Line, + startPoint: this.inboundGuidable?.isComputed ? this.inboundGuidable.getPathEndPoint() : this.fix.infos.coordinates, + endPoint: this.turn1.itp, + }, + { + type: PathVectorType.Arc, + startPoint: this.turn1.itp, + centrePoint: this.turn1.arcCentre, + endPoint: this.turn1.ftp, + sweepAngle: this.turn1.sweepAngle, + }, + { + type: PathVectorType.Line, + startPoint: this.turn1.ftp, + endPoint: this.turn2.itp, + }, + { + type: PathVectorType.Arc, + startPoint: this.turn2.itp, + centrePoint: this.turn2.arcCentre, + endPoint: this.turn2.ftp, + sweepAngle: this.turn2.sweepAngle, + }, + { + type: PathVectorType.Line, + startPoint: this.turn2.ftp, + endPoint: this.intercept.ftp, + }, + ...this.debugPoints, + ]; + } + + get ident(): string { + return 'INTCPT'; + } + + get repr(): string { + return `PI ${this.ident}`; + } +} diff --git a/src/fmgc/src/guidance/lnav/legs/index.ts b/src/fmgc/src/guidance/lnav/legs/index.ts index df133464be4..7007d50fdd4 100644 --- a/src/fmgc/src/guidance/lnav/legs/index.ts +++ b/src/fmgc/src/guidance/lnav/legs/index.ts @@ -5,6 +5,7 @@ import { HALeg, HFLeg, HMLeg } from '@fmgc/guidance/lnav/legs/HX'; import { Leg } from '@fmgc/guidance/lnav/legs/Leg'; +import { PILeg } from '@fmgc/guidance/lnav/legs/PI'; import { TurnDirection } from '@fmgc/types/fstypes/FSEnums'; export enum AltitudeConstraintType { @@ -86,7 +87,7 @@ export function isHold(leg: Leg): boolean { } export function isCourseReversalLeg(leg: Leg): boolean { - return isHold(leg); // TODO PILeg + return isHold(leg) || leg instanceof PILeg; } /** diff --git a/src/fmgc/src/guidance/lnav/transitions/CourseCaptureTransition.ts b/src/fmgc/src/guidance/lnav/transitions/CourseCaptureTransition.ts index ea604f12f05..a5e2cc1f0fe 100644 --- a/src/fmgc/src/guidance/lnav/transitions/CourseCaptureTransition.ts +++ b/src/fmgc/src/guidance/lnav/transitions/CourseCaptureTransition.ts @@ -19,6 +19,7 @@ import { PathVector, PathVectorType } from '@fmgc/guidance/lnav/PathVector'; import { TurnDirection } from '@fmgc/types/fstypes/FSEnums'; import { LnavConfig } from '@fmgc/guidance/LnavConfig'; import { AFLeg } from '@fmgc/guidance/lnav/legs/AF'; +import { ControlLaw } from '@shared/autopilot'; import { arcDistanceToGo, arcLength, maxBank } from '../CommonGeometry'; import { CFLeg } from '../legs/CF'; import { CRLeg } from '../legs/CR'; @@ -78,9 +79,15 @@ export class CourseCaptureTransition extends Transition { public predictedPath: PathVector[] = []; + private forcedTurnComplete = false; + + private computedTurnDirection = TurnDirection.Either; + recomputeWithParameters(_isActive: boolean, tas: Knots, gs: Knots, ppos: Coordinates, _trueTrack: DegreesTrue) { const termFix = this.previousLeg.getPathEndPoint(); + this.computedTurnDirection = TurnDirection.Either; + let courseChange; let initialTurningPoint; if (!this.inboundGuidable) { @@ -128,6 +135,8 @@ export class CourseCaptureTransition extends Transition { return; } + this.computedTurnDirection = this.clockwise ? TurnDirection.Right : TurnDirection.Left; + this.isNull = false; this.isArc = true; this.startPoint = initialTurningPoint; @@ -162,15 +171,7 @@ export class CourseCaptureTransition extends Transition { } isAbeam(ppos: LatLongData): boolean { - const [inbound, outbound] = this.getTurningPoints(); - - const inBearingAc = Avionics.Utils.computeGreatCircleHeading(inbound, ppos); - const inHeadingAc = Math.abs(MathUtils.diffAngle(this.previousLeg.outboundCourse, inBearingAc)); - - const outBearingAc = Avionics.Utils.computeGreatCircleHeading(outbound, ppos); - const outHeadingAc = Math.abs(MathUtils.diffAngle(this.nextLeg.inboundCourse, outBearingAc)); - - return inHeadingAc <= 90 && outHeadingAc >= 90; + return !this.isNull && this.computedTurnDirection !== TurnDirection.Either && !this.forcedTurnComplete && this.previousLeg.getDistanceToGo(ppos) <= 0; } get distance(): NauticalMiles { @@ -192,6 +193,24 @@ export class CourseCaptureTransition extends Transition { } getGuidanceParameters(ppos: LatLongAlt, trueTrack: number, tas: Knots, gs: Knots): GuidanceParameters | null { + if (this.computedTurnDirection !== TurnDirection.Either) { + const turnSign = this.computedTurnDirection === TurnDirection.Left ? -1 : 1; + let trackAngleError = this.nextLeg.inboundCourse - trueTrack; + if (turnSign !== Math.sign(trackAngleError)) { + trackAngleError += turnSign * 360; + } + if (Math.abs(trackAngleError) > 130) { + const phiCommand = turnSign * maxBank(tas, false); + return { + law: ControlLaw.LATERAL_PATH, + trackAngleError: 0, + phiCommand, + crossTrackError: 0, + }; + } + this.forcedTurnComplete = true; + } + // FIXME PPOS guidance and all... return this.nextLeg.getGuidanceParameters(ppos, trueTrack, tas, gs); } diff --git a/src/fmgc/src/guidance/lnav/transitions/FixedRadiusTransition.ts b/src/fmgc/src/guidance/lnav/transitions/FixedRadiusTransition.ts index 5d6545da12b..e19edb3fa9c 100644 --- a/src/fmgc/src/guidance/lnav/transitions/FixedRadiusTransition.ts +++ b/src/fmgc/src/guidance/lnav/transitions/FixedRadiusTransition.ts @@ -5,6 +5,7 @@ import { MathUtils } from '@shared/MathUtils'; import { DFLeg } from '@fmgc/guidance/lnav/legs/DF'; +import { PILeg } from '@fmgc/guidance/lnav/legs/PI'; import { TFLeg } from '@fmgc/guidance/lnav/legs/TF'; import { Transition } from '@fmgc/guidance/lnav/Transition'; import { PathCaptureTransition } from '@fmgc/guidance/lnav/transitions/PathCaptureTransition'; @@ -22,7 +23,7 @@ import { PathVector, PathVectorType } from '../PathVector'; import { CFLeg } from '../legs/CF'; type PrevLeg = CILeg | CFLeg | DFLeg | TFLeg; -type NextLeg = CFLeg | /* FALeg | FMLeg | PILeg | */ TFLeg; +type NextLeg = CFLeg | /* FALeg | FMLeg | */ PILeg | TFLeg; const mod = (x: number, n: number) => x - Math.floor(x / n) * n; diff --git a/src/fmgc/src/guidance/lnav/transitions/PathCaptureTransition.ts b/src/fmgc/src/guidance/lnav/transitions/PathCaptureTransition.ts index 122a522c5da..31bb3f22a97 100644 --- a/src/fmgc/src/guidance/lnav/transitions/PathCaptureTransition.ts +++ b/src/fmgc/src/guidance/lnav/transitions/PathCaptureTransition.ts @@ -37,6 +37,8 @@ import { placeBearingIntersection, smallCircleGreatCircleIntersection, } from 'msfs-geo'; +import { PILeg } from '@fmgc/guidance/lnav/legs/PI'; +import { isCourseReversalLeg } from '@fmgc/guidance/lnav/legs'; import { Leg } from '../legs/Leg'; import { CFLeg } from '../legs/CF'; import { CRLeg } from '../legs/CR'; @@ -44,6 +46,7 @@ import { CRLeg } from '../legs/CR'; export type PrevLeg = AFLeg | CALeg | /* CDLeg | */ CRLeg | /* FALeg | */ HALeg | HFLeg | HMLeg; export type ReversionLeg = CFLeg | CILeg | DFLeg | TFLeg; export type NextLeg = AFLeg | CFLeg | /* FALeg | */ TFLeg; +type NextReversionLeg = PILeg; const cos = (input: Degrees) => Math.cos(input * (Math.PI / 180)); const tan = (input: Degrees) => Math.tan(input * MathUtils.DEGREES_TO_RADIANS); @@ -61,7 +64,7 @@ const compareTurnDirections = (sign: number, data: TurnDirection) => { export class PathCaptureTransition extends Transition { constructor( public previousLeg: PrevLeg | ReversionLeg, - public nextLeg: NextLeg, + public nextLeg: NextLeg | NextReversionLeg, ) { super(previousLeg, nextLeg); } @@ -88,10 +91,12 @@ export class PathCaptureTransition extends Transition { tad: NauticalMiles | undefined; - private forcedTurnRequired = false; - private forcedTurnComplete = false; + private computedTurnDirection = TurnDirection.Either; + + private computedTargetTrack: DegreesTrue = 0; + recomputeWithParameters(_isActive: boolean, tas: Knots, gs: Knots, _ppos: Coordinates, _trueTrack: DegreesTrue) { if (this.isFrozen) { return; @@ -105,6 +110,9 @@ export class PathCaptureTransition extends Transition { const naturalTurnDirectionSign = Math.sign(MathUtils.diffAngle(targetTrack, this.nextLeg.inboundCourse)); + this.computedTurnDirection = TurnDirection.Either; + this.computedTargetTrack = this.nextLeg.inboundCourse; + let prevLegTermFix: LatLongAlt | Coordinates; if (this.previousLeg instanceof AFLeg) { prevLegTermFix = this.previousLeg.arcEndPoint; @@ -141,12 +149,12 @@ export class PathCaptureTransition extends Transition { } const distanceFromItp: NauticalMiles = Geo.distanceToLeg(initialTurningPoint, this.nextLeg); - const deltaTrack: Degrees = MathUtils.diffAngle(targetTrack, this.nextLeg.inboundCourse, this.nextLeg.metadata.turnDirection); + // for some legs the turn direction is not for forced turn onto the leg + const desiredDirection = isCourseReversalLeg(this.nextLeg) ? TurnDirection.Either : this.nextLeg.metadata.turnDirection; + const deltaTrack: Degrees = MathUtils.diffAngle(targetTrack, this.nextLeg.inboundCourse, desiredDirection); this.predictedPath.length = 0; - this.forcedTurnRequired = Math.abs(deltaTrack) > 130; - if (Math.abs(deltaTrack) < 3 && distanceFromItp < 0.1) { this.itp = this.previousLeg.getPathEndPoint(); this.ftp = this.previousLeg.getPathEndPoint(); @@ -299,11 +307,15 @@ export class PathCaptureTransition extends Transition { if (isReverse) { courseChange = CourseChange.reverse(turnDirection, turnCenterDistance, deltaTrack, radius); + this.computedTurnDirection = this.nextLeg.metadata.turnDirection; } else { courseChange = CourseChange.normal(turnDirection, turnCenterDistance, deltaTrack, radius); + this.computedTurnDirection = turnDirection < 0 ? TurnDirection.Left : TurnDirection.Right; } } + this.computedTargetTrack = (360 + this.previousLeg.outboundCourse + courseChange) % 360; + const finalTurningPoint = placeBearingDistance(turnCenter, targetTrack + courseChange - 90 * turnDirection, radius); let intercept; @@ -429,7 +441,7 @@ export class PathCaptureTransition extends Transition { } isAbeam(ppos: LatLongData): boolean { - return !this.isNull && this.forcedTurnRequired && !this.forcedTurnComplete && this.previousLeg.getDistanceToGo(ppos) <= 0; + return !this.isNull && this.computedTurnDirection !== TurnDirection.Either && !this.forcedTurnComplete && this.previousLeg.getDistanceToGo(ppos) <= 0; } distance = 0; @@ -442,10 +454,10 @@ export class PathCaptureTransition extends Transition { return 1; } - getGuidanceParameters(ppos: LatLongAlt, trueTrack: number, tas: Knots): GuidanceParameters | null { - if (this.forcedTurnRequired) { - const turnSign = this.nextLeg.metadata.turnDirection === TurnDirection.Left ? -1 : 1; - let trackAngleError = this.nextLeg.inboundCourse - trueTrack; + getGuidanceParameters(ppos: LatLongAlt, trueTrack: number, tas: Knots, gs: Knots): GuidanceParameters | null { + if (this.computedTurnDirection !== TurnDirection.Either) { + const turnSign = this.computedTurnDirection === TurnDirection.Left ? -1 : 1; + let trackAngleError = this.computedTargetTrack - trueTrack; if (turnSign !== Math.sign(trackAngleError)) { trackAngleError += turnSign * 360; } @@ -461,7 +473,7 @@ export class PathCaptureTransition extends Transition { this.forcedTurnComplete = true; } - return this.nextLeg.getGuidanceParameters(ppos, trueTrack, tas); + return this.nextLeg.getGuidanceParameters(ppos, trueTrack, tas, gs); } getNominalRollAngle(_gs: Knots): Degrees { diff --git a/src/fmgc/src/guidance/vnav/FlightModel.ts b/src/fmgc/src/guidance/vnav/FlightModel.ts index 3ba3f1b13d7..2cdb70c6a57 100644 --- a/src/fmgc/src/guidance/vnav/FlightModel.ts +++ b/src/fmgc/src/guidance/vnav/FlightModel.ts @@ -70,7 +70,7 @@ export class FlightModel { break; } - const spdBrkIncrement = spdBrkDeflected ? 0.01 : 0; + const spdBrkIncrement = spdBrkDeflected ? 0.00611 : 0; const gearIncrement = gearExtended ? 0.03 : 0; return baseDrag + spdBrkIncrement + gearIncrement; } diff --git a/src/fmgc/src/navigation/Navigation.ts b/src/fmgc/src/navigation/Navigation.ts index 2a80a6e9309..00446477920 100644 --- a/src/fmgc/src/navigation/Navigation.ts +++ b/src/fmgc/src/navigation/Navigation.ts @@ -3,6 +3,7 @@ import { FlightPlanManager } from '@fmgc/index'; import { RequiredPerformance } from '@fmgc/navigation/RequiredPerformance'; +import { Coordinates } from 'msfs-geo'; export class Navigation { requiredPerformance: RequiredPerformance; @@ -11,6 +12,10 @@ export class Navigation { accuracyHigh: boolean = false; + ppos: Coordinates = { lat: 0, long: 0 }; + + groundSpeed: Knots = 0; + constructor(private flightPlanManager: FlightPlanManager) { this.requiredPerformance = new RequiredPerformance(this.flightPlanManager); } @@ -22,6 +27,8 @@ export class Navigation { this.requiredPerformance.update(deltaTime); this.updateCurrentPerformance(); + + this.updatePosition(); } private updateCurrentPerformance(): void { @@ -39,4 +46,11 @@ export class Navigation { SimVar.SetSimVarValue('L:A32NX_FMGC_R_NAV_ACCURACY_HIGH', 'bool', this.accuracyHigh); } } + + private updatePosition(): void { + this.ppos.lat = SimVar.GetSimVarValue('PLANE LATITUDE', 'degree latitude'); + this.ppos.long = SimVar.GetSimVarValue('PLANE LONGITUDE', 'degree longitude'); + + this.groundSpeed = SimVar.GetSimVarValue('GPS GROUND SPEED', 'knots'); + } } diff --git a/src/fmgc/src/utils/Geo.ts b/src/fmgc/src/utils/Geo.ts index e9750a1de71..95db6da5537 100644 --- a/src/fmgc/src/utils/Geo.ts +++ b/src/fmgc/src/utils/Geo.ts @@ -35,7 +35,7 @@ export class Geo { const intersections1 = placeBearingIntersection( from, Avionics.Utils.clampAngle(leg.outboundCourse - 90), - leg.getPathEndPoint(), + leg.initialLegTermPoint, Avionics.Utils.clampAngle(leg.outboundCourse - 180), ); diff --git a/src/instruments/src/Common/simVars.tsx b/src/instruments/src/Common/simVars.tsx index bf6948793b5..4f1d1265277 100644 --- a/src/instruments/src/Common/simVars.tsx +++ b/src/instruments/src/Common/simVars.tsx @@ -3,7 +3,7 @@ import { useInteractionEvents, useUpdate } from './hooks'; type SimVarSetter = (oldValue: T) => T; -type UnitName = string | any; // once typings is next to tsconfig.json, use those units +type UnitName = SimVar.SimVarUnit; type SimVarValue = number | any; /** diff --git a/src/instruments/src/EFB/Failures/Pages/Comfort/index.tsx b/src/instruments/src/EFB/Failures/Pages/Comfort/index.tsx index 56be58df9e5..723a5dd3ba6 100644 --- a/src/instruments/src/EFB/Failures/Pages/Comfort/index.tsx +++ b/src/instruments/src/EFB/Failures/Pages/Comfort/index.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Route } from 'react-router'; import { Link } from 'react-router-dom'; import { Failure } from '@flybywiresim/failures'; +import { ScrollableContainer } from '../../../UtilComponents/ScrollableContainer'; import { t } from '../../../translation'; import { pathify } from '../../../Utils/routing'; import { AtaChapterPage } from './AtaChapterPage'; @@ -61,13 +62,15 @@ interface ComfortUIProps { export const ComfortUI = ({ filteredChapters, allChapters, failures }: ComfortUIProps) => ( <> - {filteredChapters.map((chapter) => ( - - ))} + + {filteredChapters.map((chapter) => ( + + ))} + {filteredChapters.length === 0 && (

{t('Failures.NoItemsFound')}

diff --git a/src/instruments/src/EWD/index.tsx b/src/instruments/src/EWD/index.tsx index 1c9830678f4..39a011fa4e7 100644 --- a/src/instruments/src/EWD/index.tsx +++ b/src/instruments/src/EWD/index.tsx @@ -19,7 +19,7 @@ export const EWD: React.FC = () => { return ( diff --git a/src/model/a320-interior/A320_NEO_INTERIOR_LOD00.gltf b/src/model/a320-interior/A320_NEO_INTERIOR_LOD00.gltf index d46af141cbd..93c52918274 100644 --- a/src/model/a320-interior/A320_NEO_INTERIOR_LOD00.gltf +++ b/src/model/a320-interior/A320_NEO_INTERIOR_LOD00.gltf @@ -187840,7 +187840,6 @@ 1329, 1330, 1331, - 1334, 1335, 1336, 1337, @@ -187878,8 +187877,7 @@ 1378, 1379, 1382, - 1383, - 1384 + 1383 ] } ], diff --git a/src/model/build.js b/src/model/build.js index 4edda75c75e..b3ab8dfe06d 100644 --- a/src/model/build.js +++ b/src/model/build.js @@ -176,19 +176,21 @@ function combineGltf(pathA, pathB, outputPath) { }); mesh.primitives[0].indices += accessorsCount; // workaround to allow added meshes to use existing materials - if (!Number.isFinite(mesh.primitives[0].material)) { - for (let i = 0; i < gltfA.materials.length; i += 1) { - if (gltfA.materials[i].name === mesh.primitives[0].material) { - mesh.primitives[0].material = i; - break; + for (const primitive of mesh.primitives) { + if (!Number.isFinite(primitive.material)) { + for (let i = 0; i < gltfA.materials.length; i += 1) { + if (gltfA.materials[i].name === primitive.material) { + primitive.material = i; + break; + } } + // If the material is not found, use material 0 + if (!Number.isFinite(primitive.material)) { + primitive.material = 0; + } + } else { + primitive.material += materialsCount; } - // If the material is not found, use material 0 - if (!Number.isFinite(mesh.primitives[0].material)) { - mesh.primitives[0].material = 0; - } - } else { - mesh.primitives[0].material += materialsCount; } gltfA.meshes.push(mesh); } @@ -430,13 +432,16 @@ for (const model of models) { } } } else { - combineGltf(p(model.output.gltf[i]), p(addition.gltf), p(model.output.gltf[i])); + const maxLod = addition.maxLod != null ? addition.maxLod : Infinity; + if (i <= maxLod) { + combineGltf(p(model.output.gltf[i]), p(addition.gltf), p(model.output.gltf[i])); - // add some zeroes to the end of the bin file to make sure its length is divisible by 4 - fs.appendFileSync(p(model.output.bin[i]), Buffer.alloc((4 - (fs.statSync(p(model.output.bin[i])).size % 4)) % 4)); + // add some zeroes to the end of the bin file to make sure its length is divisible by 4 + fs.appendFileSync(p(model.output.bin[i]), Buffer.alloc((4 - (fs.statSync(p(model.output.bin[i])).size % 4)) % 4)); - // add the second bin file to the end of the first one - fs.appendFileSync(p(model.output.bin[i]), fs.readFileSync(p(addition.bin))); + // add the second bin file to the end of the first one + fs.appendFileSync(p(model.output.bin[i]), fs.readFileSync(p(addition.bin))); + } } } if (model.splitAnimations) { diff --git a/src/model/cockpit/cockpit.bin b/src/model/cockpit/cockpit.bin new file mode 100644 index 00000000000..6dc9044f1e4 Binary files /dev/null and b/src/model/cockpit/cockpit.bin differ diff --git a/src/model/cockpit/cockpit.gltf b/src/model/cockpit/cockpit.gltf new file mode 100644 index 00000000000..53298d93092 --- /dev/null +++ b/src/model/cockpit/cockpit.gltf @@ -0,0 +1,1992 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 2728, + "min": [ + -1.7160303592681885, + 0.420204222202301, + 0.06134706735610962 + ], + "max": [ + 1.326724648475647, + 1.9099429845809937, + 0.9249697923660278 + ], + "type": "VEC3", + "name": "x0_COCKPIT_BACK01_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 12, + "componentType": 5120, + "count": 2728, + "type": "VEC4", + "name": "x0_COCKPIT_BACK01_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 16, + "componentType": 5120, + "count": 2728, + "type": "VEC4", + "name": "x0_COCKPIT_BACK01_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 20, + "componentType": 5122, + "count": 2728, + "type": "VEC2", + "name": "x0_COCKPIT_BACK01_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 24, + "componentType": 5122, + "count": 2728, + "type": "VEC2", + "name": "x0_COCKPIT_BACK01_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 28, + "componentType": 5123, + "count": 2728, + "type": "VEC4", + "name": "x0_COCKPIT_BACK01_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "componentType": 5123, + "count": 9156, + "type": "SCALAR", + "name": "x0_COCKPIT_BACK01_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 98208, + "componentType": 5126, + "count": 4154, + "min": [ + -1.7529988288879395, + -0.06217685341835022, + 0.27546700835227966 + ], + "max": [ + 1.0812934637069702, + 2.0699973106384277, + 1.6066025495529175 + ], + "type": "VEC3", + "name": "x0_COCKPIT_BACK02_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 98220, + "componentType": 5120, + "count": 4154, + "type": "VEC4", + "name": "x0_COCKPIT_BACK02_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 98224, + "componentType": 5120, + "count": 4154, + "type": "VEC4", + "name": "x0_COCKPIT_BACK02_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 98228, + "componentType": 5122, + "count": 4154, + "type": "VEC2", + "name": "x0_COCKPIT_BACK02_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 98232, + "componentType": 5122, + "count": 4154, + "type": "VEC2", + "name": "x0_COCKPIT_BACK02_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 98236, + "componentType": 5123, + "count": 4154, + "type": "VEC4", + "name": "x0_COCKPIT_BACK02_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 18312, + "componentType": 5123, + "count": 11061, + "type": "SCALAR", + "name": "x0_COCKPIT_BACK02_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 247752, + "componentType": 5126, + "count": 1984, + "min": [ + -1.6846565008163452, + -0.06534881889820099, + -1.5508090257644653 + ], + "max": [ + 1.299214482307434, + 2.0375022888183594, + 1.5945782661437988 + ], + "type": "VEC3", + "name": "x0_COCKPIT_DECALSALPHA_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 247764, + "componentType": 5120, + "count": 1984, + "type": "VEC4", + "name": "x0_COCKPIT_DECALSALPHA_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 247768, + "componentType": 5120, + "count": 1984, + "type": "VEC4", + "name": "x0_COCKPIT_DECALSALPHA_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 247772, + "componentType": 5122, + "count": 1984, + "type": "VEC2", + "name": "x0_COCKPIT_DECALSALPHA_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 247776, + "componentType": 5122, + "count": 1984, + "type": "VEC2", + "name": "x0_COCKPIT_DECALSALPHA_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 247780, + "componentType": 5123, + "count": 1984, + "type": "VEC4", + "name": "x0_COCKPIT_DECALSALPHA_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 40434, + "componentType": 5123, + "count": 3684, + "type": "SCALAR", + "name": "x0_COCKPIT_DECALSALPHA_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 319176, + "componentType": 5126, + "count": 3478, + "min": [ + -1.315739393234253, + 1.0269562005996704, + -1.8179785013198853 + ], + "max": [ + 0.9477484822273254, + 2.067660331726074, + 0.7846047878265381 + ], + "type": "VEC3", + "name": "x0_COCKPIT_FLOOR_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 319188, + "componentType": 5120, + "count": 3478, + "type": "VEC4", + "name": "x0_COCKPIT_FLOOR_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 319192, + "componentType": 5120, + "count": 3478, + "type": "VEC4", + "name": "x0_COCKPIT_FLOOR_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 319196, + "componentType": 5122, + "count": 3478, + "type": "VEC2", + "name": "x0_COCKPIT_FLOOR_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 319200, + "componentType": 5122, + "count": 3478, + "type": "VEC2", + "name": "x0_COCKPIT_FLOOR_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 319204, + "componentType": 5123, + "count": 3478, + "type": "VEC4", + "name": "x0_COCKPIT_FLOOR_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 47802, + "componentType": 5123, + "count": 12294, + "type": "SCALAR", + "name": "x0_COCKPIT_FLOOR_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 444384, + "componentType": 5126, + "count": 120, + "min": [ + -1.0467132329940796, + 1.0016862154006958, + -0.3815113604068756 + ], + "max": [ + 0.6788986325263977, + 1.018474817276001, + -0.3444666564464569 + ], + "type": "VEC3", + "name": "x0_COCKPIT_INPUTS01_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 444396, + "componentType": 5120, + "count": 120, + "type": "VEC4", + "name": "x0_COCKPIT_INPUTS01_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 444400, + "componentType": 5120, + "count": 120, + "type": "VEC4", + "name": "x0_COCKPIT_INPUTS01_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 444404, + "componentType": 5122, + "count": 120, + "type": "VEC2", + "name": "x0_COCKPIT_INPUTS01_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 444408, + "componentType": 5122, + "count": 120, + "type": "VEC2", + "name": "x0_COCKPIT_INPUTS01_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 444412, + "componentType": 5123, + "count": 120, + "type": "VEC4", + "name": "x0_COCKPIT_INPUTS01_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 72390, + "componentType": 5123, + "count": 264, + "type": "SCALAR", + "name": "x0_COCKPIT_INPUTS01_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 448704, + "componentType": 5126, + "count": 32868, + "min": [ + -0.5957156419754028, + -0.038016363978385925, + -0.43538352847099304 + ], + "max": [ + 0.22822688519954681, + 0.032261695712804794, + 0.863524317741394 + ], + "type": "VEC3", + "name": "x0_COCKPIT_OVERHEAD_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 448716, + "componentType": 5120, + "count": 32868, + "type": "VEC4", + "name": "x0_COCKPIT_OVERHEAD_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 448720, + "componentType": 5120, + "count": 32868, + "type": "VEC4", + "name": "x0_COCKPIT_OVERHEAD_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 448724, + "componentType": 5122, + "count": 32868, + "type": "VEC2", + "name": "x0_COCKPIT_OVERHEAD_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 448728, + "componentType": 5122, + "count": 32868, + "type": "VEC2", + "name": "x0_COCKPIT_OVERHEAD_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 448732, + "componentType": 5123, + "count": 32868, + "type": "VEC4", + "name": "x0_COCKPIT_OVERHEAD_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 72918, + "componentType": 5123, + "count": 115020, + "type": "SCALAR", + "name": "x0_COCKPIT_OVERHEAD_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 1631952, + "componentType": 5126, + "count": 4448, + "min": [ + -1.0900219678878784, + 0.6707103848457336, + -1.6970192193984985 + ], + "max": [ + 0.7220311164855957, + 1.2384802103042603, + -1.2057383060455322 + ], + "type": "VEC3", + "name": "x0_COCKPIT_PEDALS_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 1631964, + "componentType": 5120, + "count": 4448, + "type": "VEC4", + "name": "x0_COCKPIT_PEDALS_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 1631968, + "componentType": 5120, + "count": 4448, + "type": "VEC4", + "name": "x0_COCKPIT_PEDALS_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 1631972, + "componentType": 5122, + "count": 4448, + "type": "VEC2", + "name": "x0_COCKPIT_PEDALS_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 1631976, + "componentType": 5122, + "count": 4448, + "type": "VEC2", + "name": "x0_COCKPIT_PEDALS_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 1631980, + "componentType": 5123, + "count": 4448, + "type": "VEC4", + "name": "x0_COCKPIT_PEDALS_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 302958, + "componentType": 5123, + "count": 17316, + "type": "SCALAR", + "name": "x0_COCKPIT_PEDALS_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 1792080, + "componentType": 5126, + "count": 535, + "min": [ + -1.2770382165908813, + 0.30345475673675537, + -1.187746286392212 + ], + "max": [ + 0.6767993569374084, + 1.4600638151168823, + 1.0253498554229736 + ], + "type": "VEC3", + "name": "x0_COCKPIT_PLASTICGLASS_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 1792092, + "componentType": 5120, + "count": 535, + "type": "VEC4", + "name": "x0_COCKPIT_PLASTICGLASS_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 1792096, + "componentType": 5120, + "count": 535, + "type": "VEC4", + "name": "x0_COCKPIT_PLASTICGLASS_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 1792100, + "componentType": 5122, + "count": 535, + "type": "VEC2", + "name": "x0_COCKPIT_PLASTICGLASS_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 1792104, + "componentType": 5122, + "count": 535, + "type": "VEC2", + "name": "x0_COCKPIT_PLASTICGLASS_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 1792108, + "componentType": 5123, + "count": 535, + "type": "VEC4", + "name": "x0_COCKPIT_PLASTICGLASS_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 337590, + "componentType": 5123, + "count": 1881, + "type": "SCALAR", + "name": "x0_COCKPIT_PLASTICGLASS_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 1811340, + "componentType": 5126, + "count": 7740, + "min": [ + -1.1497234106063843, + 0.5126140713691711, + -0.7016947269439697 + ], + "max": [ + 0.7817324995994568, + 1.6532886028289795, + 0.38779494166374207 + ], + "type": "VEC3", + "name": "x0_COCKPIT_SEATS_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 1811352, + "componentType": 5120, + "count": 7740, + "type": "VEC4", + "name": "x0_COCKPIT_SEATS_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 1811356, + "componentType": 5120, + "count": 7740, + "type": "VEC4", + "name": "x0_COCKPIT_SEATS_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 1811360, + "componentType": 5122, + "count": 7740, + "type": "VEC2", + "name": "x0_COCKPIT_SEATS_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 1811364, + "componentType": 5122, + "count": 7740, + "type": "VEC2", + "name": "x0_COCKPIT_SEATS_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 1811368, + "componentType": 5123, + "count": 7740, + "type": "VEC4", + "name": "x0_COCKPIT_SEATS_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 341352, + "componentType": 5123, + "count": 28914, + "type": "SCALAR", + "name": "x0_COCKPIT_SEATS_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 2089980, + "componentType": 5126, + "count": 10329, + "min": [ + -1.7220906019210815, + 0.5720301270484924, + -1.811937689781189 + ], + "max": [ + 1.3319990634918213, + 1.9113353490829468, + 0.5925940275192261 + ], + "type": "VEC3", + "name": "x0_COCKPIT_SIDEPANELS_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 2089992, + "componentType": 5120, + "count": 10329, + "type": "VEC4", + "name": "x0_COCKPIT_SIDEPANELS_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 2089996, + "componentType": 5120, + "count": 10329, + "type": "VEC4", + "name": "x0_COCKPIT_SIDEPANELS_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 2090000, + "componentType": 5122, + "count": 10329, + "type": "VEC2", + "name": "x0_COCKPIT_SIDEPANELS_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 2090004, + "componentType": 5122, + "count": 10329, + "type": "VEC2", + "name": "x0_COCKPIT_SIDEPANELS_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 2090008, + "componentType": 5123, + "count": 10329, + "type": "VEC4", + "name": "x0_COCKPIT_SIDEPANELS_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 399180, + "componentType": 5123, + "count": 38946, + "type": "SCALAR", + "name": "x0_COCKPIT_SIDEPANELS_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 2461824, + "componentType": 5126, + "count": 9901, + "min": [ + -1.7248035669326782, + 0.09337909519672394, + -1.0417966842651367 + ], + "max": [ + 1.3417145013809204, + 1.2637892961502075, + 0.6432894468307495 + ], + "type": "VEC3", + "name": "x0_COCKPIT_SIDETOP_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 2461836, + "componentType": 5120, + "count": 9901, + "type": "VEC4", + "name": "x0_COCKPIT_SIDETOP_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 2461840, + "componentType": 5120, + "count": 9901, + "type": "VEC4", + "name": "x0_COCKPIT_SIDETOP_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 2461844, + "componentType": 5122, + "count": 9901, + "type": "VEC2", + "name": "x0_COCKPIT_SIDETOP_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 2461848, + "componentType": 5122, + "count": 9901, + "type": "VEC2", + "name": "x0_COCKPIT_SIDETOP_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 2461852, + "componentType": 5123, + "count": 9901, + "type": "VEC4", + "name": "x0_COCKPIT_SIDETOP_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 477072, + "componentType": 5123, + "count": 38154, + "type": "SCALAR", + "name": "x0_COCKPIT_SIDETOP_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 2818260, + "componentType": 5126, + "count": 104, + "min": [ + -0.7777349352836609, + 0.7477385997772217, + -1.3386439085006714 + ], + "max": [ + 0.40925663709640503, + 1.2983571290969849, + -0.05757951736450195 + ], + "type": "VEC3", + "name": "x0_COCKPIT_TEXT_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 2818272, + "componentType": 5120, + "count": 104, + "type": "VEC4", + "name": "x0_COCKPIT_TEXT_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 2818276, + "componentType": 5120, + "count": 104, + "type": "VEC4", + "name": "x0_COCKPIT_TEXT_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 2818280, + "componentType": 5122, + "count": 104, + "type": "VEC2", + "name": "x0_COCKPIT_TEXT_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 2818284, + "componentType": 5122, + "count": 104, + "type": "VEC2", + "name": "x0_COCKPIT_TEXT_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 2818288, + "componentType": 5123, + "count": 104, + "type": "VEC4", + "name": "x0_COCKPIT_TEXT_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 553380, + "componentType": 5123, + "count": 168, + "type": "SCALAR", + "name": "x0_COCKPIT_TEXT_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 2822004, + "componentType": 5126, + "count": 165, + "min": [ + -1.0592583417892456, + -0.03582889586687088, + -0.3984667956829071 + ], + "max": [ + 0.691586434841156, + 1.0205332040786743, + 0.20397257804870605 + ], + "type": "VEC3", + "name": "x0_COCKPIT_TOPGLASS_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 2822016, + "componentType": 5120, + "count": 165, + "type": "VEC4", + "name": "x0_COCKPIT_TOPGLASS_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 2822020, + "componentType": 5120, + "count": 165, + "type": "VEC4", + "name": "x0_COCKPIT_TOPGLASS_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 2822024, + "componentType": 5122, + "count": 165, + "type": "VEC2", + "name": "x0_COCKPIT_TOPGLASS_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 2822028, + "componentType": 5122, + "count": 165, + "type": "VEC2", + "name": "x0_COCKPIT_TOPGLASS_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 2822032, + "componentType": 5123, + "count": 165, + "type": "VEC4", + "name": "x0_COCKPIT_TOPGLASS_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 553716, + "componentType": 5123, + "count": 504, + "type": "SCALAR", + "name": "x0_COCKPIT_TOPGLASS_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 2827944, + "componentType": 5126, + "count": 7930, + "min": [ + -1.442156434059143, + -0.09175551682710648, + -0.7729623317718506 + ], + "max": [ + 1.07416570186615, + 0.5964341163635254, + 1.6307673454284668 + ], + "type": "VEC3", + "name": "x0_COCKPIT_TOPPANEL_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 2827956, + "componentType": 5120, + "count": 7930, + "type": "VEC4", + "name": "x0_COCKPIT_TOPPANEL_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 2827960, + "componentType": 5120, + "count": 7930, + "type": "VEC4", + "name": "x0_COCKPIT_TOPPANEL_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 2827964, + "componentType": 5122, + "count": 7930, + "type": "VEC2", + "name": "x0_COCKPIT_TOPPANEL_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 2827968, + "componentType": 5122, + "count": 7930, + "type": "VEC2", + "name": "x0_COCKPIT_TOPPANEL_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 2827972, + "componentType": 5123, + "count": 7930, + "type": "VEC4", + "name": "x0_COCKPIT_TOPPANEL_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 554724, + "componentType": 5123, + "count": 29727, + "type": "SCALAR", + "name": "x0_COCKPIT_TOPPANEL_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 3113424, + "componentType": 5126, + "count": 5242, + "min": [ + -1.0312665700912476, + -0.03509041666984558, + -1.3736284971237183 + ], + "max": [ + 0.6632757186889648, + 0.3814640939235687, + -0.6902344226837158 + ], + "type": "VEC3", + "name": "x0_COCKPIT_GLARESHIELD_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 3113436, + "componentType": 5120, + "count": 5242, + "type": "VEC4", + "name": "x0_COCKPIT_GLARESHIELD_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 3113440, + "componentType": 5120, + "count": 5242, + "type": "VEC4", + "name": "x0_COCKPIT_GLARESHIELD_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 3113444, + "componentType": 5122, + "count": 5242, + "type": "VEC2", + "name": "x0_COCKPIT_GLARESHIELD_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 3113448, + "componentType": 5122, + "count": 5242, + "type": "VEC2", + "name": "x0_COCKPIT_GLARESHIELD_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 3113452, + "componentType": 5123, + "count": 5242, + "type": "VEC4", + "name": "x0_COCKPIT_GLARESHIELD_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 614178, + "componentType": 5123, + "count": 21408, + "type": "SCALAR", + "name": "x0_COCKPIT_GLARESHIELD_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 3302136, + "componentType": 5126, + "count": 16763, + "min": [ + -1.2726854085922241, + 0.07592281699180603, + -1.8166934251785278 + ], + "max": [ + 0.9046947360038757, + 1.2208442687988281, + 1.5540235042572021 + ], + "type": "VEC3", + "name": "x0_COCKPIT_MAINPANEL_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 3302148, + "componentType": 5120, + "count": 16763, + "type": "VEC4", + "name": "x0_COCKPIT_MAINPANEL_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 3302152, + "componentType": 5120, + "count": 16763, + "type": "VEC4", + "name": "x0_COCKPIT_MAINPANEL_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 3302156, + "componentType": 5122, + "count": 16763, + "type": "VEC2", + "name": "x0_COCKPIT_MAINPANEL_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 3302160, + "componentType": 5122, + "count": 16763, + "type": "VEC2", + "name": "x0_COCKPIT_MAINPANEL_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 3302164, + "componentType": 5123, + "count": 16763, + "type": "VEC4", + "name": "x0_COCKPIT_MAINPANEL_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 656994, + "componentType": 5123, + "count": 62325, + "type": "SCALAR", + "name": "x0_COCKPIT_MAINPANEL_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 3905604, + "componentType": 5126, + "count": 27362, + "min": [ + -0.48907798528671265, + 0.7931820750236511, + -1.3459279537200928 + ], + "max": [ + 0.12108717858791351, + 1.5497113466262817, + -0.378397673368454 + ], + "type": "VEC3", + "name": "x0_COCKPIT_PEDESTAL_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 3905616, + "componentType": 5120, + "count": 27362, + "type": "VEC4", + "name": "x0_COCKPIT_PEDESTAL_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 3905620, + "componentType": 5120, + "count": 27362, + "type": "VEC4", + "name": "x0_COCKPIT_PEDESTAL_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 3905624, + "componentType": 5122, + "count": 27362, + "type": "VEC2", + "name": "x0_COCKPIT_PEDESTAL_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 3905628, + "componentType": 5122, + "count": 27362, + "type": "VEC2", + "name": "x0_COCKPIT_PEDESTAL_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 3905632, + "componentType": 5123, + "count": 27362, + "type": "VEC4", + "name": "x0_COCKPIT_PEDESTAL_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 781644, + "componentType": 5123, + "count": 97362, + "type": "SCALAR", + "name": "x0_COCKPIT_PEDESTAL_indices#0" + }, + { + "bufferView": 0, + "byteOffset": 4890636, + "componentType": 5126, + "count": 38850, + "min": [ + -1.3913885354995728, + -0.004859656095504761, + -0.9819670915603638 + ], + "max": [ + 0.9155333638191223, + 1.5530450344085693, + 1.0820602178573608 + ], + "type": "VEC3", + "name": "x0_COCKPIT_THROTTLE_vertices#0_POSITION" + }, + { + "bufferView": 0, + "byteOffset": 4890648, + "componentType": 5120, + "count": 38850, + "type": "VEC4", + "name": "x0_COCKPIT_THROTTLE_vertices#0_TANGENT" + }, + { + "bufferView": 0, + "byteOffset": 4890652, + "componentType": 5120, + "count": 38850, + "type": "VEC4", + "name": "x0_COCKPIT_THROTTLE_vertices#0_NORMAL" + }, + { + "bufferView": 0, + "byteOffset": 4890656, + "componentType": 5122, + "count": 38850, + "type": "VEC2", + "name": "x0_COCKPIT_THROTTLE_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 0, + "byteOffset": 4890660, + "componentType": 5122, + "count": 38850, + "type": "VEC2", + "name": "x0_COCKPIT_THROTTLE_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 0, + "byteOffset": 4890664, + "componentType": 5123, + "count": 38850, + "type": "VEC4", + "name": "x0_COCKPIT_THROTTLE_vertices#0_COLOR_0" + }, + { + "bufferView": 1, + "byteOffset": 976368, + "componentType": 5123, + "count": 208026, + "type": "SCALAR", + "name": "x0_COCKPIT_THROTTLE_indices#0" + } + ], + "asset": { + "generator": "Khronos glTF Blender I/O v3.2.40 and Asobo Studio MSFS Blender I/O v1.1.6", + "version": "2.0", + "extensions": { + "ASOBO_asset_optimized": { + "BoundingBoxMax": [ + 1.3417145013809204, + 2.0699973106384277, + 1.6307673454284668 + ], + "BoundingBoxMin": [ + -1.7529988288879395, + -0.09175551682710648, + -1.8179785013198853 + ], + "MajorVersion": 4, + "MinorVersion": 4, + "UseCheckerboardMaterialForMissingTextures": true, + "UseOnlyFilenameForImageURI": true + }, + "ASOBO_normal_map_convention": { + "tangent_space_convention": "DirectX" + } + } + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 6289236, + "byteStride": 36, + "target": 34962, + "name": "BufferViewVertexND" + }, + { + "buffer": 0, + "byteLength": 1392420, + "byteOffset": 6289236, + "target": 34963, + "name": "BufferViewIndex" + } + ], + "materials": [], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "COLOR_0": 5, + "NORMAL": 2, + "POSITION": 0, + "TANGENT": 1, + "TEXCOORD_0": 3, + "TEXCOORD_1": 4 + }, + "indices": 6, + "material": "BACK01", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 3052, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_BACK01" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 12, + "NORMAL": 9, + "POSITION": 7, + "TANGENT": 8, + "TEXCOORD_0": 10, + "TEXCOORD_1": 11 + }, + "indices": 13, + "material": "BACK02", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 3687, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_BACK02" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 19, + "NORMAL": 16, + "POSITION": 14, + "TANGENT": 15, + "TEXCOORD_0": 17, + "TEXCOORD_1": 18 + }, + "indices": 20, + "material": "DECALSALPHA", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 1228, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_DECALSALPHA" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 26, + "NORMAL": 23, + "POSITION": 21, + "TANGENT": 22, + "TEXCOORD_0": 24, + "TEXCOORD_1": 25 + }, + "indices": 27, + "material": "FLOOR", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 4098, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_FLOOR" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 33, + "NORMAL": 30, + "POSITION": 28, + "TANGENT": 29, + "TEXCOORD_0": 31, + "TEXCOORD_1": 32 + }, + "indices": 34, + "material": "INPUTS01", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 88, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_INPUTS01" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 40, + "NORMAL": 37, + "POSITION": 35, + "TANGENT": 36, + "TEXCOORD_0": 38, + "TEXCOORD_1": 39 + }, + "indices": 41, + "material": "OVERHEAD", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 38340, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_OVERHEAD" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 47, + "NORMAL": 44, + "POSITION": 42, + "TANGENT": 43, + "TEXCOORD_0": 45, + "TEXCOORD_1": 46 + }, + "indices": 48, + "material": "PEDALS", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 5772, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_PEDALS" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 54, + "NORMAL": 51, + "POSITION": 49, + "TANGENT": 50, + "TEXCOORD_0": 52, + "TEXCOORD_1": 53 + }, + "indices": 55, + "material": "PLASTICGLASS", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 627, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_PLASTICGLASS" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 61, + "NORMAL": 58, + "POSITION": 56, + "TANGENT": 57, + "TEXCOORD_0": 59, + "TEXCOORD_1": 60 + }, + "indices": 62, + "material": "SEATS", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 9638, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_SEATS" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 68, + "NORMAL": 65, + "POSITION": 63, + "TANGENT": 64, + "TEXCOORD_0": 66, + "TEXCOORD_1": 67 + }, + "indices": 69, + "material": "SIDEPANELS", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 12982, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_SIDEPANELS" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 75, + "NORMAL": 72, + "POSITION": 70, + "TANGENT": 71, + "TEXCOORD_0": 73, + "TEXCOORD_1": 74 + }, + "indices": 76, + "material": "SIDETOP", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 12718, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_SIDETOP" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 82, + "NORMAL": 79, + "POSITION": 77, + "TANGENT": 78, + "TEXCOORD_0": 80, + "TEXCOORD_1": 81 + }, + "indices": 83, + "material": "TEXT", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 56, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_TEXT" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 89, + "NORMAL": 86, + "POSITION": 84, + "TANGENT": 85, + "TEXCOORD_0": 87, + "TEXCOORD_1": 88 + }, + "indices": 90, + "material": "TOPGLASS", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 168, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_TOPGLASS" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 96, + "NORMAL": 93, + "POSITION": 91, + "TANGENT": 92, + "TEXCOORD_0": 94, + "TEXCOORD_1": 95 + }, + "indices": 97, + "material": "TOPPANEL", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 9909, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_TOPPANEL" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 103, + "NORMAL": 100, + "POSITION": 98, + "TANGENT": 99, + "TEXCOORD_0": 101, + "TEXCOORD_1": 102 + }, + "indices": 104, + "material": "GLARESHIELD", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 7136, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_GLARESHIELD" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 110, + "NORMAL": 107, + "POSITION": 105, + "TANGENT": 106, + "TEXCOORD_0": 108, + "TEXCOORD_1": 109 + }, + "indices": 111, + "material": "MAINPANEL", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 20775, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_MAINPANEL" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 117, + "NORMAL": 114, + "POSITION": 112, + "TANGENT": 113, + "TEXCOORD_0": 115, + "TEXCOORD_1": 116 + }, + "indices": 118, + "material": "PEDESTAL", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 32454, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_PEDESTAL" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 124, + "NORMAL": 121, + "POSITION": 119, + "TANGENT": 120, + "TEXCOORD_0": 122, + "TEXCOORD_1": 123 + }, + "indices": 125, + "material": "THROTTLE", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 69342, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_COCKPIT_THROTTLE" + } + ], + "nodes": [ + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 0, + "name": "COCKPIT_BACK01" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 1, + "name": "COCKPIT_BACK02" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 2, + "name": "COCKPIT_DECALSALPHA" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 3, + "name": "COCKPIT_FLOOR" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 4, + "name": "COCKPIT_INPUTS01" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 5, + "name": "COCKPIT_OVERHEAD" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 6, + "name": "COCKPIT_PEDALS" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 7, + "name": "COCKPIT_PLASTICGLASS" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 8, + "name": "COCKPIT_SEATS" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 9, + "name": "COCKPIT_SIDEPANELS" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 10, + "name": "COCKPIT_SIDETOP" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 11, + "name": "COCKPIT_TEXT" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 12, + "name": "COCKPIT_TOPGLASS" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 13, + "name": "COCKPIT_TOPPANEL" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 14, + "name": "COCKPIT_GLARESHIELD" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 15, + "name": "COCKPIT_MAINPANEL" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 16, + "name": "COCKPIT_PEDESTAL" + }, + { + "translation": [ + 0.18399550020694733, + 2.5197179317474365, + 10.99502182006836 + ], + "rotation": [ + -0.9819666743278505, + 0, + 0, + 0.18905407190322876 + ], + "mesh": 17, + "name": "COCKPIT_THROTTLE" + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ], + "name": "Scene" + } + ], + "extensionsUsed": [ + "ASOBO_normal_map_convention", + "ASOBO_asset_optimized" + ], + "buffers": [ + { + "byteLength": 7681656, + "uri": "cockpit.bin" + } + ] +} \ No newline at end of file diff --git a/src/model/cockpit/gearemer.bin b/src/model/cockpit/gearemer.bin new file mode 100644 index 00000000000..5398888915a Binary files /dev/null and b/src/model/cockpit/gearemer.bin differ diff --git a/src/model/cockpit/gearemer.gltf b/src/model/cockpit/gearemer.gltf new file mode 100644 index 00000000000..6e648b08917 --- /dev/null +++ b/src/model/cockpit/gearemer.gltf @@ -0,0 +1,1325 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 300, + "min": [ + 0 + ], + "max": [ + 12.458333333333334 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 300, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 300, + "type": "VEC4" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 300, + "type": "VEC3" + }, + { + "bufferView": 4, + "componentType": 5126, + "count": 300, + "type": "VEC3" + }, + { + "bufferView": 5, + "componentType": 5126, + "count": 300, + "type": "VEC4" + }, + { + "bufferView": 6, + "componentType": 5126, + "count": 300, + "type": "VEC3" + }, + { + "bufferView": 7, + "componentType": 5126, + "count": 101, + "min": [ + 4.166666666666667 + ], + "max": [ + 8.333333333333334 + ], + "type": "SCALAR" + }, + { + "bufferView": 8, + "componentType": 5126, + "count": 101, + "type": "VEC4" + }, + { + "bufferView": 9, + "componentType": 5126, + "count": 2, + "type": "MAT4" + }, + { + "bufferView": 10, + "componentType": 5126, + "count": 1508, + "min": [ + -0.008255073800683022, + -3.765705685054854e-7, + -0.02963937260210514 + ], + "max": [ + 0.008255073800683022, + 0.0029436075128614902, + 0.030382798984646797 + ], + "type": "VEC3", + "name": "Cylinder.010_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 12, + "componentType": 5120, + "count": 1508, + "type": "VEC4", + "name": "Cylinder.010_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 16, + "componentType": 5120, + "count": 1508, + "type": "VEC4", + "name": "Cylinder.010_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 20, + "componentType": 5122, + "count": 1508, + "type": "VEC2", + "name": "Cylinder.010_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 24, + "componentType": 5122, + "count": 1508, + "type": "VEC2", + "name": "Cylinder.010_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 28, + "componentType": 5123, + "count": 1508, + "type": "VEC4", + "name": "Cylinder.010_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "componentType": 5123, + "count": 8436, + "type": "SCALAR", + "name": "Cylinder.010_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 54288, + "componentType": 5126, + "count": 24, + "min": [ + -0.011657074093818665, + -0.034728121012449265, + -0.010439824312925339 + ], + "max": [ + 0.011656958609819412, + 0.03472788259387016, + 0.000955621711909771 + ], + "type": "VEC3", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 54300, + "componentType": 5120, + "count": 24, + "type": "VEC4", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 54304, + "componentType": 5120, + "count": 24, + "type": "VEC4", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 54308, + "componentType": 5122, + "count": 24, + "type": "VEC2", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 54312, + "componentType": 5122, + "count": 24, + "type": "VEC2", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 54316, + "componentType": 5123, + "count": 24, + "type": "VEC4", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 16872, + "componentType": 5123, + "count": 36, + "type": "SCALAR", + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 55152, + "componentType": 5126, + "count": 13806, + "min": [ + -0.023231318220496178, + -0.013297613710165024, + -0.03927605226635933 + ], + "max": [ + 0.023231318220496178, + 0.07573427259922028, + 0.11693088710308075 + ], + "type": "VEC3", + "name": "Cylinder.011_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 55164, + "componentType": 5120, + "count": 13806, + "type": "VEC4", + "name": "Cylinder.011_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 55168, + "componentType": 5120, + "count": 13806, + "type": "VEC4", + "name": "Cylinder.011_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 55172, + "componentType": 5122, + "count": 13806, + "type": "VEC2", + "name": "Cylinder.011_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 55176, + "componentType": 5122, + "count": 13806, + "type": "VEC2", + "name": "Cylinder.011_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 55180, + "componentType": 5123, + "count": 13806, + "type": "VEC4", + "name": "Cylinder.011_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 16944, + "componentType": 5123, + "count": 67656, + "type": "SCALAR", + "name": "Cylinder.011_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 552168, + "componentType": 5126, + "count": 24, + "min": [ + -0.024191241711378098, + -0.02419125847518444, + -0.0241912379860878 + ], + "max": [ + 0.024191241711378098, + 0.02419126033782959, + 0.024191245436668396 + ], + "type": "VEC3", + "name": "Cube.002_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 552180, + "componentType": 5120, + "count": 24, + "type": "VEC4", + "name": "Cube.002_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 552184, + "componentType": 5120, + "count": 24, + "type": "VEC4", + "name": "Cube.002_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 552188, + "componentType": 5122, + "count": 24, + "type": "VEC2", + "name": "Cube.002_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 552192, + "componentType": 5122, + "count": 24, + "type": "VEC2", + "name": "Cube.002_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 552196, + "componentType": 5123, + "count": 24, + "type": "VEC4", + "name": "Cube.002_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 152256, + "componentType": 5123, + "count": 36, + "type": "SCALAR", + "name": "Cube.002_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 553032, + "componentType": 5126, + "count": 44, + "min": [ + -0.01227574422955513, + -0.00024389397003687918, + -0.032475024461746216 + ], + "max": [ + 0.011588790453970432, + 0.00018276508490089327, + 0.01718912087380886 + ], + "type": "VEC3", + "name": "x0_GEAR_EXTN_Decal_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 553044, + "componentType": 5120, + "count": 44, + "type": "VEC4", + "name": "x0_GEAR_EXTN_Decal_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 553048, + "componentType": 5120, + "count": 44, + "type": "VEC4", + "name": "x0_GEAR_EXTN_Decal_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 553052, + "componentType": 5122, + "count": 44, + "type": "VEC2", + "name": "x0_GEAR_EXTN_Decal_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 553056, + "componentType": 5122, + "count": 44, + "type": "VEC2", + "name": "x0_GEAR_EXTN_Decal_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 553060, + "componentType": 5123, + "count": 44, + "type": "VEC4", + "name": "x0_GEAR_EXTN_Decal_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 152328, + "componentType": 5123, + "count": 66, + "type": "SCALAR", + "name": "x0_GEAR_EXTN_Decal_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 554616, + "componentType": 5126, + "count": 50, + "min": [ + -0.02056143805384636, + -0.15580104291439056, + -0.08455947786569595 + ], + "max": [ + 0.020561382174491882, + 0.020561516284942627, + 0.030059248208999634 + ], + "type": "VEC3", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 554628, + "componentType": 5120, + "count": 50, + "type": "VEC4", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 554632, + "componentType": 5120, + "count": 50, + "type": "VEC4", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 554636, + "componentType": 5122, + "count": 50, + "type": "VEC2", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 554640, + "componentType": 5122, + "count": 50, + "type": "VEC2", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 554644, + "componentType": 5123, + "count": 50, + "type": "VEC4", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 152460, + "componentType": 5123, + "count": 84, + "type": "SCALAR", + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 556416, + "componentType": 5126, + "count": 8682, + "min": [ + -0.04486844316124916, + -0.005818461999297142, + -0.02952420897781849 + ], + "max": [ + 0.04486844316124916, + 0.0003153801371809095, + 0.029504334554076195 + ], + "type": "VEC3", + "name": "Cube.001_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 556428, + "componentType": 5120, + "count": 8682, + "type": "VEC4", + "name": "Cube.001_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 556432, + "componentType": 5120, + "count": 8682, + "type": "VEC4", + "name": "Cube.001_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 556436, + "componentType": 5122, + "count": 8682, + "type": "VEC2", + "name": "Cube.001_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 556440, + "componentType": 5122, + "count": 8682, + "type": "VEC2", + "name": "Cube.001_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 556444, + "componentType": 5123, + "count": 8682, + "type": "VEC4", + "name": "Cube.001_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 152628, + "componentType": 5123, + "count": 40962, + "type": "SCALAR", + "name": "Cube.001_indices#0" + }, + { + "bufferView": 10, + "byteOffset": 868968, + "componentType": 5126, + "count": 1873, + "min": [ + -0.024881934747099876, + -0.03018627129495144, + -0.02490352839231491 + ], + "max": [ + 0.024881934747099876, + -0.00007844436186132953, + 0.020607840269804 + ], + "type": "VEC3", + "name": "Cylinder.001_vertices#0_POSITION" + }, + { + "bufferView": 10, + "byteOffset": 868980, + "componentType": 5120, + "count": 1873, + "type": "VEC4", + "name": "Cylinder.001_vertices#0_TANGENT" + }, + { + "bufferView": 10, + "byteOffset": 868984, + "componentType": 5120, + "count": 1873, + "type": "VEC4", + "name": "Cylinder.001_vertices#0_NORMAL" + }, + { + "bufferView": 10, + "byteOffset": 868988, + "componentType": 5122, + "count": 1873, + "type": "VEC2", + "name": "Cylinder.001_vertices#0_TEXCOORD_0" + }, + { + "bufferView": 10, + "byteOffset": 868992, + "componentType": 5122, + "count": 1873, + "type": "VEC2", + "name": "Cylinder.001_vertices#0_TEXCOORD_1" + }, + { + "bufferView": 10, + "byteOffset": 868996, + "componentType": 5123, + "count": 1873, + "type": "VEC4", + "name": "Cylinder.001_vertices#0_COLOR_0" + }, + { + "bufferView": 11, + "byteOffset": 234552, + "componentType": 5123, + "count": 9312, + "type": "SCALAR", + "name": "Cylinder.001_indices#0" + } + ], + "animations": [ + { + "name": "LEVER_GRAVITYGEAR", + "channels": [ + { + "sampler": 0, + "target": { + "node": 6, + "path": "translation" + } + }, + { + "sampler": 1, + "target": { + "node": 6, + "path": "rotation" + } + }, + { + "sampler": 2, + "target": { + "node": 6, + "path": "scale" + } + }, + { + "sampler": 3, + "target": { + "node": 2, + "path": "translation" + } + }, + { + "sampler": 4, + "target": { + "node": 2, + "path": "rotation" + } + }, + { + "sampler": 5, + "target": { + "node": 2, + "path": "scale" + } + }, + { + "sampler": 6, + "target": { + "node": 10, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 0, + "output": 1, + "interpolation": "LINEAR" + }, + { + "input": 0, + "output": 2, + "interpolation": "LINEAR" + }, + { + "input": 0, + "output": 3, + "interpolation": "LINEAR" + }, + { + "input": 0, + "output": 4, + "interpolation": "LINEAR" + }, + { + "input": 0, + "output": 5, + "interpolation": "LINEAR" + }, + { + "input": 0, + "output": 6, + "interpolation": "LINEAR" + }, + { + "input": 7, + "output": 8, + "interpolation": "LINEAR" + } + ] + } + ], + "asset": { + "generator": "Khronos glTF Blender I/O v3.2.40 and Asobo Studio MSFS Blender I/O v1.1.6", + "version": "2.0", + "extensions": { + "ASOBO_asset_optimized": { + "BoundingBoxMax": [ + 0.04486844316124916, + 0.07573427259922028, + 0.11693088710308075 + ], + "BoundingBoxMin": [ + -0.04486844316124916, + -0.15580104291439056, + -0.08455947786569595 + ], + "MajorVersion": 4, + "MinorVersion": 4, + "UseCheckerboardMaterialForMissingTextures": true, + "UseOnlyFilenameForImageURI": true + }, + "ASOBO_normal_map_convention": { + "tangent_space_convention": "DirectX" + } + } + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 1200 + }, + { + "buffer": 0, + "byteLength": 3600, + "byteOffset": 1200 + }, + { + "buffer": 0, + "byteLength": 4800, + "byteOffset": 4800 + }, + { + "buffer": 0, + "byteLength": 3600, + "byteOffset": 9600 + }, + { + "buffer": 0, + "byteLength": 3600, + "byteOffset": 13200 + }, + { + "buffer": 0, + "byteLength": 4800, + "byteOffset": 16800 + }, + { + "buffer": 0, + "byteLength": 3600, + "byteOffset": 21600 + }, + { + "buffer": 0, + "byteLength": 404, + "byteOffset": 25200 + }, + { + "buffer": 0, + "byteLength": 1616, + "byteOffset": 25604 + }, + { + "buffer": 0, + "byteLength": 128, + "byteOffset": 27220 + }, + { + "buffer": 0, + "byteLength": 936396, + "byteStride": 36, + "byteOffset": 27348, + "target": 34962, + "name": "BufferViewVertexND" + }, + { + "buffer": 0, + "byteLength": 253176, + "byteOffset": 963744, + "target": 34963, + "name": "BufferViewIndex" + } + ], + "extensionsRequired": [ + "MSFT_texture_dds" + ], + "materials": [ + { + "name": "GravityGear_Invisible", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.800000011920929, + 0.800000011920929, + 0.800000011920929, + 1 + ] + }, + "extensions": { + "ASOBO_material_invisible": {} + } + }, + { + "name": "EMERG_GEAR_EXT", + "normalTexture": { + "index": 0 + }, + "occlusionTexture": { + "index": 1 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 2 + }, + "metallicRoughnessTexture": { + "index": 1 + } + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "COLOR_0": 22, + "NORMAL": 19, + "POSITION": 17, + "TANGENT": 18, + "TEXCOORD_0": 20, + "TEXCOORD_1": 21 + }, + "indices": 23, + "material": 0, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 12, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_LEVER_GRAVITYGEAR_BUTTON_CLICKZONE" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 15, + "NORMAL": 12, + "POSITION": 10, + "TANGENT": 11, + "TEXCOORD_0": 13, + "TEXCOORD_1": 14 + }, + "indices": 16, + "material": 1, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 2812, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "Cylinder.010" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 36, + "NORMAL": 33, + "POSITION": 31, + "TANGENT": 32, + "TEXCOORD_0": 34, + "TEXCOORD_1": 35 + }, + "indices": 37, + "material": 0, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 12, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "Cube.002" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 43, + "NORMAL": 40, + "POSITION": 38, + "TANGENT": 39, + "TEXCOORD_0": 41, + "TEXCOORD_1": 42 + }, + "indices": 44, + "material": "TEXT_NoEmi", + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 22, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_GEAR_EXTN_Decal" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 29, + "NORMAL": 26, + "POSITION": 24, + "TANGENT": 25, + "TEXCOORD_0": 27, + "TEXCOORD_1": 28 + }, + "indices": 30, + "material": 1, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 22552, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "Cylinder.011" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 50, + "NORMAL": 47, + "POSITION": 45, + "TANGENT": 46, + "TEXCOORD_0": 48, + "TEXCOORD_1": 49 + }, + "indices": 51, + "material": 0, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 28, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "x0_LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 57, + "NORMAL": 54, + "POSITION": 52, + "TANGENT": 53, + "TEXCOORD_0": 55, + "TEXCOORD_1": 56 + }, + "indices": 58, + "material": 1, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 13654, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "Cube.001" + }, + { + "primitives": [ + { + "attributes": { + "COLOR_0": 64, + "NORMAL": 61, + "POSITION": 59, + "TANGENT": 60, + "TEXCOORD_0": 62, + "TEXCOORD_1": 63 + }, + "indices": 65, + "material": 1, + "mode": 4, + "extras": { + "ASOBO_primitive": { + "PrimitiveCount": 3104, + "VertexType": "VTX", + "VertexVersion": 2 + } + } + } + ], + "name": "Cylinder.001" + } + ], + "nodes": [ + { + "translation": [ + 0, + -0.000002063562760667992, + 0 + ], + "rotation": [ + 0, + -0.7048596739768982, + 0.7093467712402344, + 9.752679375196749e-8 + ], + "scale": [ + 0.8849125504493713, + 0.8849127292633057, + 0.8849127292633057 + ], + "mesh": 0, + "name": "LEVER_GRAVITYGEAR_BUTTON_CLICKZONE" + }, + { + "translation": [ + 0.0001805223000701517, + 0.0005181958549655974, + 0.0008159926510415971 + ], + "rotation": [ + -0.7048595547676086, + 4.449356652003189e-7, + 2.123630480355132e-7, + 0.7093468904495239 + ], + "mesh": 1, + "name": "LEVER_GRAVITYGEAR_BUTTON", + "children": [ + 0 + ] + }, + { + "translation": [ + -1.637992852465686e-8, + 0.05134981498122215, + -0.005761315114796162 + ], + "rotation": [ + 0.7071067094802856, + -8.432149201098582e-9, + -1.1520227616301781e-7, + 0.7071068286895752 + ], + "name": "button", + "children": [ + 1 + ] + }, + { + "translation": [ + -0.0004391938273329288, + 0.05593426525592804, + 0.10457681864500046 + ], + "rotation": [ + 0, + 1, + 0.00021082900639157745, + 7.557741099617489e-8 + ], + "scale": [ + 0.8849125504493713, + 0.8849125504493713, + 0.8849125504493713 + ], + "mesh": 2, + "name": "LEVER_GRAVITYGEAR_CRANE_CLICKZONE" + }, + { + "translation": [ + -0.00043922202894464135, + 0.010206574574112892, + -0.014734471216797829 + ], + "rotation": [ + 0, + -0.70710688829422, + 0.7071066498756409, + 1.067701376200603e-7 + ], + "scale": [ + 0.8849125504493713, + 0.8849125504493713, + 0.8849125504493713 + ], + "mesh": 3, + "name": "LEVER_GRAVITYGEAR_DECAL" + }, + { + "translation": [ + 0.0001805584179237485, + -0.0003965531650464982, + 0.0003793853975366801 + ], + "rotation": [ + -8.146032826061855e-8, + 7.549785863147919e-8, + 2.264936824758479e-7, + 1 + ], + "mesh": 4, + "name": "LEVER_GRAVITYGEAR_CRANE", + "children": [ + 3, + 4 + ] + }, + { + "translation": [ + 3.183231456205249e-12, + 0, + -9.536470315651968e-7 + ], + "rotation": [ + 0.7071067094802856, + -5.338507236274381e-8, + -5.338507591545749e-8, + 0.7071068286895752 + ], + "name": "crank", + "children": [ + 2, + 5 + ] + }, + { + "translation": [ + 0.00018055501277558503, + 0.048293232917785645, + 0.0004749298677779734 + ], + "name": "GearEmerExt", + "children": [ + 6 + ] + }, + { + "translation": [ + -0.00007811502291588113, + 0.06264817714691162, + 0.010286331176757812 + ], + "rotation": [ + 0, + 1, + 0, + 1.6292068494294656e-7 + ], + "scale": [ + 0.8849125504493713, + 0.8849125504493713, + 0.8849125504493713 + ], + "mesh": 5, + "name": "LEVER_GRAVITYGEAR_ACTIVATE_CLICKZONE" + }, + { + "translation": [ + 0.00018055483815260231, + 0.0624462366104126, + 0.001103401300497353 + ], + "mesh": 6, + "name": "LEVER_GRAVITYGEAR_BASE" + }, + { + "translation": [ + 0.00018055576947517693, + 0.0622633695602417, + 0.00012874609092250466 + ], + "mesh": 7, + "name": "LEVER_GRAVITYGEAR_ROTOR" + }, + { + "translation": [ + 0, + 1.1405932903289795, + 10.956473350524902 + ], + "rotation": [ + 0, + -1, + 0, + 1.6292068494294656e-7 + ], + "scale": [ + 1.1300551891326904, + 1.1300551891326904, + 1.1300551891326904 + ], + "name": "LEVER_GRAVITYGEAR", + "children": [ + 7, + 8, + 9, + 10 + ] + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 11 + ], + "name": "Scene" + } + ], + "skins": [ + { + "inverseBindMatrices": 9, + "joints": [ + 6, + 2 + ], + "skeleton": -1, + "name": "GearEmerExt" + } + ], + "textures": [ + { + "extensions": { + "MSFT_texture_dds": { + "source": 0 + } + } + }, + { + "extensions": { + "MSFT_texture_dds": { + "source": 1 + } + } + }, + { + "extensions": { + "MSFT_texture_dds": { + "source": 2 + } + } + }, + { + "extensions": { + "MSFT_texture_dds": { + "source": 3 + } + } + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "extensionsUsed": [ + "ASOBO_normal_map_convention", + "ASOBO_material_invisible", + "ASOBO_material_shadow_options", + "MSFT_texture_dds", + "ASOBO_asset_optimized" + ], + "buffers": [ + { + "byteLength": 1216920, + "uri": "gearemer.bin" + } + ], + "images": [ + { + "uri": "EMERG_GEAR_EXT_NORMAL.PNG.DDS", + "name": "EMERG_GEAR_EXT_NORMAL", + "extras": "ASOBO_image_converted_meta" + }, + { + "uri": "EMERG_GEAR_EXT_COMP.PNG.DDS", + "name": "EMERG_GEAR_EXT_COMP", + "extras": "ASOBO_image_converted_meta" + }, + { + "uri": "EMERG_GEAR_EXT_ALBEDO.PNG.DDS", + "name": "EMERG_GEAR_EXT_ALBEDO", + "extras": "ASOBO_image_converted_meta" + }, + { + "uri": "A320NEO_COCKPIT_DECALSTEXT_ALBD_TIF.PNG.DDS", + "name": "A320NEO_COCKPIT_DECALSTEXT_ALBD.TIF", + "extras": "ASOBO_image_converted_meta" + } + ] +} \ No newline at end of file diff --git a/src/model/models.json b/src/model/models.json index a05be5f2160..2b398426295 100644 --- a/src/model/models.json +++ b/src/model/models.json @@ -90,6 +90,16 @@ { "gltf": "./glass/glass.gltf", "bin": "./glass/glass.bin" + }, + { + "gltf": "./cockpit/cockpit.gltf", + "bin": "./cockpit/cockpit.bin", + "maxLod": 0 + }, + { + "gltf": "./cockpit/gearemer.gltf", + "bin": "./cockpit/gearemer.bin", + "maxLod": 0 } ], "modifications": [ diff --git a/src/shared/src/FmMessages.ts b/src/shared/src/FmMessages.ts index 273275bf073..b8faf52595f 100644 --- a/src/shared/src/FmMessages.ts +++ b/src/shared/src/FmMessages.ts @@ -180,4 +180,9 @@ export const FMMessageTypes: Readonly> = { color: 'Amber', ndPriority: 9, }, + TurnAreaExceedance: { + id: 16, + text: 'TURN AREA EXCEEDANCE', + color: 'Amber', + }, }; diff --git a/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml b/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml index 24bdcda96a7..5db801daa12 100644 --- a/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml +++ b/src/systems/a320_hydraulic_simulation_graphs/Cargo.toml @@ -11,7 +11,7 @@ doc = false [dependencies] systems = { path = "../systems" } a320_systems = { path = "../a320_systems" } -uom = "0.32.0" +uom = "0.33.0" rand = "0.8.0" ntest = "0.7.2" num-derive = "0.3.3" diff --git a/src/systems/a320_systems/Cargo.toml b/src/systems/a320_systems/Cargo.toml index 02a9a36aafa..a832500fdca 100644 --- a/src/systems/a320_systems/Cargo.toml +++ b/src/systems/a320_systems/Cargo.toml @@ -5,7 +5,7 @@ authors = ["FlyByWire Simulations"] edition = "2018" [dependencies] -uom = "0.32.0" +uom = "0.33.0" rand = "0.8.0" nalgebra = "0.25.0" ntest = "0.7.2" diff --git a/src/systems/a320_systems/src/hydraulic/flaps_computer.rs b/src/systems/a320_systems/src/hydraulic/flaps_computer.rs index db3c06d329a..42bb93a7197 100644 --- a/src/systems/a320_systems/src/hydraulic/flaps_computer.rs +++ b/src/systems/a320_systems/src/hydraulic/flaps_computer.rs @@ -66,10 +66,6 @@ impl SimulationElement for FlapsHandle { } struct SlatFlapControlComputer { - left_flaps_target_angle_id: VariableIdentifier, - right_flaps_target_angle_id: VariableIdentifier, - left_slats_target_angle_id: VariableIdentifier, - right_slats_target_angle_id: VariableIdentifier, flaps_conf_index_id: VariableIdentifier, slats_fppu_angle_id: VariableIdentifier, flaps_fppu_angle_id: VariableIdentifier, @@ -86,20 +82,12 @@ struct SlatFlapControlComputer { } impl SlatFlapControlComputer { - const EQUAL_ANGLE_DELTA_DEGREE: f64 = 0.01; + const EQUAL_ANGLE_DELTA_DEGREE: f64 = 0.177; const HANDLE_ONE_CONF_AIRSPEED_THRESHOLD_KNOTS: f64 = 100.; const CONF1F_TO_CONF1_AIRSPEED_THRESHOLD_KNOTS: f64 = 210.; fn new(context: &mut InitContext) -> Self { Self { - left_flaps_target_angle_id: context - .get_identifier("LEFT_FLAPS_TARGET_ANGLE".to_owned()), - right_flaps_target_angle_id: context - .get_identifier("RIGHT_FLAPS_TARGET_ANGLE".to_owned()), - left_slats_target_angle_id: context - .get_identifier("LEFT_SLATS_TARGET_ANGLE".to_owned()), - right_slats_target_angle_id: context - .get_identifier("RIGHT_SLATS_TARGET_ANGLE".to_owned()), flaps_conf_index_id: context.get_identifier("FLAPS_CONF_INDEX".to_owned()), slats_fppu_angle_id: context.get_identifier("SLATS_FPPU_ANGLE".to_owned()), flaps_fppu_angle_id: context.get_identifier("FLAPS_FPPU_ANGLE".to_owned()), @@ -120,25 +108,27 @@ impl SlatFlapControlComputer { } } - fn demanded_flaps_angle_from_conf(flap_conf: FlapsConf) -> Angle { + // Returns a flap demanded angle in FPPU reference degree (feedback sensor) + fn demanded_flaps_fppu_angle_from_conf(flap_conf: FlapsConf) -> Angle { match flap_conf { FlapsConf::Conf0 => Angle::new::(0.), FlapsConf::Conf1 => Angle::new::(0.), - FlapsConf::Conf1F => Angle::new::(10.), - FlapsConf::Conf2 => Angle::new::(15.), - FlapsConf::Conf3 => Angle::new::(20.), - FlapsConf::ConfFull => Angle::new::(40.), + FlapsConf::Conf1F => Angle::new::(120.22), + FlapsConf::Conf2 => Angle::new::(145.51), + FlapsConf::Conf3 => Angle::new::(168.35), + FlapsConf::ConfFull => Angle::new::(251.97), } } - fn demanded_slats_angle_from_conf(flap_conf: FlapsConf) -> Angle { + // Returns a slat demanded angle in FPPU reference degree (feedback sensor) + fn demanded_slats_fppu_angle_from_conf(flap_conf: FlapsConf) -> Angle { match flap_conf { FlapsConf::Conf0 => Angle::new::(0.), - FlapsConf::Conf1 => Angle::new::(18.), - FlapsConf::Conf1F => Angle::new::(18.), - FlapsConf::Conf2 => Angle::new::(22.), - FlapsConf::Conf3 => Angle::new::(22.), - FlapsConf::ConfFull => Angle::new::(27.), + FlapsConf::Conf1 => Angle::new::(222.27), + FlapsConf::Conf1F => Angle::new::(222.27), + FlapsConf::Conf2 => Angle::new::(272.27), + FlapsConf::Conf3 => Angle::new::(272.27), + FlapsConf::ConfFull => Angle::new::(334.16), } } @@ -188,8 +178,8 @@ impl SlatFlapControlComputer { ) { self.flaps_conf = self.generate_configuration(flaps_handle, context); - self.flaps_demanded_angle = Self::demanded_flaps_angle_from_conf(self.flaps_conf); - self.slats_demanded_angle = Self::demanded_slats_angle_from_conf(self.flaps_conf); + self.flaps_demanded_angle = Self::demanded_flaps_fppu_angle_from_conf(self.flaps_conf); + self.slats_demanded_angle = Self::demanded_slats_fppu_angle_from_conf(self.flaps_conf); self.flaps_feedback_angle = flaps_feedback.angle(); self.slats_feedback_angle = slats_feedback.angle(); } @@ -331,12 +321,6 @@ impl SlatFlapLane for SlatFlapControlComputer { impl SimulationElement for SlatFlapControlComputer { fn write(&self, writer: &mut SimulatorWriter) { - writer.write(&self.left_flaps_target_angle_id, self.flaps_demanded_angle); - writer.write(&self.right_flaps_target_angle_id, self.flaps_demanded_angle); - - writer.write(&self.left_slats_target_angle_id, self.slats_demanded_angle); - writer.write(&self.right_slats_target_angle_id, self.slats_demanded_angle); - writer.write(&self.flaps_conf_index_id, self.flaps_conf as u8); writer.write(&self.slats_fppu_angle_id, self.slats_feedback_angle); @@ -402,12 +386,8 @@ impl SimulationElement for SlatFlapComplex { #[cfg(test)] mod tests { - use crate::hydraulic::A320Hydraulic; - use super::*; - use ntest::assert_about_eq; use std::time::Duration; - use systems::shared::interpolation; use systems::simulation::{ test::{ReadByName, SimulationTestBed, TestBed, WriteByName}, Aircraft, @@ -427,26 +407,12 @@ mod tests { } impl FeedbackPositionPickoffUnit for SlatFlapGear { fn angle(&self) -> Angle { - let synchro_gear_breakpoints = match self.surface_type.as_str() { - "FLAPS" => A320Hydraulic::FLAP_FPPU_TO_SURFACE_ANGLE_BREAKPTS, - "SLATS" => A320Hydraulic::SLAT_FPPU_TO_SURFACE_ANGLE_BREAKPTS, - _ => panic!(), - }; - let synchro_gear_degrees = match self.surface_type.as_str() { - "FLAPS" => A320Hydraulic::FLAP_FPPU_TO_SURFACE_ANGLE_DEGREES, - "SLATS" => A320Hydraulic::SLAT_FPPU_TO_SURFACE_ANGLE_DEGREES, - _ => panic!(), - }; - Angle::new::(interpolation( - &synchro_gear_degrees, - &synchro_gear_breakpoints, - self.current_angle.get::(), - )) + self.current_angle } } impl SlatFlapGear { - const ANGLE_DELTA_DEGREE: f64 = 0.1; + const ANGLE_DELTA_DEGREE: f64 = 0.01; fn new( context: &mut InitContext, @@ -484,16 +450,25 @@ mod tests { || hydraulic_pressure_right_side.get::() > 1500. { if let Some(demanded_angle) = sfcc.signal_demanded_angle(&self.surface_type) { - let actual_minus_target = demanded_angle - self.current_angle; - if actual_minus_target.get::().abs() > Self::ANGLE_DELTA_DEGREE { + let actual_minus_target_ffpu = demanded_angle - self.angle(); + + let fppu_angle = self.angle(); + + if actual_minus_target_ffpu.get::().abs() > Self::ANGLE_DELTA_DEGREE { self.current_angle += Angle::new::( - actual_minus_target.get::().signum() + actual_minus_target_ffpu.get::().signum() * self.speed.get::() * context.delta_as_secs_f64(), ); self.current_angle = self.current_angle.max(Angle::new::(0.)); - } else { - self.current_angle = demanded_angle; + + let new_ffpu_angle = self.angle(); + // If demand was crossed between two frames: fixing to demand + if new_ffpu_angle > demanded_angle && fppu_angle < demanded_angle + || new_ffpu_angle < demanded_angle && fppu_angle > demanded_angle + { + self.current_angle = demanded_angle; + } } } } @@ -539,14 +514,14 @@ mod tests { flap_gear: SlatFlapGear::new( context, - AngularVelocity::new::(4.), - Angle::new::(40.), + AngularVelocity::new::(7.5), + Angle::new::(251.97), "FLAPS", ), slat_gear: SlatFlapGear::new( context, - AngularVelocity::new::(3.), - Angle::new::(27.), + AngularVelocity::new::(7.5), + Angle::new::(334.16), "SLATS", ), @@ -676,12 +651,12 @@ mod tests { self.query(|a| a.slat_flap_complex.sfcc.flaps_conf) } - fn get_flaps_angle(&self) -> f64 { - self.query(|a| a.flap_gear.current_angle.get::()) + fn get_flaps_fppu_feedback(&self) -> f64 { + self.query(|a| a.flap_gear.angle().get::()) } - fn get_slats_angle(&self) -> f64 { - self.query(|a| a.slat_gear.current_angle.get::()) + fn get_slats_fppu_feedback(&self) -> f64 { + self.query(|a| a.slat_gear.angle().get::()) } fn test_flap_conf( @@ -726,15 +701,11 @@ mod tests { assert!(test_bed.contains_variable_with_name("RIGHT_FLAPS_ANGLE")); assert!(test_bed.contains_variable_with_name("LEFT_FLAPS_POSITION_PERCENT")); assert!(test_bed.contains_variable_with_name("RIGHT_FLAPS_POSITION_PERCENT")); - assert!(test_bed.contains_variable_with_name("LEFT_FLAPS_TARGET_ANGLE")); - assert!(test_bed.contains_variable_with_name("RIGHT_FLAPS_TARGET_ANGLE")); assert!(test_bed.contains_variable_with_name("LEFT_SLATS_ANGLE")); assert!(test_bed.contains_variable_with_name("RIGHT_SLATS_ANGLE")); assert!(test_bed.contains_variable_with_name("LEFT_SLATS_POSITION_PERCENT")); assert!(test_bed.contains_variable_with_name("RIGHT_SLATS_POSITION_PERCENT")); - assert!(test_bed.contains_variable_with_name("LEFT_SLATS_TARGET_ANGLE")); - assert!(test_bed.contains_variable_with_name("RIGHT_SLATS_TARGET_ANGLE")); assert!(test_bed.contains_variable_with_name("FLAPS_CONF_INDEX")); } @@ -906,7 +877,7 @@ mod tests { assert!(test_bed.read_slat_flap_system_status_word().get_bit(21)); assert!(!test_bed.read_slat_flap_system_status_word().get_bit(26)); - test_bed = test_bed.run_waiting_for(Duration::from_secs(31)); + test_bed = test_bed.run_waiting_for(Duration::from_secs(45)); assert!(!test_bed.read_slat_flap_actual_position_word().get_bit(12)); assert!(test_bed.read_slat_flap_actual_position_word().get_bit(13)); @@ -934,19 +905,19 @@ mod tests { test_bed = test_bed.set_flaps_handle_position(1).run_one_tick(); - test_bed.test_flap_conf(1, 10., 18., FlapsConf::Conf1F, angle_delta); + test_bed.test_flap_conf(1, 120.22, 222.27, FlapsConf::Conf1F, angle_delta); test_bed = test_bed.set_flaps_handle_position(2).run_one_tick(); - test_bed.test_flap_conf(2, 15., 22., FlapsConf::Conf2, angle_delta); + test_bed.test_flap_conf(2, 145.51, 272.27, FlapsConf::Conf2, angle_delta); test_bed = test_bed.set_flaps_handle_position(3).run_one_tick(); - test_bed.test_flap_conf(3, 20., 22., FlapsConf::Conf3, angle_delta); + test_bed.test_flap_conf(3, 168.35, 272.27, FlapsConf::Conf3, angle_delta); test_bed = test_bed.set_flaps_handle_position(4).run_one_tick(); - test_bed.test_flap_conf(4, 40., 27., FlapsConf::ConfFull, angle_delta); + test_bed.test_flap_conf(4, 251.97, 334.16, FlapsConf::ConfFull, angle_delta); } // Tests flaps configuration and angles for regular @@ -964,19 +935,19 @@ mod tests { test_bed = test_bed.set_flaps_handle_position(1).run_one_tick(); - test_bed.test_flap_conf(1, 0., 18., FlapsConf::Conf1, angle_delta); + test_bed.test_flap_conf(1, 0., 222.27, FlapsConf::Conf1, angle_delta); test_bed = test_bed.set_flaps_handle_position(2).run_one_tick(); - test_bed.test_flap_conf(2, 15., 22., FlapsConf::Conf2, angle_delta); + test_bed.test_flap_conf(2, 145.51, 272.27, FlapsConf::Conf2, angle_delta); test_bed = test_bed.set_flaps_handle_position(3).run_one_tick(); - test_bed.test_flap_conf(3, 20., 22., FlapsConf::Conf3, angle_delta); + test_bed.test_flap_conf(3, 168.35, 272.27, FlapsConf::Conf3, angle_delta); test_bed = test_bed.set_flaps_handle_position(4).run_one_tick(); - test_bed.test_flap_conf(4, 40., 27., FlapsConf::ConfFull, angle_delta); + test_bed.test_flap_conf(4, 251.97, 334.16, FlapsConf::ConfFull, angle_delta); } //Tests regular transition 2->1 below and above 210 knots @@ -1038,19 +1009,19 @@ mod tests { test_bed = test_bed.set_flaps_handle_position(4).run_one_tick(); - test_bed.test_flap_conf(4, 40., 27., FlapsConf::ConfFull, angle_delta); + test_bed.test_flap_conf(4, 251.97, 334.16, FlapsConf::ConfFull, angle_delta); test_bed = test_bed.set_flaps_handle_position(3).run_one_tick(); - test_bed.test_flap_conf(3, 20., 22., FlapsConf::Conf3, angle_delta); + test_bed.test_flap_conf(3, 168.35, 272.27, FlapsConf::Conf3, angle_delta); test_bed = test_bed.set_flaps_handle_position(2).run_one_tick(); - test_bed.test_flap_conf(2, 15., 22., FlapsConf::Conf2, angle_delta); + test_bed.test_flap_conf(2, 145.51, 272.27, FlapsConf::Conf2, angle_delta); test_bed = test_bed.set_flaps_handle_position(1).run_one_tick(); - test_bed.test_flap_conf(1, 10., 18., FlapsConf::Conf1F, angle_delta); + test_bed.test_flap_conf(1, 120.22, 222.27, FlapsConf::Conf1F, angle_delta); test_bed = test_bed.set_flaps_handle_position(0).run_one_tick(); @@ -1070,19 +1041,19 @@ mod tests { test_bed = test_bed.set_flaps_handle_position(4).run_one_tick(); - test_bed.test_flap_conf(4, 40., 27., FlapsConf::ConfFull, angle_delta); + test_bed.test_flap_conf(4, 251.97, 334.16, FlapsConf::ConfFull, angle_delta); test_bed = test_bed.set_flaps_handle_position(3).run_one_tick(); - test_bed.test_flap_conf(3, 20., 22., FlapsConf::Conf3, angle_delta); + test_bed.test_flap_conf(3, 168.35, 272.27, FlapsConf::Conf3, angle_delta); test_bed = test_bed.set_flaps_handle_position(2).run_one_tick(); - test_bed.test_flap_conf(2, 15., 22., FlapsConf::Conf2, angle_delta); + test_bed.test_flap_conf(2, 145.51, 272.27, FlapsConf::Conf2, angle_delta); test_bed = test_bed.set_flaps_handle_position(1).run_one_tick(); - test_bed.test_flap_conf(1, 0., 18., FlapsConf::Conf1, angle_delta); + test_bed.test_flap_conf(1, 0., 222.27, FlapsConf::Conf1, angle_delta); test_bed = test_bed.set_flaps_handle_position(0).run_one_tick(); @@ -1360,11 +1331,9 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::ConfFull); } - // The tests below test the movement of the - // flaps/slats. #[test] fn flaps_test_movement_0_to_1f() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1373,40 +1342,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf0); - test_bed = test_bed.set_flaps_handle_position(1); + test_bed = test_bed + .set_flaps_handle_position(1) + .run_waiting_for(Duration::from_secs(20)); - let mut previous_angle: f64 = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - println!( - "{}, {}, {}", - test_bed.get_flaps_angle(), - test_bed.get_flaps_demanded_angle(), - previous_angle - ); - if (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_flaps_angle()); - } - previous_angle = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() <= angle_delta + (test_bed.get_flaps_fppu_feedback() - test_bed.get_flaps_demanded_angle()).abs() + <= angle_delta ); } #[test] fn flaps_test_movement_1f_to_2() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1415,34 +1363,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf1F); - test_bed = test_bed.set_flaps_handle_position(2); + test_bed = test_bed + .set_flaps_handle_position(2) + .run_waiting_for(Duration::from_secs(20)); - let mut previous_angle: f64 = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_flaps_angle()); - } - previous_angle = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() <= angle_delta + (test_bed.get_flaps_fppu_feedback() - test_bed.get_flaps_demanded_angle()).abs() + <= angle_delta ); } #[test] fn flaps_test_movement_2_to_3() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1451,34 +1384,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf2); - test_bed = test_bed.set_flaps_handle_position(3); + test_bed = test_bed + .set_flaps_handle_position(3) + .run_waiting_for(Duration::from_secs(30)); - let mut previous_angle: f64 = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_flaps_angle()); - } - previous_angle = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() <= angle_delta + (test_bed.get_flaps_fppu_feedback() - test_bed.get_flaps_demanded_angle()).abs() + <= angle_delta ); } #[test] fn flaps_test_movement_3_to_full() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1487,34 +1405,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf3); - test_bed = test_bed.set_flaps_handle_position(4); + test_bed = test_bed + .set_flaps_handle_position(4) + .run_waiting_for(Duration::from_secs(20)); - let mut previous_angle: f64 = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_flaps_angle()); - } - previous_angle = test_bed.get_flaps_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_flaps_angle() - test_bed.get_flaps_demanded_angle()).abs() <= angle_delta + (test_bed.get_flaps_fppu_feedback() - test_bed.get_flaps_demanded_angle()).abs() + <= angle_delta ); } #[test] fn slats_test_movement_0_to_1f() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1523,34 +1426,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf0); - test_bed = test_bed.set_flaps_handle_position(1); + test_bed = test_bed + .set_flaps_handle_position(1) + .run_waiting_for(Duration::from_secs(30)); - let mut previous_angle: f64 = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_slats_angle()); - } - previous_angle = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() <= angle_delta + (test_bed.get_slats_fppu_feedback() - test_bed.get_slats_demanded_angle()).abs() + <= angle_delta ); } #[test] fn slats_and_flaps_test_movement_0_to_1() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_blue_hyd_pressure() @@ -1560,37 +1448,23 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf0); - test_bed = test_bed.set_flaps_handle_position(1); - - let mut previous_angle: f64 = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); + test_bed = test_bed + .set_flaps_handle_position(1) + .run_waiting_for(Duration::from_secs(30)); - for _ in 0..300 { - if (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() + assert!( + (test_bed.get_flaps_fppu_feedback() - test_bed.get_flaps_demanded_angle()).abs() <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_slats_angle()); - } - previous_angle = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - } - + ); assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() <= angle_delta + (test_bed.get_slats_fppu_feedback() - test_bed.get_slats_demanded_angle()).abs() + <= angle_delta ); - assert!((test_bed.get_flaps_angle() - 0.).abs() < f64::EPSILON); } #[test] fn slats_test_movement_1f_to_2() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1599,34 +1473,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf1F); - test_bed = test_bed.set_flaps_handle_position(2); + test_bed = test_bed + .set_flaps_handle_position(2) + .run_waiting_for(Duration::from_secs(40)); - let mut previous_angle: f64 = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_slats_angle()); - } - previous_angle = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() <= angle_delta + (test_bed.get_slats_fppu_feedback() - test_bed.get_slats_demanded_angle()).abs() + <= angle_delta ); } #[test] fn slats_test_movement_2_to_3() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1635,34 +1494,19 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf2); - test_bed = test_bed.set_flaps_handle_position(3); + test_bed = test_bed + .set_flaps_handle_position(3) + .run_waiting_for(Duration::from_secs(40)); - let mut previous_angle: f64 = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_slats_angle()); - } - previous_angle = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() <= angle_delta + (test_bed.get_slats_fppu_feedback() - test_bed.get_slats_demanded_angle()).abs() + <= angle_delta ); } #[test] fn slats_test_movement_3_to_full() { - let angle_delta = 0.01; + let angle_delta = 0.2; let mut test_bed = test_bed_with() .set_green_hyd_pressure() .set_indicated_airspeed(0.) @@ -1671,70 +1515,13 @@ mod tests { assert_eq!(test_bed.get_flaps_conf(), FlapsConf::Conf3); - test_bed = test_bed.set_flaps_handle_position(4); + test_bed = test_bed + .set_flaps_handle_position(4) + .run_waiting_for(Duration::from_secs(50)); - let mut previous_angle: f64 = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - if (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - { - test_bed = test_bed.run_waiting_for(Duration::from_secs(5)); - assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() - <= angle_delta - ); - break; - } else { - assert!(previous_angle < test_bed.get_slats_angle()); - } - previous_angle = test_bed.get_slats_angle(); - test_bed = test_bed.run_one_tick(); - } assert!( - (test_bed.get_slats_angle() - test_bed.get_slats_demanded_angle()).abs() <= angle_delta + (test_bed.get_slats_fppu_feedback() - test_bed.get_slats_demanded_angle()).abs() + <= angle_delta ); } - - #[test] - fn only_flaps_move_when_yellow_only() { - let mut test_bed = test_bed_with() - .set_yellow_hyd_pressure() - .set_indicated_airspeed(0.) - .set_flaps_handle_position(0) - .run_one_tick(); - - test_bed = test_bed.set_flaps_handle_position(1); - - let starting_flap_angle: f64 = test_bed.get_flaps_angle(); - let starting_slat_angle: f64 = test_bed.get_slats_angle(); - - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - test_bed = test_bed.run_one_tick(); - } - assert!(test_bed.get_flaps_angle() > starting_flap_angle); - assert_about_eq!(test_bed.get_slats_angle(), starting_slat_angle); - } - - #[test] - fn only_slats_move_when_blue_only() { - let mut test_bed = test_bed_with() - .set_blue_hyd_pressure() - .set_indicated_airspeed(0.) - .set_flaps_handle_position(0) - .run_one_tick(); - - test_bed = test_bed.set_flaps_handle_position(1); - - let starting_flap_angle: f64 = test_bed.get_flaps_angle(); - let starting_slat_angle: f64 = test_bed.get_slats_angle(); - - test_bed = test_bed.run_one_tick(); - for _ in 0..300 { - test_bed = test_bed.run_one_tick(); - } - assert_about_eq!(test_bed.get_flaps_angle(), starting_flap_angle); - assert!(test_bed.get_slats_angle() > starting_slat_angle); - } } diff --git a/src/systems/a320_systems/src/hydraulic/mod.rs b/src/systems/a320_systems/src/hydraulic/mod.rs index 77e3489f9a0..dbab98bed60 100644 --- a/src/systems/a320_systems/src/hydraulic/mod.rs +++ b/src/systems/a320_systems/src/hydraulic/mod.rs @@ -37,6 +37,10 @@ use systems::{ SteeringRatioToAngle, }, pumps::PumpCharacteristics, + trimmable_horizontal_stabilizer::{ + ManualPitchTrimController, PitchTrimActuatorController, + TrimmableHorizontalStabilizerAssembly, + }, ElectricPump, EngineDrivenPump, HydraulicCircuit, HydraulicCircuitController, HydraulicPressureSensors, PowerTransferUnit, PowerTransferUnitCharacteristics, PowerTransferUnitController, PressureSwitch, PressureSwitchType, PumpController, @@ -1239,6 +1243,10 @@ pub(super) struct A320Hydraulic { gear_system: HydraulicGearSystem, ptu_high_pitch_sound_active: DelayedFalseLogicGate, + + trim_controller: A320TrimInputController, + + trim_assembly: TrimmableHorizontalStabilizerAssembly, } impl A320Hydraulic { const HIGH_PITCH_PTU_SOUND_DELTA_PRESS_THRESHOLD_PSI: f64 = 2400.; @@ -1497,6 +1505,19 @@ impl A320Hydraulic { ptu_high_pitch_sound_active: DelayedFalseLogicGate::new( Self::HIGH_PITCH_PTU_SOUND_DURATION, ), + + trim_controller: A320TrimInputController::new(context), + trim_assembly: TrimmableHorizontalStabilizerAssembly::new( + context, + Angle::new::(360. * -1.4), + Angle::new::(360. * 6.13), + Angle::new::(360. * -1.87), + Angle::new::(360. * 8.19), // 1.87 rotations down 6.32 up, + AngularVelocity::new::(5000.), + Ratio::new::(2035. / 6.13), + Angle::new::(-4.), + Angle::new::(17.5), + ), } } @@ -1750,15 +1771,26 @@ impl A320Hydraulic { emergency_elec, ); - self.gear_system_gravity_extension_controller - .update(context); - self.gear_system_hydraulic_controller.update( adirs, lgciu1, lgciu2, &self.gear_system_gravity_extension_controller, ); + + self.trim_assembly.update( + context, + &self.trim_controller, + &self.trim_controller, + [ + self.green_circuit + .system_section() + .pressure_downstream_leak_valve(), + self.yellow_circuit + .system_section() + .pressure_downstream_leak_valve(), + ], + ); } fn update_with_sim_rate( @@ -1905,6 +1937,9 @@ impl A320Hydraulic { for actuator in self.gear_system.all_actuators() { self.green_circuit.update_actuator_volumes(actuator); } + + self.green_circuit + .update_actuator_volumes(self.trim_assembly.left_motor()); } fn update_yellow_actuators_volume(&mut self) { @@ -1940,6 +1975,9 @@ impl A320Hydraulic { .update_actuator_volumes(self.right_spoilers.actuator(1)); self.yellow_circuit .update_actuator_volumes(self.right_spoilers.actuator(3)); + + self.yellow_circuit + .update_actuator_volumes(self.trim_assembly.right_motor()); } fn update_blue_actuators_volume(&mut self) { @@ -2255,6 +2293,9 @@ impl SimulationElement for A320Hydraulic { .accept(visitor); self.gear_system.accept(visitor); + self.trim_controller.accept(visitor); + self.trim_assembly.accept(visitor); + visitor.visit(self); } @@ -5066,7 +5107,7 @@ impl ElevatorAssembly { ], ); - self.position = self.hydraulic_assembly.actuator_position_normalized(0); + self.position = self.hydraulic_assembly.position_normalized(); } } impl SimulationElement for ElevatorAssembly { @@ -5184,9 +5225,7 @@ impl SpoilerElement { [current_pressure], ); - // We return actuator position so it's consistent with demand - // Later we must decide who works in actuator position and surface position between demand and control - self.position = self.hydraulic_assembly.actuator_position_normalized(0); + self.position = self.hydraulic_assembly.position_normalized(); } } impl SimulationElement for SpoilerElement { @@ -5384,86 +5423,112 @@ impl SimulationElement for SpoilerComputer { } struct A320GravityExtension { - gear_gravity_extension_active_id: VariableIdentifier, - gear_gravity_extension_handle_is_turned_id: VariableIdentifier, + gear_gravity_extension_handle_position_id: VariableIdentifier, handle_angle: Angle, - - is_extending_gear: bool, - - is_turned: bool, } impl A320GravityExtension { - const INCREMENT_ANGLE_DEGREE_PER_SECOND: f64 = 220.; - const MAX_CRANK_HANDLE_ANGLE_DEGREE: f64 = 360. * 3.; - const MIN_CRANK_HANDLE_ANGLE_DEGREE: f64 = 0.; - const CRANK_HANDLE_ANGLE_MARGIN_AT_MAX_ROTATION_DEGREE: f64 = 0.1; - fn new(context: &mut InitContext) -> Self { Self { - gear_gravity_extension_active_id: context - .get_identifier("GEAR_EMERGENCY_EXTENSION_ACTIVE".to_owned()), - gear_gravity_extension_handle_is_turned_id: context - .get_identifier("GEAR_EMERGENCY_EXTENSION_IS_TURNED".to_owned()), + gear_gravity_extension_handle_position_id: context + .get_identifier("GRAVITYGEAR_ROTATE_PCT".to_owned()), handle_angle: Angle::default(), - is_extending_gear: true, - is_turned: false, } } +} +impl GearGravityExtension for A320GravityExtension { + fn extension_handle_number_of_turns(&self) -> u8 { + (self.handle_angle.get::() / 360.).floor() as u8 + } +} +impl SimulationElement for A320GravityExtension { + fn read(&mut self, reader: &mut SimulatorReader) { + let handle_percent: f64 = reader.read(&self.gear_gravity_extension_handle_position_id); - fn update(&mut self, context: &UpdateContext) { - if self.is_turned { - if self.is_extending_gear { - self.handle_angle += Angle::new::( - Self::INCREMENT_ANGLE_DEGREE_PER_SECOND * context.delta_as_secs_f64(), - ); - } else { - self.handle_angle -= Angle::new::( - Self::INCREMENT_ANGLE_DEGREE_PER_SECOND * context.delta_as_secs_f64(), - ); + self.handle_angle = Angle::new::(handle_percent * 3.6) + .max(Angle::new::(0.)) + .min(Angle::new::(360. * 3.)); + } +} + +struct A320TrimInputController { + motor1_active_id: VariableIdentifier, + motor2_active_id: VariableIdentifier, + motor3_active_id: VariableIdentifier, + + motor1_position_id: VariableIdentifier, + motor2_position_id: VariableIdentifier, + motor3_position_id: VariableIdentifier, + + manual_control_active_id: VariableIdentifier, + manual_control_speed_id: VariableIdentifier, + + motor_active: [bool; 3], + motor_position: [Angle; 3], + + manual_control: bool, + manual_control_speed: AngularVelocity, +} +impl A320TrimInputController { + fn new(context: &mut InitContext) -> Self { + Self { + motor1_active_id: context.get_identifier("THS_1_ACTIVE_MODE_COMMANDED".to_owned()), + motor2_active_id: context.get_identifier("THS_2_ACTIVE_MODE_COMMANDED".to_owned()), + motor3_active_id: context.get_identifier("THS_3_ACTIVE_MODE_COMMANDED".to_owned()), + + motor1_position_id: context.get_identifier("THS_1_COMMANDED_POSITION".to_owned()), + motor2_position_id: context.get_identifier("THS_2_COMMANDED_POSITION".to_owned()), + motor3_position_id: context.get_identifier("THS_3_COMMANDED_POSITION".to_owned()), + + manual_control_active_id: context + .get_identifier("THS_MANUAL_CONTROL_ACTIVE".to_owned()), + manual_control_speed_id: context.get_identifier("THS_MANUAL_CONTROL_SPEED".to_owned()), + + motor_active: [false; 3], + motor_position: [Angle::default(); 3], + + manual_control: false, + manual_control_speed: AngularVelocity::default(), + } + } +} +impl PitchTrimActuatorController for A320TrimInputController { + fn commanded_position(&self) -> Angle { + for (idx, motor_active) in self.motor_active.iter().enumerate() { + if *motor_active { + return self.motor_position[idx]; } } - self.handle_angle = self - .handle_angle - .min(Angle::new::( - Self::MAX_CRANK_HANDLE_ANGLE_DEGREE - + Self::CRANK_HANDLE_ANGLE_MARGIN_AT_MAX_ROTATION_DEGREE, - )) - .max(Angle::new::( - Self::MIN_CRANK_HANDLE_ANGLE_DEGREE - - Self::CRANK_HANDLE_ANGLE_MARGIN_AT_MAX_ROTATION_DEGREE, - )); + Angle::default() + } - if self.handle_angle.get::() > Self::MAX_CRANK_HANDLE_ANGLE_DEGREE - && !self.is_turned - && self.is_extending_gear - { - self.is_extending_gear = false; - } else if self.handle_angle.get::() < Self::MIN_CRANK_HANDLE_ANGLE_DEGREE - && !self.is_turned - && !self.is_extending_gear - { - self.is_extending_gear = true; - } + fn energised_motor(&self) -> [bool; 3] { + self.motor_active } } -impl GearGravityExtension for A320GravityExtension { - fn extension_handle_number_of_turns(&self) -> u8 { - (self.handle_angle.get::() / 360.).floor() as u8 +impl ManualPitchTrimController for A320TrimInputController { + fn is_manually_moved(&self) -> bool { + self.manual_control || self.manual_control_speed.get::() != 0. + } + + fn moving_speed(&self) -> AngularVelocity { + self.manual_control_speed } } -impl SimulationElement for A320GravityExtension { +impl SimulationElement for A320TrimInputController { fn read(&mut self, reader: &mut SimulatorReader) { - self.is_turned = reader.read(&self.gear_gravity_extension_active_id); - } + self.motor_active[0] = reader.read(&self.motor1_active_id); + self.motor_active[1] = reader.read(&self.motor2_active_id); + self.motor_active[2] = reader.read(&self.motor3_active_id); - fn write(&self, writer: &mut SimulatorWriter) { - writer.write( - &self.gear_gravity_extension_handle_is_turned_id, - self.handle_angle.get::() < 360. && self.is_turned, - ); + self.motor_position[0] = reader.read(&self.motor1_position_id); + self.motor_position[1] = reader.read(&self.motor2_position_id); + self.motor_position[2] = reader.read(&self.motor3_position_id); + + self.manual_control = reader.read(&self.manual_control_active_id); + self.manual_control_speed = reader.read(&self.manual_control_speed_id); } } @@ -6132,14 +6197,6 @@ mod tests { self.read_by_name("A32NX_HYD_RAT_RPM") } - fn get_emergency_handle_number_of_turns(&self) -> u8 { - self.query(|a| { - a.hydraulics - .gear_system_gravity_extension_controller - .extension_handle_number_of_turns() - }) - } - fn get_left_aileron_position(&mut self) -> Ratio { Ratio::new::(self.read_by_name("HYD_AIL_LEFT_DEFLECTION")) } @@ -6246,6 +6303,13 @@ mod tests { self } + fn on_the_ground_after_touchdown(mut self) -> Self { + self.set_indicated_altitude(Length::new::(0.)); + self.set_on_ground(true); + self.set_indicated_airspeed(Velocity::new::(100.)); + self + } + fn air_press_low(mut self) -> Self { self.command(|a| a.pneumatics.set_low_air_pressure()); self @@ -6500,7 +6564,7 @@ mod tests { self.read_by_name("RIGHT_SLATS_POSITION_PERCENT") } - fn get_real_gear_position(&mut self, wheel_id: GearWheel) -> f64 { + fn get_real_gear_position(&mut self, wheel_id: GearWheel) -> Ratio { match wheel_id { GearWheel::NOSE => self.read_by_name("GEAR_CENTER_POSITION"), GearWheel::LEFT => self.read_by_name("GEAR_LEFT_POSITION"), @@ -6508,7 +6572,7 @@ mod tests { } } - fn get_real_gear_door_position(&mut self, wheel_id: GearWheel) -> f64 { + fn get_real_gear_door_position(&mut self, wheel_id: GearWheel) -> Ratio { match wheel_id { GearWheel::NOSE => self.read_by_name("GEAR_DOOR_CENTER_POSITION"), GearWheel::LEFT => self.read_by_name("GEAR_DOOR_LEFT_POSITION"), @@ -6517,27 +6581,30 @@ mod tests { } fn is_all_gears_really_up(&mut self) -> bool { - self.get_real_gear_position(GearWheel::NOSE) <= 0. - && self.get_real_gear_position(GearWheel::LEFT) <= 0. - && self.get_real_gear_position(GearWheel::RIGHT) <= 0. + self.get_real_gear_position(GearWheel::NOSE) <= Ratio::new::(0.01) + && self.get_real_gear_position(GearWheel::LEFT) <= Ratio::new::(0.01) + && self.get_real_gear_position(GearWheel::RIGHT) <= Ratio::new::(0.01) } fn is_all_gears_really_down(&mut self) -> bool { - self.get_real_gear_position(GearWheel::NOSE) >= 1. - && self.get_real_gear_position(GearWheel::LEFT) >= 1. - && self.get_real_gear_position(GearWheel::RIGHT) >= 1. + self.get_real_gear_position(GearWheel::NOSE) >= Ratio::new::(0.99) + && self.get_real_gear_position(GearWheel::LEFT) >= Ratio::new::(0.99) + && self.get_real_gear_position(GearWheel::RIGHT) >= Ratio::new::(0.99) } fn is_all_doors_really_up(&mut self) -> bool { - self.get_real_gear_door_position(GearWheel::NOSE) <= 0. - && self.get_real_gear_door_position(GearWheel::LEFT) <= 0. - && self.get_real_gear_door_position(GearWheel::RIGHT) <= 0. + self.get_real_gear_door_position(GearWheel::NOSE) <= Ratio::new::(0.01) + && self.get_real_gear_door_position(GearWheel::LEFT) + <= Ratio::new::(0.01) + && self.get_real_gear_door_position(GearWheel::RIGHT) + <= Ratio::new::(0.01) } fn is_all_doors_really_down(&mut self) -> bool { - self.get_real_gear_door_position(GearWheel::NOSE) >= 0.9 - && self.get_real_gear_door_position(GearWheel::LEFT) >= 0.9 - && self.get_real_gear_door_position(GearWheel::RIGHT) >= 0.9 + self.get_real_gear_door_position(GearWheel::NOSE) >= Ratio::new::(0.9) + && self.get_real_gear_door_position(GearWheel::LEFT) >= Ratio::new::(0.9) + && self.get_real_gear_door_position(GearWheel::RIGHT) + >= Ratio::new::(0.9) } fn ac_bus_1_lost(mut self) -> Self { @@ -6744,38 +6811,13 @@ mod tests { self } - fn set_gear_emergency_extension_active(mut self, is_active: bool) -> Self { - self.write_by_name("GEAR_EMERGENCY_EXTENSION_ACTIVE", is_active); - self - } - fn turn_emergency_gear_extension_n_turns(mut self, number_of_turns: u8) -> Self { - let mut number_of_loops = 0; - while self.get_emergency_handle_number_of_turns() < number_of_turns { - self = self - .set_gear_emergency_extension_active(true) - .run_waiting_for(Duration::from_secs_f64(0.5)); - number_of_loops += 1; - assert!(number_of_loops < 50); - } - - self = self.set_gear_emergency_extension_active(false); - + self.write_by_name("GRAVITYGEAR_ROTATE_PCT", number_of_turns as f64 * 100.); self } fn stow_emergency_gear_extension(mut self) -> Self { - let mut number_of_loops = 0; - while self.get_emergency_handle_number_of_turns() != 0 { - self = self - .set_gear_emergency_extension_active(true) - .run_waiting_for(Duration::from_secs_f64(0.5)); - number_of_loops += 1; - assert!(number_of_loops < 50); - } - - self = self.set_gear_emergency_extension_active(false); - + self.write_by_name("GRAVITYGEAR_ROTATE_PCT", 0.); self } @@ -10401,7 +10443,7 @@ mod tests { test_bed = test_bed .turn_emergency_gear_extension_n_turns(2) - .run_waiting_for(Duration::from_secs_f64(10.)); + .run_waiting_for(Duration::from_secs_f64(25.)); assert!(test_bed.is_all_doors_really_down()); } @@ -10484,7 +10526,7 @@ mod tests { test_bed = test_bed .set_gear_lever_down() .turn_emergency_gear_extension_n_turns(3) - .run_waiting_for(Duration::from_secs_f64(20.)); + .run_waiting_for(Duration::from_secs_f64(30.)); assert!(test_bed.is_all_gears_really_down()); assert!(test_bed.is_all_doors_really_down()); @@ -10535,6 +10577,39 @@ mod tests { assert!(test_bed.gear_system_state() == GearSystemState::AllUpLocked); } + #[test] + fn gear_gravity_extension_reverted_has_correct_sequence() { + let mut test_bed = test_bed_in_flight_with() + .set_cold_dark_inputs() + .with_worst_case_ptu() + .in_flight() + .run_one_tick(); + + assert!(test_bed.gear_system_state() == GearSystemState::AllUpLocked); + + test_bed = test_bed + .turn_emergency_gear_extension_n_turns(3) + .run_waiting_for(Duration::from_secs_f64(35.)); + + assert!(test_bed.is_all_doors_really_down()); + assert!(test_bed.is_all_gears_really_down()); + + test_bed = test_bed + .stow_emergency_gear_extension() + .run_waiting_for(Duration::from_secs_f64(10.)); + + // // After 10 seconds we expect gear being retracted and doors still down + assert!(test_bed.gear_system_state() == GearSystemState::Retracting); + assert!(test_bed.is_all_doors_really_down()); + assert!(!test_bed.is_all_gears_really_down()); + + test_bed = test_bed.run_waiting_for(Duration::from_secs_f64(10.)); + + assert!(test_bed.gear_system_state() == GearSystemState::AllUpLocked); + assert!(test_bed.is_all_doors_really_up()); + assert!(test_bed.is_all_gears_really_up()); + } + #[test] fn aileron_init_centered_if_spawning_in_air() { let mut test_bed = test_bed_in_flight_with() @@ -10600,5 +10675,31 @@ mod tests { assert!(test_bed.get_left_elevator_position().get::() < 0.1); assert!(test_bed.get_right_elevator_position().get::() < 0.1); } + + #[test] + fn brakes_on_ground_work_after_emergency_extension() { + let mut test_bed = test_bed_in_flight_with() + .set_cold_dark_inputs() + .in_flight() + .set_gear_lever_up() + .run_waiting_for(Duration::from_secs_f64(1.)); + + assert!(test_bed.gear_system_state() == GearSystemState::AllUpLocked); + + test_bed = test_bed + .turn_emergency_gear_extension_n_turns(3) + .run_waiting_for(Duration::from_secs_f64(30.)); + assert!(test_bed.is_all_gears_really_down()); + assert!(test_bed.is_all_doors_really_down()); + + test_bed = test_bed + .on_the_ground_after_touchdown() + .set_left_brake(Ratio::new::(1.)) + .set_right_brake(Ratio::new::(1.)) + .run_waiting_for(Duration::from_secs_f64(2.)); + + assert!(test_bed.get_brake_left_green_pressure() > Pressure::new::(500.)); + assert!(test_bed.get_brake_right_green_pressure() > Pressure::new::(500.)); + } } } diff --git a/src/systems/a320_systems_wasm/Cargo.toml b/src/systems/a320_systems_wasm/Cargo.toml index d0ebab12deb..eee2513a186 100644 --- a/src/systems/a320_systems_wasm/Cargo.toml +++ b/src/systems/a320_systems_wasm/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] test = false [dependencies] -uom = "0.32.0" +uom = "0.33.0" a320_systems = { path = "../a320_systems" } systems = { path = "../systems" } systems_wasm = { path = "../systems_wasm" } diff --git a/src/systems/a320_systems_wasm/src/ailerons.rs b/src/systems/a320_systems_wasm/src/ailerons.rs index f6508bb5e72..96cb3c8ca9f 100644 --- a/src/systems/a320_systems_wasm/src/ailerons.rs +++ b/src/systems/a320_systems_wasm/src/ailerons.rs @@ -81,7 +81,7 @@ impl VariablesToObject for RollSimOutput { fn variables(&self) -> Vec { vec![ Variable::aspect("HYD_FINAL_AILERON_FEEDBACK"), - Variable::aspect("FLIGHT_CONTROLS_TRACKING_MODE"), + Variable::named("FLIGHT_CONTROLS_TRACKING_MODE"), ] } diff --git a/src/systems/a320_systems_wasm/src/elevators.rs b/src/systems/a320_systems_wasm/src/elevators.rs index 9168303205c..d04b2fc4d99 100644 --- a/src/systems/a320_systems_wasm/src/elevators.rs +++ b/src/systems/a320_systems_wasm/src/elevators.rs @@ -75,7 +75,7 @@ impl VariablesToObject for PitchSimOutput { fn variables(&self) -> Vec { vec![ Variable::aspect("HYD_FINAL_ELEVATOR_FEEDBACK"), - Variable::aspect("FLIGHT_CONTROLS_TRACKING_MODE"), + Variable::named("FLIGHT_CONTROLS_TRACKING_MODE"), ] } diff --git a/src/systems/a320_systems_wasm/src/gear.rs b/src/systems/a320_systems_wasm/src/gear.rs index 95f312983d5..cebf2865ccc 100644 --- a/src/systems/a320_systems_wasm/src/gear.rs +++ b/src/systems/a320_systems_wasm/src/gear.rs @@ -3,9 +3,8 @@ use std::error::Error; use msfs::sim_connect; use msfs::{sim_connect::SimConnect, sim_connect::SIMCONNECT_OBJECT_ID_USER}; -use systems::shared::to_bool; use systems_wasm::aspects::{ - EventToVariableMapping, ExecuteOn, MsfsAspectBuilder, ObjectWrite, VariableToEventMapping, + EventToVariableMapping, MsfsAspectBuilder, ObjectWrite, VariableToEventMapping, VariableToEventWriteOn, VariablesToObject, }; use systems_wasm::{set_data_on_sim_object, Variable}; @@ -56,30 +55,6 @@ pub(super) fn gear(builder: &mut MsfsAspectBuilder) -> Result<(), Box gear_set_set_event_id, ); - // MANUAL GRAVITY GEAR EXTENSION - builder.event_to_variable( - "GEAR_EMERGENCY_HANDLE_TOGGLE", - EventToVariableMapping::Value(1.), - Variable::aspect("GEAR_EMERGENCY_EXTENSION_EVENT"), - |options| options.mask().afterwards_reset_to(0.), - )?; - - builder.map_many( - ExecuteOn::PreTick, - vec![ - Variable::named("GEAR_EMERGENCY_EXTENSION_CLICKED"), - Variable::aspect("GEAR_EMERGENCY_EXTENSION_EVENT"), - ], - |values| { - if to_bool(values[0]) || to_bool(values[1]) { - 1. - } else { - 0. - } - }, - Variable::aspect("GEAR_EMERGENCY_EXTENSION_ACTIVE"), - ); - // GEAR POSITION FEEDBACK TO SIM builder.variables_to_object(Box::new(GearPosition { nose_position: 1., diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index b1b272a9f15..0cede93734c 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -7,6 +7,7 @@ mod gear; mod nose_wheel_steering; mod rudder; mod spoilers; +mod trimmable_horizontal_stabilizer; use a320_systems::A320; use ailerons::ailerons; @@ -25,6 +26,7 @@ use systems::shared::{ }; use systems_wasm::aspects::ExecuteOn; use systems_wasm::{MsfsSimulationBuilder, Variable}; +use trimmable_horizontal_stabilizer::trimmable_horizontal_stabilizer; #[msfs::gauge(name=systems)] async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { @@ -268,6 +270,7 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { .with_aspect(elevators)? .with_aspect(rudder)? .with_aspect(gear)? + .with_aspect(trimmable_horizontal_stabilizer)? .build(A320::new)?; while let Some(event) = gauge.next_event().await { diff --git a/src/systems/a320_systems_wasm/src/rudder.rs b/src/systems/a320_systems_wasm/src/rudder.rs index 67532e76b36..60e1fc26c22 100644 --- a/src/systems/a320_systems_wasm/src/rudder.rs +++ b/src/systems/a320_systems_wasm/src/rudder.rs @@ -23,10 +23,10 @@ pub(super) fn rudder(builder: &mut MsfsAspectBuilder) -> Result<(), Box Vec { vec![ Variable::aspect("HYD_FINAL_RUDDER_FEEDBACK"), - Variable::aspect("FLIGHT_CONTROLS_TRACKING_MODE"), + Variable::named("FLIGHT_CONTROLS_TRACKING_MODE"), ] } diff --git a/src/systems/a320_systems_wasm/src/trimmable_horizontal_stabilizer.rs b/src/systems/a320_systems_wasm/src/trimmable_horizontal_stabilizer.rs new file mode 100644 index 00000000000..1090ae1ec0b --- /dev/null +++ b/src/systems/a320_systems_wasm/src/trimmable_horizontal_stabilizer.rs @@ -0,0 +1,130 @@ +use std::error::Error; + +use systems_wasm::aspects::{ + EventToVariableMapping, ExecuteOn, MsfsAspectBuilder, ObjectWrite, VariablesToObject, +}; +use systems_wasm::{set_data_on_sim_object, Variable}; + +use systems::shared::to_bool; + +use msfs::sim_connect; +use msfs::{sim_connect::SimConnect, sim_connect::SIMCONNECT_OBJECT_ID_USER}; + +pub(super) fn trimmable_horizontal_stabilizer( + builder: &mut MsfsAspectBuilder, +) -> Result<(), Box> { + builder.event_to_variable( + "ELEV_TRIM_UP", + EventToVariableMapping::Value(35.), + Variable::aspect("THS_MANUAL_CONTROL_SPEED_KEY"), + |options| options.mask().afterwards_reset_to(0.), + )?; + + builder.event_to_variable( + "ELEV_TRIM_DN", + EventToVariableMapping::Value(-35.), + Variable::aspect("THS_MANUAL_CONTROL_SPEED_KEY"), + |options| options.mask().afterwards_reset_to(0.), + )?; + + builder.event_to_variable( + "ELEVATOR_TRIM_SET", + EventToVariableMapping::EventDataToValue(|event_data| (event_data as f64) / 16383.), + Variable::aspect("THS_MAN_POS_SET_16K"), + |options| options.mask().afterwards_reset_to(-1.), + )?; + + builder.event_to_variable( + "AXIS_ELEV_TRIM_SET", + EventToVariableMapping::EventData32kPosition, + Variable::aspect("THS_MAN_POS_SET_32K"), + |options| options.mask().afterwards_reset_to(-1.), + )?; + + // Sends a trim speed from position error + builder.map_many( + ExecuteOn::PreTick, + vec![ + Variable::aspect("THS_MAN_POS_SET_16K"), + Variable::aspect("THS_MAN_POS_SET_32K"), + Variable::named("HYD_TRIM_WHEEL_PERCENT"), + ], + |values| { + if values[0] >= 0. { + pos_error_to_speed(values[0] - values[2] / 100.) + } else if values[1] >= 0. { + pos_error_to_speed(values[1] - values[2] / 100.) + } else { + 0. + } + }, + Variable::aspect("THS_MANUAL_CONTROL_SPEED_AXIS"), + ); + + // Selects final speed to use from keys or axis events + builder.map_many( + ExecuteOn::PreTick, + vec![ + Variable::aspect("THS_MANUAL_CONTROL_SPEED_KEY"), + Variable::aspect("THS_MANUAL_CONTROL_SPEED_AXIS"), + ], + |values| { + if values[0].abs() > 0. { + values[0] + } else { + values[1] + } + }, + Variable::aspect("THS_MANUAL_CONTROL_SPEED"), + ); + + // Sends manual control state when receiveing an event even if position is not moving or when keys event are used + builder.map_many( + ExecuteOn::PreTick, + vec![ + Variable::aspect("THS_MAN_POS_SET_16K"), + Variable::aspect("THS_MAN_POS_SET_32K"), + Variable::aspect("THS_MANUAL_CONTROL_SPEED_KEY"), + ], + |values| { + if values[0] >= 0. || values[1] >= 0. || values[2].abs() > 0. { + 1. + } else { + 0. + } + }, + Variable::aspect("THS_MANUAL_CONTROL_ACTIVE"), + ); + + builder.variables_to_object(Box::new(PitchTrimSimOutput { elevator_trim: 0. })); + + Ok(()) +} + +#[sim_connect::data_definition] +struct PitchTrimSimOutput { + #[name = "ELEVATOR TRIM POSITION"] + #[unit = "DEGREE"] + elevator_trim: f64, +} +impl VariablesToObject for PitchTrimSimOutput { + fn variables(&self) -> Vec { + vec![ + Variable::aspect("HYD_FINAL_THS_DEFLECTION"), + Variable::named("FLIGHT_CONTROLS_TRACKING_MODE"), + ] + } + + fn write(&mut self, values: Vec) -> ObjectWrite { + self.elevator_trim = values[0]; + + //Not writing control feedback when in tracking mode + ObjectWrite::on(!to_bool(values[1])) + } + + set_data_on_sim_object!(); +} + +fn pos_error_to_speed(error: f64) -> f64 { + (1000. * error).min(45.).max(-45.) +} diff --git a/src/systems/systems/Cargo.toml b/src/systems/systems/Cargo.toml index 38681c472fb..9d7b31c767f 100644 --- a/src/systems/systems/Cargo.toml +++ b/src/systems/systems/Cargo.toml @@ -5,7 +5,7 @@ authors = ["FlyByWire Simulations"] edition = "2018" [dependencies] -uom = "0.32.0" +uom = "0.33.0" rand = { version = "0.8.0", features = ["small_rng"] } rand_distr = "0.4.3" ntest = "0.7.2" diff --git a/src/systems/systems/src/hydraulic/electrical_generator.rs b/src/systems/systems/src/hydraulic/electrical_generator.rs index d39d0b4a8ba..e5e85daa0f4 100644 --- a/src/systems/systems/src/hydraulic/electrical_generator.rs +++ b/src/systems/systems/src/hydraulic/electrical_generator.rs @@ -16,8 +16,9 @@ use crate::shared::{ HydraulicGeneratorControlUnit, LgciuWeightOnWheels, SectionPressure, }; -use crate::simulation::{InitContext, VariableIdentifier}; -use crate::simulation::{SimulationElement, SimulatorWriter, UpdateContext, Write}; +use crate::simulation::{ + InitContext, SimulationElement, SimulatorWriter, UpdateContext, VariableIdentifier, Write, +}; use std::time::Duration; diff --git a/src/systems/systems/src/hydraulic/flap_slat.rs b/src/systems/systems/src/hydraulic/flap_slat.rs index 027647ff9dd..4917804870f 100644 --- a/src/systems/systems/src/hydraulic/flap_slat.rs +++ b/src/systems/systems/src/hydraulic/flap_slat.rs @@ -257,11 +257,9 @@ impl FlapSlatAssembly { sfcc2_angle_request: Option, ) { if let Some(sfcc1_angle) = sfcc1_angle_request { - self.final_requested_synchro_gear_position = - self.feedback_angle_from_surface_angle(sfcc1_angle); + self.final_requested_synchro_gear_position = sfcc1_angle; } else if let Some(sfcc2_angle) = sfcc2_angle_request { - self.final_requested_synchro_gear_position = - self.feedback_angle_from_surface_angle(sfcc2_angle); + self.final_requested_synchro_gear_position = sfcc2_angle; } } @@ -430,6 +428,7 @@ impl FlapSlatAssembly { )) } + #[cfg(test)] /// Gets Feedback Position Pickup Unit (FPPU) position from current flap surface angle fn feedback_angle_from_surface_angle(&self, flap_surface_angle: Angle) -> Angle { Angle::new::(interpolation( @@ -564,18 +563,20 @@ mod tests { self.right_motor_pressure.set_pressure(right_motor_pressure); } - fn set_angle_request(&mut self, angle_request: Option) { - self.left_motor_angle_request = angle_request; - self.right_motor_angle_request = angle_request; + fn set_angle_request(&mut self, surface_angle_request: Option) { + self.left_motor_angle_request = flap_fppu_from_surface_angle(surface_angle_request); + self.right_motor_angle_request = flap_fppu_from_surface_angle(surface_angle_request); } fn set_angle_per_sfcc( &mut self, - angle_request_sfcc1: Option, - angle_request_sfcc2: Option, + surface_angle_request_sfcc1: Option, + surface_angle_request_sfcc2: Option, ) { - self.left_motor_angle_request = angle_request_sfcc1; - self.right_motor_angle_request = angle_request_sfcc2; + self.left_motor_angle_request = + flap_fppu_from_surface_angle(surface_angle_request_sfcc1); + self.right_motor_angle_request = + flap_fppu_from_surface_angle(surface_angle_request_sfcc2); } } impl Aircraft for TestAircraft { @@ -1083,4 +1084,22 @@ mod tests { Pressure::new::(MAX_CIRCUIT_PRESSURE_PSI), ) } + + #[cfg(test)] + fn flap_fppu_from_surface_angle(surface_angle: Option) -> Option { + let synchro_gear_map = [ + 0., 65., 115., 120.53, 136., 145.5, 152., 165., 168.3, 179., 231.2, 251.97, + ]; + let surface_degrees_breakpoints = [ + 0., 10.318, 18.2561, 19.134, 21.59, 23.098, 24.13, 26.196, 26.72, 28.42, 36.703, 40., + ]; + + surface_angle.map(|angle| { + Angle::new::(interpolation( + &surface_degrees_breakpoints, + &synchro_gear_map, + angle.get::(), + )) + }) + } } diff --git a/src/systems/systems/src/hydraulic/landing_gear.rs b/src/systems/systems/src/hydraulic/landing_gear.rs index bbc63704b3f..cf466b46429 100644 --- a/src/systems/systems/src/hydraulic/landing_gear.rs +++ b/src/systems/systems/src/hydraulic/landing_gear.rs @@ -585,9 +585,9 @@ impl GearSystemComponentHydraulicController { self.actual_position = actual_position; self.requested_position = if should_open { - Ratio::new::(1.5) + Ratio::new::(1.1) } else { - Ratio::new::(-0.5) + Ratio::new::(-0.1) }; self.should_lock = actual_position.get::() > 0.5 && should_downlock diff --git a/src/systems/systems/src/hydraulic/linear_actuator.rs b/src/systems/systems/src/hydraulic/linear_actuator.rs index 2313c789a51..5df096316ba 100644 --- a/src/systems/systems/src/hydraulic/linear_actuator.rs +++ b/src/systems/systems/src/hydraulic/linear_actuator.rs @@ -772,7 +772,12 @@ impl HydraulicLinearActuatorAssembly { current_pressure: [Pressure; N], ) { for (index, actuator) in self.linear_actuators.iter_mut().enumerate() { - actuator.set_position_target(assembly_controllers[index].requested_position()); + actuator.set_position_target( + self.rigid_body + .linear_actuator_pos_normalized_from_angular_position_normalized( + assembly_controllers[index].requested_position(), + ), + ); } // Only one lock mechanism on the connected rigid body so we only look at first controller demand @@ -878,6 +883,9 @@ pub struct LinearActuatedRigidBodyOnHingeAxis { rotation_transform: Rotation3, plane_acceleration_filtered: LowPassFilter>, + + min_absolute_length_to_anchor: Length, + max_absolute_length_to_anchor: Length, } impl LinearActuatedRigidBodyOnHingeAxis { // Rebound energy when hiting min or max position. 0.3 means the body rebounds at 30% of the speed it hit the min/max position @@ -940,11 +948,15 @@ impl LinearActuatedRigidBodyOnHingeAxis { plane_acceleration_filtered: LowPassFilter::>::new( Self::PLANE_ACCELERATION_FILTERING_TIME_CONSTANT, ), + min_absolute_length_to_anchor: Length::default(), + max_absolute_length_to_anchor: Length::default(), }; // Make sure the new object has coherent structure by updating internal roations and positions once new_body.initialize_actuator_force_direction(); new_body.update_all_rotations(); + new_body.init_min_max_linear_length(); new_body.init_position_normalized(); + new_body } @@ -1155,18 +1167,51 @@ impl LinearActuatedRigidBodyOnHingeAxis { Length::new::((self.anchor_point - control_arm_position).norm()) } + + fn init_min_max_linear_length(&mut self) { + let length_at_min_angle = self.absolute_length_to_anchor_at_angle(self.min_angle); + let length_at_max_angle = self.absolute_length_to_anchor_at_angle(self.max_angle); + self.min_absolute_length_to_anchor = length_at_min_angle.min(length_at_max_angle); + self.max_absolute_length_to_anchor = length_at_min_angle.max(length_at_max_angle); + } + + fn linear_length_normalized_from_absolute_angle(&self, angle_demand: Angle) -> Ratio { + let total_linear_travel = + self.max_absolute_length_to_anchor - self.min_absolute_length_to_anchor; + + let current_length_from_angle = self.absolute_length_to_anchor_at_angle(angle_demand); + + (current_length_from_angle - self.min_absolute_length_to_anchor) / total_linear_travel + } + + fn angular_ratio_to_absolute_angle(&self, angular_ratio_normalized: Ratio) -> Angle { + if self.actuator_extension_gives_positive_angle() { + angular_ratio_normalized.get::() * self.total_travel + self.min_angle + } else { + -angular_ratio_normalized.get::() * self.total_travel + self.max_angle + } + } + + pub fn linear_actuator_pos_normalized_from_angular_position_normalized( + &self, + angular_ratio_normalized: Ratio, + ) -> Ratio { + // We allow 0.1 over min and max range so actuators that are controlled to end position do not slow down just before max position + let limited_ratio = angular_ratio_normalized + .max(Ratio::new::(-0.1)) + .min(Ratio::new::(1.1)); + self.linear_length_normalized_from_absolute_angle( + self.angular_ratio_to_absolute_angle(limited_ratio), + ) + } } impl BoundedLinearLength for LinearActuatedRigidBodyOnHingeAxis { fn min_absolute_length_to_anchor(&self) -> Length { - let length_at_min_angle = self.absolute_length_to_anchor_at_angle(self.min_angle); - let length_at_max_angle = self.absolute_length_to_anchor_at_angle(self.max_angle); - length_at_min_angle.min(length_at_max_angle) + self.min_absolute_length_to_anchor } fn max_absolute_length_to_anchor(&self) -> Length { - let length_at_min_angle = self.absolute_length_to_anchor_at_angle(self.min_angle); - let length_at_max_angle = self.absolute_length_to_anchor_at_angle(self.max_angle); - length_at_min_angle.max(length_at_max_angle) + self.max_absolute_length_to_anchor } fn absolute_length_to_anchor(&self) -> Length { @@ -1197,11 +1242,12 @@ impl AerodynamicBody for LinearActuatedRigidBodyOnHingeAxis { #[cfg(test)] mod tests { use nalgebra::Vector3; + use ntest::assert_about_eq; use super::*; use crate::shared::update_iterator::MaxStepLoop; - use crate::simulation::test::{SimulationTestBed, TestBed, WriteByName}; + use crate::simulation::test::{ElementCtorFn, SimulationTestBed, TestBed, WriteByName}; use crate::simulation::{Aircraft, SimulationElement}; use std::time::Duration; use uom::si::{angle::degree, mass::kilogram, pressure::psi}; @@ -1352,12 +1398,6 @@ mod tests { self.aero_forces.apply_up_force(force_up); } - fn actuator_position(&self, actuator_id: usize) -> Ratio { - assert!(actuator_id < N); - self.hydraulic_assembly - .actuator_position_normalized(actuator_id) - } - fn is_locked(&self) -> bool { self.hydraulic_assembly.is_locked() } @@ -1409,6 +1449,235 @@ mod tests { } impl SimulationElement for TestAircraft<2> {} + impl SimulationElement for LinearActuatedRigidBodyOnHingeAxis {} + + #[test] + fn asymetrical_deflection_body_converts_angle_to_linear_ratio() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| elevator_body())); + + assert!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(0.)) + }) + .get::() + >= 0.35 + ); + assert!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(0.)) + }) + .get::() + <= 0.45 + ); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(-17.)) + }) + .get::(), + 0. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(30.)) + }) + .get::(), + 1. + ); + } + + #[test] + fn asymetrical_deflection_body_converts_angle_ratio_to_absolute_angle() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| elevator_body())); + + assert!( + test_bed + .command_element(|e| { + e.angular_ratio_to_absolute_angle(Ratio::new::(0.36)) + }) + .get::() + >= -1. + ); + assert!( + test_bed + .command_element(|e| { + e.angular_ratio_to_absolute_angle(Ratio::new::(0.36)) + }) + .get::() + <= 1. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { e.angular_ratio_to_absolute_angle(Ratio::new::(0.)) }) + .get::(), + -17. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { e.angular_ratio_to_absolute_angle(Ratio::new::(1.)) }) + .get::(), + 30. + ); + } + + #[test] + fn symetrical_deflection_body_converts_angle_to_linear_ratio() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| aileron_body(true))); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(0.)) + }) + .get::(), + 0.5, + 0.001 + ); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(-25.)) + }) + .get::(), + 0. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(25.)) + }) + .get::(), + 1. + ); + } + + #[test] + fn left_gear_body_converts_angle_ratio_to_absolute_angle() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| main_gear_left_body(true))); + + assert!( + test_bed + .command_element(|e| { + e.angular_ratio_to_absolute_angle(Ratio::new::(0.5)) + }) + .get::() + >= 35. + ); + assert!( + test_bed + .command_element(|e| { + e.angular_ratio_to_absolute_angle(Ratio::new::(0.5)) + }) + .get::() + <= 45. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { e.angular_ratio_to_absolute_angle(Ratio::new::(0.)) }) + .get::(), + 0. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { e.angular_ratio_to_absolute_angle(Ratio::new::(1.)) }) + .get::(), + 80. + ); + } + + #[test] + fn right_gear_body_converts_angle_ratio_to_absolute_angle() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| main_gear_right_body(true))); + + assert!( + test_bed + .command_element(|e| { + e.angular_ratio_to_absolute_angle(Ratio::new::(0.5)) + }) + .get::() + <= -35. + ); + assert!( + test_bed + .command_element(|e| { + e.angular_ratio_to_absolute_angle(Ratio::new::(0.5)) + }) + .get::() + >= -45. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { e.angular_ratio_to_absolute_angle(Ratio::new::(0.)) }) + .get::(), + 0. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { e.angular_ratio_to_absolute_angle(Ratio::new::(1.)) }) + .get::(), + -80. + ); + } + + #[test] + fn left_gear_body_converts_angle_to_linear_ratio() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| main_gear_left_body(true))); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(0.)) + }) + .get::(), + 0. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(80.)) + }) + .get::(), + 1. + ); + } + + #[test] + fn right_gear_body_converts_angle_to_linear_ratio() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(|_| main_gear_right_body(true))); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(0.)) + }) + .get::(), + 0. + ); + + assert_about_eq!( + test_bed + .command_element(|e| { + e.linear_length_normalized_from_absolute_angle(Angle::new::(-80.)) + }) + .get::(), + 1. + ); + } + #[test] fn linear_actuator_not_moving_on_locked_rigid_body() { let mut test_bed = SimulationTestBed::new(|_| { @@ -1651,14 +1920,14 @@ mod tests { test_bed.run_with_delta(Duration::from_secs(20)); - assert!(test_bed.query(|a| a.actuator_position(0)) > Ratio::new::(0.68)); - assert!(test_bed.query(|a| a.actuator_position(0)) < Ratio::new::(0.72)); + assert!(test_bed.query(|a| a.body_position()) > Ratio::new::(0.68)); + assert!(test_bed.query(|a| a.body_position()) < Ratio::new::(0.72)); test_bed.command(|a| a.command_position_control(Ratio::new::(0.2), 0)); test_bed.run_with_delta(Duration::from_secs(20)); - assert!(test_bed.query(|a| a.actuator_position(0)) > Ratio::new::(0.18)); - assert!(test_bed.query(|a| a.actuator_position(0)) < Ratio::new::(0.22)); + assert!(test_bed.query(|a| a.body_position()) > Ratio::new::(0.18)); + assert!(test_bed.query(|a| a.body_position()) < Ratio::new::(0.22)); } #[test] @@ -1986,7 +2255,7 @@ mod tests { }); test_bed.command(|a| a.set_pressures([Pressure::new::(3000.)])); - test_bed.command(|a| a.command_position_control(Ratio::new::(-0.5), 0)); + test_bed.command(|a| a.command_position_control(Ratio::new::(-0.1), 0)); test_bed.command(|a| a.command_unlock()); test_bed.run_with_delta(Duration::from_secs(1)); diff --git a/src/systems/systems/src/hydraulic/mod.rs b/src/systems/systems/src/hydraulic/mod.rs index 26ac1e1cc6a..9e4c1390ac1 100644 --- a/src/systems/systems/src/hydraulic/mod.rs +++ b/src/systems/systems/src/hydraulic/mod.rs @@ -34,6 +34,7 @@ pub mod landing_gear; pub mod linear_actuator; pub mod nose_steering; pub mod pumps; +pub mod trimmable_horizontal_stabilizer; /// Indicates the pressure sensors info of an hydraulic circuit at different locations /// Information can be wrong in case of sensor failure -> do not use for physical pressure diff --git a/src/systems/systems/src/hydraulic/trimmable_horizontal_stabilizer.rs b/src/systems/systems/src/hydraulic/trimmable_horizontal_stabilizer.rs new file mode 100644 index 00000000000..06175393774 --- /dev/null +++ b/src/systems/systems/src/hydraulic/trimmable_horizontal_stabilizer.rs @@ -0,0 +1,1066 @@ +use uom::si::{ + angle::{degree, radian}, + angular_velocity::{radian_per_second, revolution_per_minute}, + f64::*, + pressure::psi, + ratio::ratio, + volume::gallon, + volume_rate::gallon_per_minute, +}; + +use crate::simulation::{ + InitContext, SimulationElement, SimulationElementVisitor, SimulatorWriter, UpdateContext, + VariableIdentifier, Write, +}; + +use crate::shared::{ + interpolation, low_pass_filter::LowPassFilter, ElectricalBusType, ElectricalBuses, +}; + +use super::linear_actuator::Actuator; + +use std::time::Duration; + +struct TrimWheels { + position_percent_id: VariableIdentifier, + + position: Angle, + trim_actuator_over_trim_wheel_ratio: Ratio, + + min_angle: Angle, + max_angle: Angle, +} +impl TrimWheels { + fn new( + context: &mut InitContext, + trim_actuator_over_trim_wheel_ratio: Ratio, + min_angle: Angle, + total_range_angle: Angle, + ) -> Self { + Self { + position_percent_id: context.get_identifier("HYD_TRIM_WHEEL_PERCENT".to_owned()), + + position: Angle::default(), + trim_actuator_over_trim_wheel_ratio, + min_angle, + max_angle: min_angle + total_range_angle, + } + } + + fn update(&mut self, pta: &PitchTrimActuator) { + self.position = pta.position / self.trim_actuator_over_trim_wheel_ratio.get::(); + } + + fn position_normalized(&self) -> Ratio { + ((self.position - self.min_angle) / (self.max_angle - self.min_angle)) + .min(Ratio::new::(100.)) + .max(Ratio::new::(0.)) + } +} +impl SimulationElement for TrimWheels { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write( + &self.position_percent_id, + self.position_normalized().get::() * 100., + ); + } +} + +#[derive(Clone, Copy)] +struct DriveMotor { + speed: LowPassFilter, + + is_active: bool, + max_speed: AngularVelocity, + + speed_regulation_coef_map: [f64; 7], + speed_error_breakpoint: [f64; 7], +} +impl DriveMotor { + fn new( + max_speed: AngularVelocity, + + speed_error_breakpoint: [f64; 7], + speed_regulation_coef_map: [f64; 7], + ) -> Self { + Self { + speed: LowPassFilter::new(Duration::from_millis(50)), + + is_active: false, + max_speed, + + speed_regulation_coef_map, + speed_error_breakpoint, + } + } + + fn set_active_state(&mut self, is_active: bool) { + self.is_active = is_active; + } + + fn update( + &mut self, + context: &UpdateContext, + measured_position: Angle, + requested_position: Angle, + ) { + let new_speed = if self.is_active { + let position_error = requested_position - measured_position; + + let speed_coef = interpolation( + &self.speed_error_breakpoint, + &self.speed_regulation_coef_map, + position_error.get::(), + ); + + self.max_speed * speed_coef + } else { + AngularVelocity::default() + }; + + self.speed.update(context.delta(), new_speed); + } + + fn speed(&self) -> AngularVelocity { + self.speed.output() + } + + fn set_max_speed(&mut self, new_max_speed: AngularVelocity) { + self.max_speed = new_max_speed; + } +} + +#[derive(PartialEq, Clone, Copy)] +enum ElectricalTrimMotorElecBus { + _Norm = 0, + Standby = 1, +} + +struct ElectricDriveMotor { + motor: DriveMotor, + + is_powered: bool, + + powered_by_bus_array: Vec, + powered_by_bus: ElectricalTrimMotorElecBus, +} +impl ElectricDriveMotor { + /// Creates an electric motor driving the trim input system. + /// Power bus provided can contain only one main bus, or one main bus plus a secondary standby bus + fn new( + max_speed: AngularVelocity, + + speed_error_breakpoint: [f64; 7], + speed_regulation_coef_map: [f64; 7], + + powered_by_bus_array: Vec, + ) -> Self { + // Only supports one main bus or one main plus one standby + assert!(powered_by_bus_array.len() <= 2); + + Self { + motor: DriveMotor::new(max_speed, speed_error_breakpoint, speed_regulation_coef_map), + is_powered: true, + + powered_by_bus_array, + + // TODO: Init with Standby to select most restrictive case until the bus selection logic is implemented + powered_by_bus: ElectricalTrimMotorElecBus::Standby, + } + } + + fn set_active_state(&mut self, is_active: bool) { + self.motor.set_active_state(is_active && self.is_powered); + } + + fn update( + &mut self, + context: &UpdateContext, + measured_position: Angle, + requested_position: Angle, + ) { + self.motor + .update(context, measured_position, requested_position); + } + + fn speed(&self) -> AngularVelocity { + self.motor.speed() + } + + /// Selects which bus powers the motor (if more than one bus available for that motor) + // TODO: The logic to select this bus is not implemented yet in flight computers + fn _set_power_bus_in_use(&mut self, elec_bus_in_use: ElectricalTrimMotorElecBus) { + self.powered_by_bus = elec_bus_in_use; + } + + fn is_powered(&self) -> bool { + self.is_powered + } +} +impl SimulationElement for ElectricDriveMotor { + fn receive_power(&mut self, buses: &impl ElectricalBuses) { + let bus_selected_index: usize = + (self.powered_by_bus as usize).min(self.powered_by_bus_array.len() - 1); + + self.is_powered = buses.is_powered(self.powered_by_bus_array[bus_selected_index]); + } +} + +#[derive(Clone, Copy)] +struct HydraulicDriveMotor { + max_speed: AngularVelocity, + motor: DriveMotor, + + total_volume_to_actuator: Volume, + total_volume_to_reservoir: Volume, +} +impl HydraulicDriveMotor { + const FLOW_CONSTANT_RPM_CUBIC_INCH_TO_GPM: f64 = 231.; + const DISPLACEMENT_CUBIC_INCH: f64 = 0.4; + + // Hyd pressure at which motor has its max speed + const MAX_SPEED_HYD_PRESSURE_PSI: f64 = 3000.; + + fn new( + max_speed: AngularVelocity, + + speed_error_breakpoint: [f64; 7], + speed_regulation_coef_map: [f64; 7], + ) -> Self { + Self { + max_speed, + motor: DriveMotor::new(max_speed, speed_error_breakpoint, speed_regulation_coef_map), + + total_volume_to_actuator: Volume::default(), + total_volume_to_reservoir: Volume::default(), + } + } + + fn update( + &mut self, + context: &UpdateContext, + measured_position: Angle, + position_requested: Angle, + pressure: Pressure, + ) { + self.motor + .set_active_state(pressure > Pressure::new::(1450.)); + + self.motor + .set_max_speed(self.current_max_speed_from_hydraulic_pressure(pressure)); + + self.motor + .update(context, measured_position, position_requested); + + self.update_flow(context); + } + + fn update_flow(&mut self, context: &UpdateContext) { + let total_volume = self.flow() * context.delta_as_time(); + self.total_volume_to_actuator += total_volume.abs(); + self.total_volume_to_reservoir = self.total_volume_to_actuator; + } + + fn current_max_speed_from_hydraulic_pressure(&self, pressure: Pressure) -> AngularVelocity { + let pressure_coefficient = (pressure.get::() / Self::MAX_SPEED_HYD_PRESSURE_PSI) + .min(1.) + .max(0.); + + self.max_speed * pressure_coefficient + } + + fn speed(&self) -> AngularVelocity { + self.motor.speed() + } + + fn flow(&self) -> VolumeRate { + VolumeRate::new::( + self.speed().get::() * Self::DISPLACEMENT_CUBIC_INCH + / Self::FLOW_CONSTANT_RPM_CUBIC_INCH_TO_GPM, + ) + } +} +impl Actuator for HydraulicDriveMotor { + fn used_volume(&self) -> Volume { + self.total_volume_to_actuator + } + + fn reservoir_return(&self) -> Volume { + self.total_volume_to_reservoir + } + + fn reset_volumes(&mut self) { + self.total_volume_to_reservoir = Volume::new::(0.); + self.total_volume_to_actuator = Volume::new::(0.); + } +} + +#[derive(Clone, Copy)] +struct ElectricMotorClutch { + is_powered: bool, + is_energized: bool, +} +impl ElectricMotorClutch { + fn default() -> Self { + Self { + is_powered: true, + is_energized: false, + } + } + + fn is_clutch_engaged(&self) -> bool { + self.is_powered && self.is_energized + } + + fn set_energized(&mut self, is_energized: bool) { + self.is_energized = is_energized; + } + + fn set_is_powered(&mut self, elec_motor_is_powered: bool) { + self.is_powered = elec_motor_is_powered; + } +} + +pub trait PitchTrimActuatorController { + fn commanded_position(&self) -> Angle; + fn energised_motor(&self) -> [bool; 3]; +} + +pub trait ManualPitchTrimController { + fn is_manually_moved(&self) -> bool; + fn moving_speed(&self) -> AngularVelocity; +} + +struct PitchTrimActuator { + manual_override_id: VariableIdentifier, + + electric_motors: [ElectricDriveMotor; 3], + electric_clutches: [ElectricMotorClutch; 3], + manual_override_active: bool, + + position: Angle, + speed: AngularVelocity, + + elec_motor_over_trim_actuator_ratio: Ratio, + + min_actuator_angle: Angle, + max_actuator_angle: Angle, +} +impl PitchTrimActuator { + const ELECTRIC_MOTOR_POSITION_ERROR_BREAKPOINT: [f64; 7] = + [-50., -5., -0.05, 0., 0.05, 5., 50.]; + const ELECTRIC_MOTOR_SPEED_REGULATION_COEF_MAP: [f64; 7] = [-1., -1., -0.01, 0., 0.01, 1., 1.]; + + const MIN_ELEC_MOTOR_SPEED_FOR_MANUAL_OVERRIDE_DETECTION_RPM: f64 = 10.; + + fn new( + context: &mut InitContext, + min_actuator_angle: Angle, + total_actuator_range_angle: Angle, + max_elec_motor_speed: AngularVelocity, + elec_motor_over_trim_actuator_ratio: Ratio, + ) -> Self { + Self { + manual_override_id: context.get_identifier("HYD_THS_TRIM_MANUAL_OVERRIDE".to_owned()), + + electric_motors: [ + ElectricDriveMotor::new( + max_elec_motor_speed, + Self::ELECTRIC_MOTOR_POSITION_ERROR_BREAKPOINT, + Self::ELECTRIC_MOTOR_SPEED_REGULATION_COEF_MAP, + vec![ + ElectricalBusType::DirectCurrent(2), + ElectricalBusType::DirectCurrentHot(2), + ], + ), + ElectricDriveMotor::new( + max_elec_motor_speed, + Self::ELECTRIC_MOTOR_POSITION_ERROR_BREAKPOINT, + Self::ELECTRIC_MOTOR_SPEED_REGULATION_COEF_MAP, + vec![ElectricalBusType::DirectCurrentEssential], + ), + ElectricDriveMotor::new( + max_elec_motor_speed, + Self::ELECTRIC_MOTOR_POSITION_ERROR_BREAKPOINT, + Self::ELECTRIC_MOTOR_SPEED_REGULATION_COEF_MAP, + vec![ElectricalBusType::DirectCurrent(2)], + ), + ], + electric_clutches: [ElectricMotorClutch::default(); 3], + manual_override_active: false, + + position: Angle::default(), + speed: AngularVelocity::default(), + + elec_motor_over_trim_actuator_ratio, + + min_actuator_angle, + max_actuator_angle: min_actuator_angle + total_actuator_range_angle, + } + } + + fn update( + &mut self, + context: &UpdateContext, + electric_controller: &impl PitchTrimActuatorController, + manual_controller: &impl ManualPitchTrimController, + ths_hydraulic_assembly: &TrimmableHorizontalStabilizerHydraulics, + ) { + self.update_clutches_state(electric_controller); + self.update_motors(context, electric_controller, ths_hydraulic_assembly); + + self.update_speed_and_override(manual_controller, ths_hydraulic_assembly); + + self.update_position(context); + } + + fn update_position(&mut self, context: &UpdateContext) { + self.position += Angle::new::( + self.speed.get::() * context.delta_as_secs_f64(), + ); + + self.position = self + .position + .min(self.max_actuator_angle) + .max(self.min_actuator_angle); + } + + fn update_speed_and_override( + &mut self, + manual_controller: &impl ManualPitchTrimController, + ths_hydraulic_assembly: &TrimmableHorizontalStabilizerHydraulics, + ) { + let elec_drive_speed = self.elec_motor_drive_total_speed(); + + if manual_controller.is_manually_moved() { + self.speed = manual_controller.moving_speed(); + self.manual_override_active = elec_drive_speed.get::().abs() + > Self::MIN_ELEC_MOTOR_SPEED_FOR_MANUAL_OVERRIDE_DETECTION_RPM; + } else { + self.speed = elec_drive_speed; + self.manual_override_active = false + } + + if ths_hydraulic_assembly.is_at_max_down_spool_valve + && self.speed.get::() < 0. + || ths_hydraulic_assembly.is_at_max_up_spool_valve + && self.speed.get::() > 0. + { + self.speed = AngularVelocity::default(); + } + } + + fn elec_motor_drive_total_speed(&self) -> AngularVelocity { + let mut sum_of_speeds = AngularVelocity::default(); + + for (motor_index, motor) in self.electric_motors.iter().enumerate() { + if self.electric_clutches[motor_index].is_clutch_engaged() { + sum_of_speeds += + motor.speed() / self.elec_motor_over_trim_actuator_ratio.get::(); + } + } + + sum_of_speeds + } + + fn update_clutches_state(&mut self, controller: &impl PitchTrimActuatorController) { + for (clutch_index, clutch) in self.electric_clutches.iter_mut().enumerate() { + clutch.set_is_powered(self.electric_motors[clutch_index].is_powered()); + clutch.set_energized(controller.energised_motor()[clutch_index]); + } + } + + fn update_motors( + &mut self, + context: &UpdateContext, + controller: &impl PitchTrimActuatorController, + ths_hydraulic_assembly: &TrimmableHorizontalStabilizerHydraulics, + ) { + for (motor_index, motor) in self.electric_motors.iter_mut().enumerate() { + motor.set_active_state(controller.energised_motor()[motor_index]); + + let trim_actuator_normalized_position_request = ths_hydraulic_assembly + .normalized_position_from_ths_deflection(controller.commanded_position()); + + let final_trim_actuator_position_request = trim_actuator_normalized_position_request + .get::() + * (self.max_actuator_angle - self.min_actuator_angle) + + self.min_actuator_angle; + + motor.update(context, self.position, final_trim_actuator_position_request); + } + } + + fn position_normalized(&self) -> Ratio { + let range = self.max_actuator_angle - self.min_actuator_angle; + + Ratio::new::( + (self.position.get::() - self.min_actuator_angle.get::()) + / range.get::(), + ) + .min(Ratio::new::(100.)) + .max(Ratio::new::(0.)) + } +} +impl SimulationElement for PitchTrimActuator { + fn accept(&mut self, visitor: &mut T) { + accept_iterable!(self.electric_motors, visitor); + visitor.visit(self); + } + + fn write(&self, writer: &mut SimulatorWriter) { + writer.write(&self.manual_override_id, self.manual_override_active); + } +} + +pub struct TrimmableHorizontalStabilizerAssembly { + pitch_trim_actuator: PitchTrimActuator, + trim_wheel: TrimWheels, + ths_hydraulics: TrimmableHorizontalStabilizerHydraulics, +} +impl TrimmableHorizontalStabilizerAssembly { + pub fn new( + context: &mut InitContext, + + min_actuator_angle: Angle, + total_actuator_range_angle: Angle, + + min_trim_wheel_angle: Angle, + total_trim_wheel_range_angle: Angle, + + max_elec_motor_speed: AngularVelocity, + elec_motor_over_trim_actuator_ratio: Ratio, + + min_ths_deflection: Angle, + ths_deflection_range: Angle, + ) -> Self { + Self { + pitch_trim_actuator: PitchTrimActuator::new( + context, + min_actuator_angle, + total_actuator_range_angle, + max_elec_motor_speed, + elec_motor_over_trim_actuator_ratio, + ), + trim_wheel: TrimWheels::new( + context, + total_actuator_range_angle / total_trim_wheel_range_angle, + min_trim_wheel_angle, + total_trim_wheel_range_angle, + ), + ths_hydraulics: TrimmableHorizontalStabilizerHydraulics::new( + context, + min_ths_deflection, + ths_deflection_range, + ), + } + } + + pub fn update( + &mut self, + context: &UpdateContext, + electric_controller: &impl PitchTrimActuatorController, + manual_controller: &impl ManualPitchTrimController, + pressures: [Pressure; 2], + ) { + self.pitch_trim_actuator.update( + context, + electric_controller, + manual_controller, + &self.ths_hydraulics, + ); + + self.trim_wheel.update(&self.pitch_trim_actuator); + + self.ths_hydraulics.update( + context, + pressures, + self.pitch_trim_actuator.position_normalized(), + ) + } + + pub fn position_normalized(&self) -> Ratio { + self.pitch_trim_actuator.position_normalized() + } + + pub fn left_motor(&mut self) -> &mut impl Actuator { + self.ths_hydraulics.left_motor() + } + + pub fn right_motor(&mut self) -> &mut impl Actuator { + self.ths_hydraulics.right_motor() + } +} +impl SimulationElement for TrimmableHorizontalStabilizerAssembly { + fn accept(&mut self, visitor: &mut T) { + self.pitch_trim_actuator.accept(visitor); + self.trim_wheel.accept(visitor); + self.ths_hydraulics.accept(visitor); + } +} + +struct TrimmableHorizontalStabilizerHydraulics { + deflection_id: VariableIdentifier, + hydraulic_motors: [HydraulicDriveMotor; 2], + + speed: AngularVelocity, + actual_deflection: Angle, + + min_deflection: Angle, + max_deflection: Angle, + deflection_range: Angle, + + is_at_max_up_spool_valve: bool, + is_at_max_down_spool_valve: bool, +} +impl TrimmableHorizontalStabilizerHydraulics { + const HYDRAULIC_MOTOR_POSITION_ERROR_BREAKPOINT: [f64; 7] = + [-50., -0.5, -0.1, 0., 0.1, 0.5, 50.]; + const HYDRAULIC_MOTOR_SPEED_REGULATION_COEF_MAP: [f64; 7] = [-1., -1., -0.6, 0., 0.6, 1., 1.]; + + // Gain to convert hyd motor speed to ths deflection speed + const HYD_MOTOR_SPEED_TO_THS_DEFLECTION_SPEED_GAIN: f64 = 0.000085; + + const MAX_DEFLECTION_FOR_FULL_OPEN_SPOOL_VALVE_DEGREES: f64 = 0.4; + + pub fn new(context: &mut InitContext, min_deflection: Angle, deflection_range: Angle) -> Self { + Self { + deflection_id: context.get_identifier("HYD_FINAL_THS_DEFLECTION".to_owned()), + hydraulic_motors: [HydraulicDriveMotor::new( + AngularVelocity::new::(2500.), + Self::HYDRAULIC_MOTOR_POSITION_ERROR_BREAKPOINT, + Self::HYDRAULIC_MOTOR_SPEED_REGULATION_COEF_MAP, + ); 2], + + speed: AngularVelocity::default(), + actual_deflection: Angle::default(), + min_deflection, + max_deflection: min_deflection + deflection_range, + deflection_range, + + is_at_max_up_spool_valve: false, + is_at_max_down_spool_valve: false, + } + } + + pub fn update( + &mut self, + context: &UpdateContext, + pressures: [Pressure; 2], + trim_actuator_output_position_normalized: Ratio, + ) { + let deflection_demand = self.deflection_range + * trim_actuator_output_position_normalized.get::() + + self.min_deflection; + + self.update_spool_valve_lock_position(deflection_demand); + + for (motor_index, motor) in self.hydraulic_motors.iter_mut().enumerate() { + motor.update( + context, + self.actual_deflection, + deflection_demand, + pressures[motor_index], + ) + } + + self.update_speed(); + + self.update_position(context); + } + + fn update_speed(&mut self) { + let mut sum_of_speeds = AngularVelocity::default(); + + for motor in self.hydraulic_motors { + sum_of_speeds += motor.speed() * Self::HYD_MOTOR_SPEED_TO_THS_DEFLECTION_SPEED_GAIN; + } + + self.speed = sum_of_speeds; + } + + fn update_spool_valve_lock_position(&mut self, deflection_demand: Angle) { + let deflection_error = deflection_demand - self.actual_deflection; + self.is_at_max_up_spool_valve = deflection_error.get::() + > Self::MAX_DEFLECTION_FOR_FULL_OPEN_SPOOL_VALVE_DEGREES; + self.is_at_max_down_spool_valve = deflection_error.get::() + < -Self::MAX_DEFLECTION_FOR_FULL_OPEN_SPOOL_VALVE_DEGREES; + } + + fn update_position(&mut self, context: &UpdateContext) { + self.actual_deflection += Angle::new::( + self.speed.get::() * context.delta_as_secs_f64(), + ); + + self.actual_deflection = self + .actual_deflection + .min(self.max_deflection) + .max(self.min_deflection); + } + + fn normalized_position_from_ths_deflection(&self, deflection: Angle) -> Ratio { + (deflection - self.min_deflection) / self.deflection_range + } + + pub fn left_motor(&mut self) -> &mut impl Actuator { + &mut self.hydraulic_motors[0] + } + + pub fn right_motor(&mut self) -> &mut impl Actuator { + &mut self.hydraulic_motors[1] + } +} +impl SimulationElement for TrimmableHorizontalStabilizerHydraulics { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write(&self.deflection_id, self.actual_deflection.get::()); + } +} + +#[cfg(test)] +mod tests { + use uom::si::angle::degree; + use uom::si::{angular_velocity::degree_per_second, electric_potential::volt, ratio::percent}; + + use crate::electrical::test::TestElectricitySource; + use crate::electrical::ElectricalBus; + use crate::electrical::Electricity; + + use super::*; + use crate::shared::{update_iterator::FixedStepLoop, PotentialOrigin}; + use crate::simulation::test::{ReadByName, SimulationTestBed, TestBed}; + use crate::simulation::{Aircraft, SimulationElement}; + use std::time::Duration; + + use rstest::rstest; + + struct TestElecTrimControl { + control_active: bool, + position_request: Angle, + motor_idx_in_control: usize, + } + impl TestElecTrimControl { + fn with_motor_idx_and_pos_demand( + motor_idx_in_control: usize, + position_request: Angle, + ) -> Self { + Self { + control_active: true, + position_request, + motor_idx_in_control, + } + } + + fn inactive_control() -> Self { + Self { + control_active: false, + position_request: Angle::default(), + motor_idx_in_control: 0, + } + } + } + impl PitchTrimActuatorController for TestElecTrimControl { + fn commanded_position(&self) -> Angle { + self.position_request + } + + fn energised_motor(&self) -> [bool; 3] { + if !self.control_active { + [false, false, false] + } else { + let mut energized_array = [false, false, false]; + energized_array[self.motor_idx_in_control] = true; + energized_array + } + } + } + + struct TestManualTrimControl { + control_active: bool, + speed: AngularVelocity, + } + impl TestManualTrimControl { + fn with_manual_input(speed: AngularVelocity) -> Self { + Self { + control_active: true, + speed, + } + } + + fn without_manual_input() -> Self { + Self { + control_active: false, + speed: AngularVelocity::default(), + } + } + } + impl ManualPitchTrimController for TestManualTrimControl { + fn is_manually_moved(&self) -> bool { + self.control_active + } + + fn moving_speed(&self) -> AngularVelocity { + self.speed + } + } + + struct TestAircraft { + updater_fixed_step: FixedStepLoop, + + elec_trim_control: TestElecTrimControl, + manual_trim_control: TestManualTrimControl, + + trim_assembly: TrimmableHorizontalStabilizerAssembly, + + hydraulic_pressures: [Pressure; 2], + + powered_source_dc: TestElectricitySource, + dc_2_bus: ElectricalBus, + dc_hot_bus: ElectricalBus, + dc_ess_bus: ElectricalBus, + is_elec_powered: bool, + } + impl TestAircraft { + fn new(context: &mut InitContext) -> Self { + Self { + updater_fixed_step: FixedStepLoop::new(Duration::from_millis(33)), + elec_trim_control: TestElecTrimControl::inactive_control(), + manual_trim_control: TestManualTrimControl::without_manual_input(), + trim_assembly: TrimmableHorizontalStabilizerAssembly::new( + context, + Angle::new::(360. * -1.4), + Angle::new::(360. * 6.13), + Angle::new::(360. * -1.87), + Angle::new::(360. * 8.19), // 1.87 rotations down 6.32 up + AngularVelocity::new::(5000.), + Ratio::new::(2035. / 6.13), + Angle::new::(-4.), + Angle::new::(17.5), + ), + + hydraulic_pressures: [Pressure::new::(3000.); 2], + + powered_source_dc: TestElectricitySource::powered( + context, + PotentialOrigin::Battery(2), + ), + + dc_2_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrent(2)), + dc_hot_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrentHot(2)), + dc_ess_bus: ElectricalBus::new(context, ElectricalBusType::DirectCurrentEssential), + is_elec_powered: true, + } + } + + fn set_elec_trim_demand(&mut self, angle_request: Angle, motor_idx: usize) { + self.elec_trim_control = + TestElecTrimControl::with_motor_idx_and_pos_demand(motor_idx, angle_request); + } + + fn set_manual_trim_input(&mut self, trim_up: bool) { + self.manual_trim_control = if trim_up { + TestManualTrimControl::with_manual_input(AngularVelocity::new::( + 500., + )) + } else { + TestManualTrimControl::with_manual_input(AngularVelocity::new::( + -500., + )) + } + } + + fn set_no_manual_input(&mut self) { + self.manual_trim_control = TestManualTrimControl::without_manual_input(); + } + + fn set_no_elec_input(&mut self) { + self.elec_trim_control = TestElecTrimControl::inactive_control(); + } + + fn set_hyd_pressure(&mut self, pressures: [Pressure; 2]) { + self.hydraulic_pressures = pressures; + } + + fn set_no_elec_power(&mut self) { + self.is_elec_powered = false; + } + } + impl Aircraft for TestAircraft { + fn update_before_power_distribution( + &mut self, + _: &UpdateContext, + electricity: &mut Electricity, + ) { + self.powered_source_dc + .power_with_potential(ElectricPotential::new::(24.)); + electricity.supplied_by(&self.powered_source_dc); + + if self.is_elec_powered { + electricity.flow(&self.powered_source_dc, &self.dc_2_bus); + electricity.flow(&self.powered_source_dc, &self.dc_ess_bus); + electricity.flow(&self.powered_source_dc, &self.dc_hot_bus); + } + } + + fn update_after_power_distribution(&mut self, context: &UpdateContext) { + self.updater_fixed_step.update(context); + + for cur_time_step in &mut self.updater_fixed_step { + self.trim_assembly.update( + &context.with_delta(cur_time_step), + &self.elec_trim_control, + &self.manual_trim_control, + self.hydraulic_pressures, + ); + + println!( + "TW turns: {:.3} ,PTA turns {:.3}, PTA norm {:.2} EMotors rpm {:.0}/{:.0}/{:.0} HydMotors rpm {:.0} {:.0} THS Deg {:.1}", + self.trim_assembly.trim_wheel.position.get::() / 360., + self.trim_assembly + .pitch_trim_actuator + .position + .get::() + / 360., + self.trim_assembly.position_normalized().get::(), + self.trim_assembly.pitch_trim_actuator.electric_motors[0] + .speed() + .get::(), + self.trim_assembly.pitch_trim_actuator.electric_motors[1] + .speed() + .get::(), + self.trim_assembly.pitch_trim_actuator.electric_motors[2] + .speed() + .get::(), + self.trim_assembly.ths_hydraulics.hydraulic_motors[0].speed().get::(), + self.trim_assembly.ths_hydraulics.hydraulic_motors[1].speed().get::(), + self.trim_assembly.ths_hydraulics.actual_deflection.get::() + + ); + } + } + } + impl SimulationElement for TestAircraft { + fn accept(&mut self, visitor: &mut V) { + self.trim_assembly.accept(visitor); + + visitor.visit(self); + } + } + + #[test] + fn trim_assembly_init_state() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.run_with_delta(Duration::from_millis(100)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::().abs() < 0.01); + } + + #[rstest] + #[case(0)] + #[case(1)] + #[case(2)] + fn trim_assembly_trim_up_trim_down_motor_n(#[case] motor_idx: usize) { + let mut test_bed = SimulationTestBed::new(|context| TestAircraft::new(context)); + + test_bed.command(|a| a.set_elec_trim_demand(Angle::new::(13.), motor_idx)); + test_bed.command(|a| a.set_no_manual_input()); + + test_bed.run_with_delta(Duration::from_millis(20000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() > 12.9); + assert!(deflection.get::() < 13.1); + + let man_override: f64 = test_bed.read_by_name("HYD_THS_TRIM_MANUAL_OVERRIDE"); + assert!(man_override <= 0.5); + + test_bed.command(|a| a.set_elec_trim_demand(Angle::new::(-2.), motor_idx)); + test_bed.run_with_delta(Duration::from_millis(25000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() >= -2.1); + assert!(deflection.get::() < -1.9); + } + + #[test] + fn trim_assembly_moves_but_ths_stops_with_hyd_press_below_1450psi() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| a.set_elec_trim_demand(Angle::new::(13.), 0)); + test_bed.run_with_delta(Duration::from_millis(5000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() > 2.); + + println!("PRESSURE DROP"); + test_bed.command(|a| { + a.set_hyd_pressure([Pressure::new::(1300.), Pressure::new::(1300.)]) + }); + test_bed.run_with_delta(Duration::from_millis(5000)); + + let deflection_after_hyd_fail: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!((deflection - deflection_after_hyd_fail).abs() < Angle::new::(1.)); + } + + #[test] + fn trim_assembly_max_motor_0() { + let mut test_bed = SimulationTestBed::new(|context| TestAircraft::new(context)); + + test_bed.command(|a| a.set_elec_trim_demand(Angle::new::(13.5), 0)); + test_bed.run_with_delta(Duration::from_millis(20000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() > 13.45); + assert!(deflection.get::() < 13.55); + + let trim_wheel_position_percent: Ratio = test_bed.read_by_name("HYD_TRIM_WHEEL_PERCENT"); + assert!(trim_wheel_position_percent.get::() > 99.9); + assert!(trim_wheel_position_percent.get::() < 100.1); + } + + #[test] + fn trim_assembly_motor_0_without_elec_is_stuck() { + let mut test_bed = SimulationTestBed::new(|context| TestAircraft::new(context)); + + test_bed.command(|a| a.set_elec_trim_demand(Angle::new::(13.5), 0)); + test_bed.command(|a| a.set_no_elec_power()); + test_bed.run_with_delta(Duration::from_millis(20000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() >= -0.1); + assert!(deflection.get::() <= 0.1); + } + + #[test] + fn trim_assembly_min_motor_0() { + let mut test_bed = SimulationTestBed::new(|context| TestAircraft::new(context)); + + test_bed.command(|a| a.set_elec_trim_demand(Angle::new::(-4.), 0)); + test_bed.run_with_delta(Duration::from_millis(20000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() > -4.1); + assert!(deflection.get::() < -3.9); + + let trim_wheel_position_percent: Ratio = test_bed.read_by_name("HYD_TRIM_WHEEL_PERCENT"); + assert!(trim_wheel_position_percent.get::() > -0.1); + assert!(trim_wheel_position_percent.get::() < 0.1); + } + + #[test] + fn trim_wheel_moves_up_with_hyd_press_if_moved_manually() { + let mut test_bed = SimulationTestBed::new(TestAircraft::new); + + test_bed.command(|a| { + a.set_hyd_pressure([Pressure::new::(3000.), Pressure::new::(3000.)]) + }); + + test_bed.command(|a| a.set_no_elec_input()); + test_bed.command(|a| a.set_manual_trim_input(true)); + test_bed.run_with_delta(Duration::from_millis(5000)); + + let deflection: Angle = test_bed.read_by_name("HYD_FINAL_THS_DEFLECTION"); + assert!(deflection.get::() > 5.); + } +} diff --git a/src/systems/systems/src/landing_gear/mod.rs b/src/systems/systems/src/landing_gear/mod.rs index 7b42b38a4c5..492bbc621e7 100644 --- a/src/systems/systems/src/landing_gear/mod.rs +++ b/src/systems/systems/src/landing_gear/mod.rs @@ -1026,7 +1026,13 @@ impl GearSystemStateMachine { if !gear_handle_position_is_up { GearSystemState::Extending } else { - self.gears_state + // Checking consistency as we should be all uplocked with lever up + if lgciu.all_up_and_locked() && lgciu.all_closed_and_locked() { + self.gears_state + } else { + // Else we are supposed to be all uplocked but we aren't so back to retraction + GearSystemState::Retracting + } } } GearSystemState::Retracting => { diff --git a/src/systems/systems_wasm/Cargo.toml b/src/systems/systems_wasm/Cargo.toml index e6feaf0d107..1b31f124dc5 100644 --- a/src/systems/systems_wasm/Cargo.toml +++ b/src/systems/systems_wasm/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" test = false [dependencies] -uom = "0.30.0" +uom = "0.33.0" systems = { path = "../systems" } msfs = { git = "https://github.com/flybywiresim/msfs-rs", branch = "main" } fxhash = "0.2.1" diff --git a/typings/fs-base-ui/html_ui/JS/Avionics.d.ts b/typings/fs-base-ui/html_ui/JS/Avionics.d.ts index 7f5eeab4286..2ed645ada88 100644 --- a/typings/fs-base-ui/html_ui/JS/Avionics.d.ts +++ b/typings/fs-base-ui/html_ui/JS/Avionics.d.ts @@ -305,6 +305,8 @@ declare global { protected updateAlwaysList(): void; protected clearAlwaysList(): void; registerInstrument(_instrumentName: string, _instrumentClass: CustomElementConstructor): void; + guidanceController?: any; + navigation?: any; } declare function registerInstrument(_instrumentName: string, _instrumentClass: CustomElementConstructor): void; diff --git a/typings/fs-base-ui/html_ui/JS/simvar.d.ts b/typings/fs-base-ui/html_ui/JS/simvar.d.ts index ae2a6c84a99..85c5bf5b0a8 100644 --- a/typings/fs-base-ui/html_ui/JS/simvar.d.ts +++ b/typings/fs-base-ui/html_ui/JS/simvar.d.ts @@ -4,7 +4,7 @@ declare namespace SimVar { var g_bUseWatcher: boolean; - type SimVarUnit = "" | "amp" | "ampere" | "amperes" | "amps" | "angl16" | "angl32" | "atm" | "atmosphere" | "atmospheres" | "bar" | "bars" | "Bco16" | "bel" | "bels" | "Bool" | "Boolean" | "boost cmHg" | "boost inHg" | "boost psi" | "celsius fs7 egt" | "celsius fs7 oil temp" | "celsius scaler 1/256" | "celsius scaler 16k" | "celsius scaler 256" | "celsius" | "centimeter of mercury" | "centimeter" | "centimeters of mercury" | "centimeters" | "cm" | "cm2" | "cm3" | "cmHg" | "cu cm" | "cu ft" | "cu in" | "cu km" | "cu m" | "cu mm" | "cu yd" | "cubic centimeter" | "cubic centimeters" | "cubic feet" | "cubic foot" | "cubic inch" | "cubic inches" | "cubic kilometer" | "cubic kilometers" | "cubic meter" | "cubic meters" | "cubic mile" | "cubic miles" | "cubic millimeter" | "cubic millimeters" | "cubic yard" | "cubic yards" | "day" | "days" | "decibel" | "decibels" | "decimile" | "decimiles" | "decinmile" | "decinmiles" | "degree angl16" | "degree angl32" | "degree latitude" | "degree longitude" | "degree per second ang16" | "degree per second" | "degree" | "degrees angl16" | "degrees angl32" | "degrees latitude" | "degrees longitude" | "degrees per second ang16" | "degrees per second" | "Degrees" | "degrees" | "Enum" | "fahrenheit" | "farenheit" | "feet per minute" | "feet per second squared" | "feet per second" | "feet" | "feet/minute" | "feet/second" | "flags" | "foot per second squared" | "foot pound" | "foot pounds" | "foot" | "foot-pound" | "foot-pounds" | "FractionalLatLonDigits" | "Frequency ADF BCD32" | "Frequency BCD16" | "Frequency BCD32" | "fs7 charging amps" | "fs7 oil quantity" | "ft lb per second" | "ft" | "ft-lbs" | "ft/min" | "ft2" | "ft3" | "G Force 624 scaled" | "G Force" | "gallon per hour" | "gallon" | "gallons per hour" | "gallons" | "geepound" | "geepounds" | "GForce" | "GLOBALP->delta_heading_rate" | "GLOBALP->eng1.manifold_pressure" | "GLOBALP->eng1.oil_prs" | "GLOBALP->eng1.oil_tmp" | "GLOBALP->vertical_speed" | "gph" | "grad" | "grads" | "half" | "halfs" | "hectopascal" | "hectopascals" | "Hertz" | "hour over 10" | "hour" | "hours over 10" | "hours" | "Hz" | "in" | "in2" | "in3" | "inch of mercury" | "inch" | "inches of mercury" | "inches" | "inHg 64 over 64k" | "inHg" | "kelvin" | "keyframe" | "keyframes" | "kg" | "kgf meter" | "kgf meters" | "KgFSqCm" | "KHz" | "kilogram force per square centimeter" | "kilogram meter squared" | "kilogram meter" | "kilogram meters" | "kilogram per cubic meter" | "kilogram per second" | "kilogram" | "kilograms meter squared" | "kilograms per cubic meter" | "kilograms per second" | "kilograms" | "Kilohertz" | "kilometer per hour" | "kilometer" | "kilometer/hour" | "kilometers per hour" | "kilometers" | "kilometers/hour" | "kilopascal" | "km" | "km2" | "km3" | "knot scaler 128" | "knot" | "knots scaler 128" | "knots" | "kPa" | "kph" | "LatLonFormat" | "lbf-feet" | "lbs" | "liter per hour" | "liter" | "liters per hour" | "liters" | "m/s" | "m2" | "m3" | "mach 3d2 over 64k" | "mach" | "machs" | "mask" | "mbar" | "mbars" | "Megahertz" | "meter cubed per second" | "meter cubed" | "meter latitude" | "meter per minute" | "meter per second scaler 256" | "meter per second squared" | "meter per second" | "meter scaler 256" | "meter" | "meter/second" | "meters cubed per second" | "meters cubed" | "meters latitude" | "meters per minute" | "meters per second scaler 256" | "meters per second squared" | "meters per second" | "meters scaler 256" | "meters" | "meters/second" | "MHz" | "mile per hour" | "mile" | "miles per hour" | "miles" | "millibar scaler 16" | "millibar" | "millibars scaler 16" | "millibars" | "millimeter of mercury" | "millimeter of water" | "millimeter" | "millimeters of mercury" | "millimeters of water" | "millimeters" | "minute per round" | "minute" | "minutes per round" | "minutes" | "mm2" | "mm3" | "mmHg" | "more_than_a_half" | "mph" | "nautical mile" | "nautical miles" | "newton meter" | "newton meters" | "newton per square meter" | "newtons per square meter" | "nice minute per round" | "nice minutes per round" | "Nm" | "nmile" | "nmiles" | "number" | "number_number" | "numbers" | "Pa" | "part" | "pascal" | "pascals" | "per degree" | "per hour" | "per minute" | "per radian" | "per second" | "percent over 100" | "percent scaler 16k" | "percent scaler 2pow23" | "percent scaler 32k" | "percent" | "percentage" | "position 128" | "position 16k" | "position 32k" | "position" | "pound per hour" | "pound scaler 256" | "pound" | "pound-force per square foot" | "pound-force per square inch" | "poundal feet" | "pounds per hour" | "pounds scaler 256" | "pounds" | "pph" | "psf scaler 16k" | "psf" | "psi 4 over 16k" | "psi fs7 oil pressure" | "psi scaler 16k" | "psi" | "quart" | "quarts" | "radian per second" | "radian" | "radians per second" | "radians" | "rankine" | "ratio" | "revolution per minute" | "revolutions per minute" | "round" | "rounds" | "rpm 1 over 16k" | "rpm" | "rpms" | "scaler" | "second" | "Seconds" | "seconds" | "slug feet squared" | "Slug per cubic feet" | "Slug per cubic foot" | "slug" | "Slug/ft3" | "slugs feet squared" | "Slugs per cubic feet" | "Slugs per cubic foot" | "slugs" | "sq cm" | "sq ft" | "sq in" | "sq km" | "sq m" | "sq mm" | "sq yd" | "square centimeter" | "square centimeters" | "square feet" | "square foot" | "square inch" | "square inches" | "square kilometer" | "square kilometers" | "square meter" | "square meters" | "square mile" | "square miles" | "square millimeter" | "square millimeters" | "square yard" | "square yards" | "third" | "thirds" | "times" | "volt" | "volts" | "Watt" | "Watts" | "yard" | "yards" | "yd2" | "yd3" | "year" | "years" | "string" | "latlonaltpbh" | "latlonalt" | "Millibars" | "PBH" | "XYZ" | "PID_STRUCT" | "POIList" | "GlassCockpitSettings" | "FuelLevels" | string; + type SimVarUnit = "" | "amp" | "ampere" | "amperes" | "amps" | "angl16" | "angl32" | "atm" | "atmosphere" | "atmospheres" | "bar" | "bars" | "Bco16" | "bel" | "bels" | "Bool" | "Boolean" | "boost cmHg" | "boost inHg" | "boost psi" | "celsius fs7 egt" | "celsius fs7 oil temp" | "celsius scaler 1/256" | "celsius scaler 16k" | "celsius scaler 256" | "celsius" | "centimeter of mercury" | "centimeter" | "centimeters of mercury" | "centimeters" | "cm" | "cm2" | "cm3" | "cmHg" | "cu cm" | "cu ft" | "cu in" | "cu km" | "cu m" | "cu mm" | "cu yd" | "cubic centimeter" | "cubic centimeters" | "cubic feet" | "cubic foot" | "cubic inch" | "cubic inches" | "cubic kilometer" | "cubic kilometers" | "cubic meter" | "cubic meters" | "cubic mile" | "cubic miles" | "cubic millimeter" | "cubic millimeters" | "cubic yard" | "cubic yards" | "day" | "days" | "decibel" | "decibels" | "decimile" | "decimiles" | "decinmile" | "decinmiles" | "degree angl16" | "degree angl32" | "degree latitude" | "degree longitude" | "degree per second ang16" | "degree per second" | "degree" | "degrees angl16" | "degrees angl32" | "degrees latitude" | "degrees longitude" | "degrees per second ang16" | "degrees per second" | "Degrees" | "degrees" | "Enum" | "fahrenheit" | "farenheit" | "feet per minute" | "feet per second squared" | "feet per second" | "feet" | "feet/minute" | "feet/second" | "flags" | "foot per second squared" | "foot pound" | "foot pounds" | "foot" | "foot-pound" | "foot-pounds" | "FractionalLatLonDigits" | "Frequency ADF BCD32" | "Frequency BCD16" | "Frequency BCD32" | "fs7 charging amps" | "fs7 oil quantity" | "ft lb per second" | "ft" | "ft-lbs" | "ft/min" | "ft2" | "ft3" | "G Force 624 scaled" | "G Force" | "gallon per hour" | "gallon" | "gallons per hour" | "gallons" | "geepound" | "geepounds" | "GForce" | "GLOBALP->delta_heading_rate" | "GLOBALP->eng1.manifold_pressure" | "GLOBALP->eng1.oil_prs" | "GLOBALP->eng1.oil_tmp" | "GLOBALP->vertical_speed" | "gph" | "grad" | "grads" | "half" | "halfs" | "hectopascal" | "hectopascals" | "Hertz" | "hour over 10" | "hour" | "hours over 10" | "hours" | "Hz" | "in" | "in2" | "in3" | "inch of mercury" | "inch" | "inches of mercury" | "inches" | "inHg 64 over 64k" | "inHg" | "kelvin" | "keyframe" | "keyframes" | "kg" | "kgf meter" | "kgf meters" | "KgFSqCm" | "KHz" | "kilogram force per square centimeter" | "kilogram meter squared" | "kilogram meter" | "kilogram meters" | "kilogram per cubic meter" | "kilogram per second" | "kilogram" | "kilograms meter squared" | "kilograms per cubic meter" | "kilograms per second" | "kilograms" | "Kilohertz" | "kilometer per hour" | "kilometer" | "kilometer/hour" | "kilometers per hour" | "kilometers" | "kilometers/hour" | "kilopascal" | "km" | "km2" | "km3" | "knot scaler 128" | "knot" | "knots scaler 128" | "knots" | "kPa" | "kph" | "LatLonFormat" | "lbf-feet" | "lbs" | "liter per hour" | "liter" | "liters per hour" | "liters" | "m/s" | "m2" | "m3" | "mach 3d2 over 64k" | "mach" | "machs" | "mask" | "mbar" | "mbars" | "Megahertz" | "meter cubed per second" | "meter cubed" | "meter latitude" | "meter per minute" | "meter per second scaler 256" | "meter per second squared" | "meter per second" | "meter scaler 256" | "meter" | "meter/second" | "meters cubed per second" | "meters cubed" | "meters latitude" | "meters per minute" | "meters per second scaler 256" | "meters per second squared" | "meters per second" | "meters scaler 256" | "meters" | "meters/second" | "MHz" | "mile per hour" | "mile" | "miles per hour" | "miles" | "millibar scaler 16" | "millibar" | "millibars scaler 16" | "millibars" | "millimeter of mercury" | "millimeter of water" | "millimeter" | "millimeters of mercury" | "millimeters of water" | "millimeters" | "minute per round" | "minute" | "minutes per round" | "minutes" | "mm2" | "mm3" | "mmHg" | "more_than_a_half" | "mph" | "nautical mile" | "nautical miles" | "newton meter" | "newton meters" | "newton per square meter" | "newtons per square meter" | "nice minute per round" | "nice minutes per round" | "Nm" | "nmile" | "nmiles" | "number" | "number_number" | "numbers" | "Pa" | "part" | "pascal" | "pascals" | "per degree" | "per hour" | "per minute" | "per radian" | "per second" | "percent over 100" | "percent scaler 16k" | "percent scaler 2pow23" | "percent scaler 32k" | "percent" | "percentage" | "position 128" | "position 16k" | "position 32k" | "position" | "pound per hour" | "pound scaler 256" | "pound" | "pound-force per square foot" | "pound-force per square inch" | "poundal feet" | "pounds per hour" | "pounds scaler 256" | "pounds" | "pph" | "psf scaler 16k" | "psf" | "psi 4 over 16k" | "psi fs7 oil pressure" | "psi scaler 16k" | "psi" | "quart" | "quarts" | "radian per second" | "radian" | "radians per second" | "radians" | "rankine" | "ratio" | "revolution per minute" | "revolutions per minute" | "round" | "rounds" | "rpm 1 over 16k" | "rpm" | "rpms" | "scaler" | "second" | "Seconds" | "seconds" | "slug feet squared" | "Slug per cubic feet" | "Slug per cubic foot" | "slug" | "Slug/ft3" | "slugs feet squared" | "Slugs per cubic feet" | "Slugs per cubic foot" | "slugs" | "sq cm" | "sq ft" | "sq in" | "sq km" | "sq m" | "sq mm" | "sq yd" | "square centimeter" | "square centimeters" | "square feet" | "square foot" | "square inch" | "square inches" | "square kilometer" | "square kilometers" | "square meter" | "square meters" | "square mile" | "square miles" | "square millimeter" | "square millimeters" | "square yard" | "square yards" | "third" | "thirds" | "times" | "volt" | "volts" | "Watt" | "Watts" | "yard" | "yards" | "yd2" | "yd3" | "year" | "years" | "string" | "latlonaltpbh" | "latlonalt" | "Millibars" | "PBH" | "XYZ" | "PID_STRUCT" | "POIList" | "GlassCockpitSettings" | "FuelLevels" | (string & {}); type SimVarType = number | LatLongAlt | LatLongAltPBH | string | PID_STRUCT | XYZ | PitchBankHeading | boolean; class SimVarValue { __type: string;