diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/Cockpit.loc b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/Cockpit.loc index 59e54b8c911..f228346b258 100644 --- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/Cockpit.loc +++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/Cockpit.loc @@ -64,7 +64,7 @@ }, "Languages": { "en-US": { - "Text": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "Text": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", "LocalizationStatus": "TranslationNeeded" }, "fr-FR": { diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Cockpit.loc b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Cockpit.loc new file mode 100644 index 00000000000..59e54b8c911 --- /dev/null +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/Cockpit.loc @@ -0,0 +1,99 @@ +{ + "LocalisationFile": { + "Version": 2, + "UUID": "8fd165b7-840b-481e-95d8-6d31a70f63d6", + "Languages": [ + "en-US", + "fr-FR", + "es-ES", + "de-DE", + "it-IT", + "pl-PL", + "pt-BR", + "ru-RU", + "zh-CN" + ], + "Strings": { + "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": { + "UUID": "c3803db3-2bbd-4a83-abef-74b926de0852", + "LastModifiedBy": "ajimeno", + "LastModifiedDate": "2020-04-26 09:25:28", + "LocalizationStatus": "TranslationNeeded", + "Tags": { + "Locked": "False" + }, + "Languages": { + "en-US": { + "Text": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "LocalizationStatus": "TranslationNeeded" + }, + "fr-FR": { + "Text": "Adapter le cap souhaité sur la gauche (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "es-ES": { + "Text": "Ajustar el rumbo seleccionado a la izquierda (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "de-DE": { + "Text": "Ausgewählten Steuerkurs nach links anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!)" + }, + "it-IT": { + "Text": "Regola a sinistra rotta selezionata (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "pl-PL": { + "Text": "Zmień wybrany kurs w lewo o (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "pt-BR": { + "Text": "Ajustar a proa selecionada para a esquerda (%((A:AUTOPILOT HEADING LOCK DIR,degrees))%!d!°)" + }, + "ru-RU": { + "Text": "Скорректировать заданный курс влево (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "zh-CN": { + "Text": "向左调整所选航向(%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "LocalizationStatus": "TranslationNeeded" + } + } + }, + "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": { + "UUID": "8464ffeb-508a-4318-95d0-53a50e29b00d", + "LastModifiedBy": "ajimeno", + "LastModifiedDate": "2020-04-26 09:25:46", + "LocalizationStatus": "TranslationNeeded", + "Tags": { + "Locked": "False" + }, + "Languages": { + "en-US": { + "Text": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "LocalizationStatus": "TranslationNeeded" + }, + "fr-FR": { + "Text": "Adapter le cap souhaité sur la droite (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "es-ES": { + "Text": "Ajustar el rumbo seleccionado a la derecha (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "de-DE": { + "Text": "Ausgewählten Steuerkurs nach rechts anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!)" + }, + "it-IT": { + "Text": "Regola a destra rotta selezionata (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "pl-PL": { + "Text": "Zmień wybrany kurs w prawo o (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "pt-BR": { + "Text": "Ajustar a proa selecionada para a direita (%((A:AUTOPILOT HEADING LOCK DIR,degrees))%!d!°)" + }, + "ru-RU": { + "Text": "Скорректировать заданный курс вправо (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + }, + "zh-CN": { + "Text": "向右调整所选航向(%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "LocalizationStatus": "TranslationNeeded" + } + } + } + } + } +} diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/de-DE.locPak b/fbw-a380x/src/base/flybywire-aircraft-a380-842/de-DE.locPak new file mode 100644 index 00000000000..ed199df4eb1 --- /dev/null +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/de-DE.locPak @@ -0,0 +1,20 @@ +{ + "LocalisationPackage": { + "Language": "de-DE", + "Strings": { + "AIRCRAFT.DESCRIPTION": "Die erste A320 von Airbus wurde 1984 der Weltöffentlichkeit präsentiert. 26 Jahre später, 2010, verkündete das Unternehmen, dass die Familie ein neues Mitglied in ihrer Mitte begrüßen kann, die A320neo (neo: neue Triebwerksoption), die dazu beitragen soll, die Unternehmenslinie der weltweit führenden Schmalrumpfflugzeuge mit Doppel-Mantelstromtriebwerk zu revitalisieren und in die Zukunft zu führen.<\/br><\/br>Nach einem ausgedehnten Entwicklungsprozess wurde die A320neo von der Lufthansa 2016 in Betrieb genommen und entwickelte sich rasch zum am meisten verkauften Flugzeug aller Zeiten, wobei bis zum heutigen Tag 115 verschiedene Airlines insgesamt 7.000 Flugzeuge orderten.<\/br><\/br>Ganz im Sinne der Tradition, die mit der ursprünglichen A320 begründet wurde – diese war die erste Passagiermaschine, mit Ausnahme der Concorde, die für normale Operationen auf die Fly-by-Wire-Steuerung setzte – teilt die A320neo eine mehr als 95 %ige Gemeinsamkeit der Flugzeugzelle mit ihren Vorgängern. Allerdings versteht es die A320neo ebenfalls, sich entsprechend abzugrenzen: und zwar durch die Einführung zweier Triebwerke der neuesten Generation, die mit den unternehmenseigenen „Sharklet“-Flügelspitzen kombiniert werden.<\/br><\/br>Zu den Schlüsselverbesserungen der neo zählen unter anderem Treibstoffersparnis, eine größere Reichweite und erhöhte max. Ladung sowie eine verkürzte Landedistanz als auch eine dramatische Reduzierung der Lautstärke und der Kohlenstoffdioxid-Emissionen. All dies bezeugt, dass die A320 nicht nur ein kommerzieller Erfolg, sondern auch ein wichtiger Schritt hin zu einem Kurs der Luftfahrt ist, bei dem es um eine größere Umweltverantwortlichkeit geht.", + "AIRCRAFT.UI_MANUFACTURER": "Airbus", + "AIRCRAFT.UI_MODEL": "A320neo", + "AIRCRAFT.UI_VARIATION": "Airbus", + + "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Ausgewählten Steuerkurs nach links anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!)", + "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Ausgewählten Steuerkurs nach rechts anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!)", + + "A380X.TOOLTIPS.STATE_ESS": "ESS", + "A380X.TOOLTIPS.STATE_APU": "APU", + "A380X.TOOLTIPS.BAT_SELECTOR_TITLE": "Wahlschalter für Batteriespannungsanzeige", + "A380X.TOOLTIPS.BAT_SELECTOR_ACTION": "Wählen Sie die Batterie, deren Spannung angezeigt werden soll" + + } + } +} diff --git a/fbw-a380x/src/localization/msfs/de-DE.locPak b/fbw-a380x/src/localization/msfs/de-DE.locPak index 40bb689e0df..2b4b90f85cb 100644 --- a/fbw-a380x/src/localization/msfs/de-DE.locPak +++ b/fbw-a380x/src/localization/msfs/de-DE.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Ausgewählten Steuerkurs nach links anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Ausgewählten Steuerkurs nach rechts anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Ausgewählten Steuerkurs nach links anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Ausgewählten Steuerkurs nach rechts anpassen (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/es-ES.locPak b/fbw-a380x/src/localization/msfs/es-ES.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/es-ES.locPak +++ b/fbw-a380x/src/localization/msfs/es-ES.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/es-MX.locPak b/fbw-a380x/src/localization/msfs/es-MX.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/es-MX.locPak +++ b/fbw-a380x/src/localization/msfs/es-MX.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/fi-FI.locPak b/fbw-a380x/src/localization/msfs/fi-FI.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/fi-FI.locPak +++ b/fbw-a380x/src/localization/msfs/fi-FI.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/fr-FR.locPak b/fbw-a380x/src/localization/msfs/fr-FR.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/fr-FR.locPak +++ b/fbw-a380x/src/localization/msfs/fr-FR.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/it-IT.locPak b/fbw-a380x/src/localization/msfs/it-IT.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/it-IT.locPak +++ b/fbw-a380x/src/localization/msfs/it-IT.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/ja-JP.locPak b/fbw-a380x/src/localization/msfs/ja-JP.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/ja-JP.locPak +++ b/fbw-a380x/src/localization/msfs/ja-JP.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/nb-NO.locPak b/fbw-a380x/src/localization/msfs/nb-NO.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/nb-NO.locPak +++ b/fbw-a380x/src/localization/msfs/nb-NO.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/nl-NL.locPak b/fbw-a380x/src/localization/msfs/nl-NL.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/nl-NL.locPak +++ b/fbw-a380x/src/localization/msfs/nl-NL.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/pl-PL.locPak b/fbw-a380x/src/localization/msfs/pl-PL.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/pl-PL.locPak +++ b/fbw-a380x/src/localization/msfs/pl-PL.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/pt-BR.locPak b/fbw-a380x/src/localization/msfs/pt-BR.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/pt-BR.locPak +++ b/fbw-a380x/src/localization/msfs/pt-BR.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/pt-PT.locPak b/fbw-a380x/src/localization/msfs/pt-PT.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/pt-PT.locPak +++ b/fbw-a380x/src/localization/msfs/pt-PT.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/ru-RU.locPak b/fbw-a380x/src/localization/msfs/ru-RU.locPak index 58a88fcc4e5..70dc50994fd 100644 --- a/fbw-a380x/src/localization/msfs/ru-RU.locPak +++ b/fbw-a380x/src/localization/msfs/ru-RU.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/sv-SE.locPak b/fbw-a380x/src/localization/msfs/sv-SE.locPak index 58a88fcc4e5..b462d4b0079 100644 --- a/fbw-a380x/src/localization/msfs/sv-SE.locPak +++ b/fbw-a380x/src/localization/msfs/sv-SE.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380X.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380X.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/localization/msfs/zh-CN.locPak b/fbw-a380x/src/localization/msfs/zh-CN.locPak index 58a88fcc4e5..b462d4b0079 100644 --- a/fbw-a380x/src/localization/msfs/zh-CN.locPak +++ b/fbw-a380x/src/localization/msfs/zh-CN.locPak @@ -15,8 +15,8 @@ "AIRCRAFT.UI_MANUFACTURER": "Airbus", "AIRCRAFT.UI_MODEL": "A380", "AIRCRAFT.UI_VARIATION": "FlyByWire A380X (A380-842)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", - "COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" + "A380X.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_LEFT": "Adjust selected heading to the left (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)", + "A380X.COCKPIT.TOOLTIPS.AUTOPILOT_PANEL_HDG_TUNING_RIGHT": "Adjust selected heading to the right (%((A:AUTOPILOT HEADING LOCK DIR:1,degrees))%!d!°)" } } -} \ No newline at end of file +} diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/FmsOansPublisher.ts b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/FmsOansPublisher.ts index 398607c5cbe..475df9bd5e2 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/FmsOansPublisher.ts +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/providers/FmsOansPublisher.ts @@ -50,8 +50,8 @@ export enum FmsOansSimVars { oansRemainingDistToRwyEndRaw = 'L:A32NX_OANS_BTV_REMAINING_DIST_TO_RWY_END', oansRemainingDistToExitRaw = 'L:A32NX_OANS_BTV_REMAINING_DIST_TO_EXIT', btvRotRaw = 'L:A32NX_BTV_ROT', - btvTurnAroundIdleReverseRaw = 'L:A32NX_BTV_TURNAROUND_IDLE_REV', - btvTurnAroundMaxReverseRaw = 'L:A32NX_BTV_TURNAROUND_MAX_REV', + btvTurnAroundIdleReverseRaw = 'L:A32NX_BTV_TURNAROUND_IDLE_REVERSE', + btvTurnAroundMaxReverseRaw = 'L:A32NX_BTV_TURNAROUND_MAX_REVERSE', } /** A publisher to poll and publish nav/com simvars. */ diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/autobrakes.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/autobrakes.rs index 435bcf9f470..4b7710be0cb 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/autobrakes.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/autobrakes.rs @@ -2,8 +2,11 @@ use systems::{ hydraulic::brake_circuit::AutobrakeDecelerationGovernor, overhead::PressSingleSignalButton, shared::{ - interpolation, DelayedPulseTrueLogicGate, DelayedTrueLogicGate, ElectricalBusType, - ElectricalBuses, LgciuInterface, + arinc429::{Arinc429Word, SignStatus}, + interpolation, + low_pass_filter::LowPassFilter, + Clamp, DelayedPulseTrueLogicGate, DelayedTrueLogicGate, ElectricalBusType, ElectricalBuses, + LgciuInterface, }, simulation::{ InitContext, Read, Reader, SimulationElement, SimulationElementVisitor, SimulatorReader, @@ -15,7 +18,9 @@ use std::time::Duration; use uom::si::{ acceleration::meter_per_second_squared, f64::*, + length::{foot, meter}, ratio::{percent, ratio}, + velocity::{knot, meter_per_second}, }; #[derive(PartialEq, Clone, Copy, Debug)] @@ -92,6 +97,10 @@ impl A380AutobrakePanel { self.selected_mode } + pub fn is_disarmed_position(&self) -> bool { + self.selected_mode == A380AutobrakeKnobPosition::DISARM + } + pub fn selected_mode_has_changed(&self) -> bool { self.mode_has_changed } @@ -180,10 +189,16 @@ pub struct A380AutobrakeController { should_reject_rto_mode_after_time_in_flight: DelayedTrueLogicGate, autobrake_knob: A380AutobrakeKnobSelectorSolenoid, + selection_knob_should_return_disarm: DelayedTrueLogicGate, external_disarm_event: bool, placeholder_ground_spoilers_out: bool, + + btv_scheduler: BtvDecelScheduler, + + braking_distance_calculator: BrakingDistanceCalculator, + autobrake_runway_overrun_protection: AutobrakeRunwayOverrunProtection, } impl A380AutobrakeController { const DURATION_OF_FLIGHT_TO_DISARM_AUTOBRAKE: Duration = Duration::from_secs(10); @@ -192,14 +207,10 @@ impl A380AutobrakeController { // Time breakpoint map is shared by all normal modes, and there's a BTV placeholder delaying braking const NORMAL_MODE_DECEL_PROFILE_TIME_S: [f64; 3] = [0., 0.1, 2.5]; - // BTV placeholder delays braking 4s - const BTV_MODE_DECEL_PROFILE_TIME_S: [f64; 4] = [0., 3.99, 4., 6.]; - const LOW_MODE_DECEL_PROFILE_ACCEL_MS2: [f64; 3] = [4., 0., -2.]; const L2_MODE_DECEL_PROFILE_ACCEL_MS2: [f64; 3] = [4., 0., -2.5]; const L3_MODE_DECEL_PROFILE_ACCEL_MS2: [f64; 3] = [4., 0., -3.]; const HIGH_MODE_DECEL_PROFILE_ACCEL_MS2: [f64; 3] = [4., -2., -3.5]; - const BTV_MODE_DECEL_PROFILE_ACCEL_MS2: [f64; 4] = [4., 4., -1., -2.5]; const RTO_MODE_DECEL_TARGET_MS2: f64 = -6.; const OFF_MODE_DECEL_TARGET_MS2: f64 = 5.; @@ -209,6 +220,8 @@ impl A380AutobrakeController { const TARGET_TO_SHOW_DECEL_IN_RTO_MS2: f64 = -2.7; const TARGET_TO_REMOVE_DECEL_IN_RTO_MS2: f64 = -2.; + const KNOB_SOLENOID_DISARM_DELAY: Duration = Duration::from_millis(1000); + pub fn new(context: &mut InitContext) -> A380AutobrakeController { A380AutobrakeController { armed_mode_id: context.get_identifier("AUTOBRAKES_ARMED_MODE".to_owned()), @@ -245,10 +258,18 @@ impl A380AutobrakeController { context, ElectricalBusType::DirectCurrent(2), ), + selection_knob_should_return_disarm: DelayedTrueLogicGate::new( + Self::KNOB_SOLENOID_DISARM_DELAY, + ), external_disarm_event: false, placeholder_ground_spoilers_out: false, + + btv_scheduler: BtvDecelScheduler::new(context), + + braking_distance_calculator: BrakingDistanceCalculator::new(context), + autobrake_runway_overrun_protection: AutobrakeRunwayOverrunProtection::new(context), } } @@ -261,55 +282,45 @@ impl A380AutobrakeController { } pub fn brake_output(&self) -> Ratio { - Ratio::new::(self.deceleration_governor.output()) - } - - fn determine_mode( - &mut self, - context: &UpdateContext, - autobrake_panel: &A380AutobrakePanel, - ) -> A380AutobrakeMode { - if self.should_disarm(context, autobrake_panel) { - self.disarm_actions(); - return A380AutobrakeMode::DISARM; + if self + .autobrake_runway_overrun_protection + .rop_max_braking_requested() + { + Ratio::new::(1.) + } else { + Ratio::new::(self.deceleration_governor.output()) } + } - if self.mode == A380AutobrakeMode::RTO - || autobrake_panel.rto_pressed() - && !self.should_reject_rto_mode_after_time_in_flight.output() + fn determine_mode(&mut self, autobrake_panel: &A380AutobrakePanel) -> A380AutobrakeMode { + if self.mode != A380AutobrakeMode::RTO + && autobrake_panel.rto_pressed() + && !self.should_reject_rto_mode_after_time_in_flight.output() { - if autobrake_panel.selected_mode() != A380AutobrakeKnobPosition::DISARM { - self.autobrake_knob.disarm(true); - } - A380AutobrakeMode::RTO - } else { - self.autobrake_knob.disarm(false); - if autobrake_panel.selected_mode_has_changed() { - match autobrake_panel.selected_mode() { - A380AutobrakeKnobPosition::DISARM => A380AutobrakeMode::DISARM, - A380AutobrakeKnobPosition::LOW => A380AutobrakeMode::LOW, - A380AutobrakeKnobPosition::L2 => A380AutobrakeMode::L2, - A380AutobrakeKnobPosition::L3 => A380AutobrakeMode::L3, - A380AutobrakeKnobPosition::HIGH => A380AutobrakeMode::HIGH, - A380AutobrakeKnobPosition::BTV => A380AutobrakeMode::BTV, + } else if autobrake_panel.selected_mode_has_changed() { + match autobrake_panel.selected_mode() { + A380AutobrakeKnobPosition::DISARM => A380AutobrakeMode::DISARM, + A380AutobrakeKnobPosition::LOW => A380AutobrakeMode::LOW, + A380AutobrakeKnobPosition::L2 => A380AutobrakeMode::L2, + A380AutobrakeKnobPosition::L3 => A380AutobrakeMode::L3, + A380AutobrakeKnobPosition::HIGH => A380AutobrakeMode::HIGH, + A380AutobrakeKnobPosition::BTV => { + self.btv_scheduler.enable(); + A380AutobrakeMode::BTV } - } else { - self.mode } + } else { + self.mode } } - fn should_engage_deceleration_governor( - &self, - context: &UpdateContext, - autobrake_panel: &A380AutobrakePanel, - ) -> bool { + fn should_engage_deceleration_governor(&self, context: &UpdateContext) -> bool { self.is_armed() && self.ground_spoilers_are_deployed // We wait 5s after deploy, but they need to be deployed even if nose compressed && (self.ground_spoilers_are_deployed_since_5s.output() || self.nose_gear_was_compressed_once) - && !self.should_disarm(context, autobrake_panel) + && !self.should_disarm(context) } fn is_armed(&self) -> bool { @@ -397,22 +408,23 @@ impl A380AutobrakeController { } } - fn should_disarm(&self, context: &UpdateContext, autobrake_panel: &A380AutobrakePanel) -> bool { + fn should_disarm(&self, context: &UpdateContext) -> bool { // when a simulation is started in flight, some values need to be ignored for a certain time to ensure // an unintended disarm is not happening (self.deceleration_governor.is_engaged() && self.should_disarm_due_to_pedal_input()) || (context.is_sim_ready() && !self.arming_is_allowed_by_bcu) || self.spoilers_retracted_during_this_update() - || self.rto_mode_deselected_this_update(autobrake_panel) || self.should_disarm_after_time_in_flight.output() || (self.external_disarm_event && self.mode != A380AutobrakeMode::RTO) || (self.mode == A380AutobrakeMode::RTO && self.should_reject_rto_mode_after_time_in_flight.output()) + || (self.mode == A380AutobrakeMode::BTV && !self.btv_scheduler.is_armed()) } fn disarm_actions(&mut self) { - self.autobrake_knob.disarm(true); + self.btv_scheduler.disarm(); self.nose_gear_was_compressed_once = false; + self.mode = A380AutobrakeMode::DISARM; } fn calculate_target(&mut self) -> Acceleration { @@ -444,13 +456,7 @@ impl A380AutobrakeController { } fn compute_btv_decel_target_ms2(&self) -> f64 { - // Placeholder BTV deceleration - - interpolation( - &Self::BTV_MODE_DECEL_PROFILE_TIME_S, - &Self::BTV_MODE_DECEL_PROFILE_ACCEL_MS2, - self.deceleration_governor.time_engaged().as_secs_f64(), - ) + self.btv_scheduler.decel().get::() } fn update_input_conditions( @@ -494,6 +500,7 @@ impl A380AutobrakeController { lgciu1: &impl LgciuInterface, lgciu2: &impl LgciuInterface, placeholder_ground_spoilers_out: bool, + ground_speed: Velocity, ) { self.update_input_conditions( context, @@ -504,21 +511,72 @@ impl A380AutobrakeController { lgciu2, ); - self.mode = self.determine_mode(context, autobrake_panel); + self.braking_distance_calculator.update_braking_estimations( + context, + ground_speed, + if self.mode == A380AutobrakeMode::BTV { + self.btv_scheduler.predicted_decel() + } else if self.mode != A380AutobrakeMode::DISARM { + context.long_accel() + } else { + Acceleration::default() + }, + ); + + let rto_disable = self.rto_mode_deselected_this_update(autobrake_panel); + + self.mode = self.determine_mode(autobrake_panel); + + if rto_disable || self.should_disarm(context) { + self.disarm_actions(); + } + + if self.mode != A380AutobrakeMode::BTV { + self.btv_scheduler.disarm() + } + + self.selection_knob_should_return_disarm.update( + context, + (self.mode == A380AutobrakeMode::DISARM || self.mode == A380AutobrakeMode::RTO) + && !autobrake_panel.is_disarmed_position(), + ); + + // Disarm solenoid only when arming is lost + self.autobrake_knob + .disarm(self.selection_knob_should_return_disarm.output()); self.deceleration_governor - .engage_when(self.should_engage_deceleration_governor(context, autobrake_panel)); + .engage_when(self.should_engage_deceleration_governor(context)); self.target = self.calculate_target(); self.deceleration_governor.update(context, self.target); self.update_decelerating_light_info(); self.placeholder_ground_spoilers_out = placeholder_ground_spoilers_out; + + self.btv_scheduler.update( + context, + self.ground_spoilers_are_deployed, + &self.braking_distance_calculator, + ground_speed, + &self.autobrake_runway_overrun_protection, + ); + + self.autobrake_runway_overrun_protection.update( + self.deceleration_governor.is_engaged(), + ground_speed, + &self.braking_distance_calculator, + lgciu1, + lgciu2, + ) } } impl SimulationElement for A380AutobrakeController { fn accept(&mut self, visitor: &mut T) { self.autobrake_knob.accept(visitor); + self.braking_distance_calculator.accept(visitor); + self.btv_scheduler.accept(visitor); + self.autobrake_runway_overrun_protection.accept(visitor); visitor.visit(self); } @@ -543,3 +601,823 @@ impl SimulationElement for A380AutobrakeController { } } } + +struct AutobrakeRunwayOverrunProtection { + distance_to_runway_end_id: VariableIdentifier, + autobrake_row_rop_word_id: VariableIdentifier, + + thrust_en1_id: VariableIdentifier, + thrust_en2_id: VariableIdentifier, + thrust_en3_id: VariableIdentifier, + thrust_en4_id: VariableIdentifier, + + throttle_percents: [f64; 4], + + distance_to_runway_end: Arinc429Word, + + is_actively_braking: bool, + + is_any_autobrake_active: bool, + ground_speed: Velocity, + + status_word: Arinc429Word, +} +impl AutobrakeRunwayOverrunProtection { + const MIN_ARMING_SPEED_MS2: f64 = 10.28; + + fn new(context: &mut InitContext) -> Self { + Self { + distance_to_runway_end_id: context + .get_identifier("OANS_BTV_REMAINING_DIST_TO_RWY_END".to_owned()), + autobrake_row_rop_word_id: context.get_identifier("ROW_ROP_WORD_1".to_owned()), + + thrust_en1_id: context.get_identifier("AUTOTHRUST_TLA:1".to_owned()), + thrust_en2_id: context.get_identifier("AUTOTHRUST_TLA:2".to_owned()), + thrust_en3_id: context.get_identifier("AUTOTHRUST_TLA:3".to_owned()), + thrust_en4_id: context.get_identifier("AUTOTHRUST_TLA:4".to_owned()), + + throttle_percents: [0.; 4], + + distance_to_runway_end: Arinc429Word::new( + Length::default(), + SignStatus::NoComputedData, + ), + + is_actively_braking: false, + + is_any_autobrake_active: false, + + ground_speed: Velocity::default(), + + status_word: Arinc429Word::new(0, SignStatus::NormalOperation), + } + } + + fn is_row_rop_operative(&self) -> bool { + self.distance_to_runway_end.is_normal_operation() + && self.ground_speed.get::() > Self::MIN_ARMING_SPEED_MS2 + } + + fn distance_to_runway_end(&self) -> Length { + if self.distance_to_runway_end.is_normal_operation() { + self.distance_to_runway_end.value() + } else { + Length::new::(5000.) + } + } + + fn update( + &mut self, + is_any_autobrake_active: bool, + ground_speed: Velocity, + braking_distances: &BrakingDistanceCalculator, + lgciu1: &impl LgciuInterface, + lgciu2: &impl LgciuInterface, + ) { + self.is_any_autobrake_active = is_any_autobrake_active; + self.ground_speed = ground_speed; + + let is_on_ground = lgciu1.left_and_right_gear_compressed(false) + || lgciu2.left_and_right_gear_compressed(false); + + let max_braking_prediction = braking_distances.max_braking(); + + // Can engage only above min speed + if self.is_row_rop_operative() && self.is_any_autobrake_active { + if max_braking_prediction >= self.distance_to_runway_end.value() { + self.is_actively_braking = true; + } + } else { + // Can only disengage if autobrake or distance lost (not from speed) + // TODO ROP can revert if braking force is sufficient + if !self.distance_to_runway_end.is_normal_operation() || !self.is_any_autobrake_active { + self.is_actively_braking = false; + } + } + + // IS operative + self.status_word.set_bit(11, self.is_row_rop_operative()); + + // Is active under autobrake + self.status_word.set_bit(12, self.is_actively_braking); + + // Is active under manual braking + self.status_word.set_bit( + 13, + self.should_show_manual_braking_warning(max_braking_prediction, is_on_ground), + ); + + let should_show_in_flight_row = !is_on_ground && self.is_row_rop_operative(); + // Too short if wet + self.status_word.set_bit( + 14, + should_show_in_flight_row + && braking_distances.wet_landing() >= self.distance_to_runway_end.value(), + ); + + // Too short for dry + self.status_word.set_bit( + 15, + should_show_in_flight_row + && braking_distances.dry_landing() >= self.distance_to_runway_end.value(), + ); + } + + fn rop_max_braking_requested(&self) -> bool { + self.is_actively_braking + } + + fn should_show_manual_braking_warning( + &self, + dry_stopping_prediction: Length, + is_on_ground: bool, + ) -> bool { + let any_engine_not_idle_or_reverse = self.throttle_percents.iter().any(|&x| x > 2.); + + if is_on_ground + && !any_engine_not_idle_or_reverse + && !self.is_any_autobrake_active + && self.is_row_rop_operative() + { + dry_stopping_prediction >= self.distance_to_runway_end.value() + } else { + false + } + } +} +impl SimulationElement for AutobrakeRunwayOverrunProtection { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write(&self.autobrake_row_rop_word_id, self.status_word); + } + + fn read(&mut self, reader: &mut SimulatorReader) { + let raw_feet_runway_end_arinc: Arinc429Word = + reader.read_arinc429(&self.distance_to_runway_end_id); + + self.distance_to_runway_end = Arinc429Word::new( + Length::new::(raw_feet_runway_end_arinc.value()), + raw_feet_runway_end_arinc.ssm(), + ); + + let tla1: f64 = reader.read(&self.thrust_en1_id); + let tla2: f64 = reader.read(&self.thrust_en2_id); + let tla3: f64 = reader.read(&self.thrust_en3_id); + let tla4: f64 = reader.read(&self.thrust_en4_id); + self.throttle_percents = [tla1, tla2, tla3, tla4]; + } +} + +#[derive(PartialEq, Clone, Copy, Debug)] +enum BTVState { + Disabled, + Armed, + RotOptimization, + Decel, + EndOfBraking, +} + +struct BrakingDistanceCalculator { + wet_estimated_distance_id: VariableIdentifier, + dry_estimated_distance_id: VariableIdentifier, + autobrake_estimated_stop_id: VariableIdentifier, + predicted_touchdown_speed_id: VariableIdentifier, + + dry_landing_estimated_distance: LowPassFilter, + wet_landing_estimated_distance: LowPassFilter, + braking_estimated_distance_at_current_decel: LowPassFilter, + braking_estimated_distance_at_max_decel: LowPassFilter, + predicted_touchdown_speed: Velocity, +} +impl BrakingDistanceCalculator { + const MAX_DECEL_DRY_MS2: f64 = -2.8; + const MAX_DECEL_WET_MS2: f64 = -1.8; + + const MIN_DECEL_FOR_STOPPING_ESTIMATION_MS2: f64 = -0.2; + const MIN_SPEED_FOR_STOPPING_ESTIMATION_MS: f64 = 15.; + + const MAX_STOPPING_DISTANCE_M: f64 = 5000.; + + const ROLLING_TIME_AFTER_TD_BEFORE_BRAKES_S: f64 = 5.; + + // Offset for stop bar so it shows at front of the plane instead of its reference position + const OFFSET_PLANE_REF_POINT_TO_FRONT_METERS: f64 = 40.; + + const ALTITUDE_THRESHOLD_TO_SWITCH_ESTIMATION_TO_GROUND_SPEED_FT: f64 = 500.; + const MIN_PREDICTED_TOUCHDOWN_SPEED_KNOT: f64 = 100.; + + fn new(context: &mut InitContext) -> Self { + Self { + wet_estimated_distance_id: context + .get_identifier("OANS_BTV_WET_DISTANCE_ESTIMATED".to_owned()), + dry_estimated_distance_id: context + .get_identifier("OANS_BTV_DRY_DISTANCE_ESTIMATED".to_owned()), + + autobrake_estimated_stop_id: context + .get_identifier("OANS_BTV_STOP_BAR_DISTANCE_ESTIMATED".to_owned()), + predicted_touchdown_speed_id: context.get_identifier("SPEEDS_VAPP".to_owned()), + + dry_landing_estimated_distance: LowPassFilter::new(Duration::from_millis(800)), + wet_landing_estimated_distance: LowPassFilter::new(Duration::from_millis(800)), + braking_estimated_distance_at_current_decel: LowPassFilter::new(Duration::from_millis( + 500, + )), + braking_estimated_distance_at_max_decel: LowPassFilter::new(Duration::from_millis(500)), + + predicted_touchdown_speed: Velocity::default(), + } + } + + fn update_braking_estimations( + &mut self, + context: &UpdateContext, + ground_speed: Velocity, + deceleration: Acceleration, + ) { + // TODO use correct input to switch speed used + let speed_used_for_prediction = if context.plane_height_over_ground().get::() + < Self::ALTITUDE_THRESHOLD_TO_SWITCH_ESTIMATION_TO_GROUND_SPEED_FT + { + ground_speed + } else { + self.predicted_touchdown_speed.max(Velocity::new::( + Self::MIN_PREDICTED_TOUCHDOWN_SPEED_KNOT, + )) + }; + + if ground_speed.get::() > Self::MIN_SPEED_FOR_STOPPING_ESTIMATION_MS { + self.wet_landing_estimated_distance.update( + context.delta(), + self.stopping_distance_estimation_for_wet(speed_used_for_prediction), + ); + self.dry_landing_estimated_distance.update( + context.delta(), + self.stopping_distance_estimation_for_dry(speed_used_for_prediction), + ); + } else { + self.wet_landing_estimated_distance.reset(Length::default()); + self.dry_landing_estimated_distance.reset(Length::default()); + } + + if context.long_accel().get::() + < Self::MIN_DECEL_FOR_STOPPING_ESTIMATION_MS2 + && ground_speed.get::() > Self::MIN_SPEED_FOR_STOPPING_ESTIMATION_MS + { + self.braking_estimated_distance_at_current_decel.update( + context.delta(), + self.stopping_distance_estimation_for_decel(ground_speed, deceleration), + ); + self.braking_estimated_distance_at_max_decel.update( + context.delta(), + self.stopping_distance_estimation_for_decel( + ground_speed, + Acceleration::new::(Self::MAX_DECEL_DRY_MS2), + ), + ); + } else { + self.braking_estimated_distance_at_current_decel + .reset(Length::default()); + self.braking_estimated_distance_at_max_decel + .reset(Length::default()); + } + } + + fn stopping_distance_estimation_for_dry(&self, current_speed: Velocity) -> Length { + self.distance_run_before_autobrake_active(current_speed) + + self.stopping_distance_estimation_for_decel( + current_speed, + Acceleration::new::(Self::MAX_DECEL_DRY_MS2), + ) + } + + fn stopping_distance_estimation_for_wet(&self, current_speed: Velocity) -> Length { + self.distance_run_before_autobrake_active(current_speed) + + self.stopping_distance_estimation_for_decel( + current_speed, + Acceleration::new::(Self::MAX_DECEL_WET_MS2), + ) + } + + fn stopping_distance_estimation_for_decel( + &self, + current_speed: Velocity, + deceleration: Acceleration, + ) -> Length { + if deceleration.get::() + < Self::MIN_DECEL_FOR_STOPPING_ESTIMATION_MS2 + { + Length::new::( + (current_speed.get::().powi(2) + / (2. * deceleration.get::().abs())) + .clamp(0., Self::MAX_STOPPING_DISTANCE_M), + ) + } else { + Length::new::(0.) + } + } + + fn dry_landing(&self) -> Length { + self.dry_landing_estimated_distance.output() + } + + fn max_braking(&self) -> Length { + self.braking_estimated_distance_at_max_decel.output() + } + + fn wet_landing(&self) -> Length { + self.wet_landing_estimated_distance.output() + } + + fn distance_run_before_autobrake_active(&self, speed_at_touchdown: Velocity) -> Length { + Length::new::( + speed_at_touchdown.get::() + * Self::ROLLING_TIME_AFTER_TD_BEFORE_BRAKES_S, + ) + } +} +impl SimulationElement for BrakingDistanceCalculator { + fn write(&self, writer: &mut SimulatorWriter) { + writer.write( + &self.wet_estimated_distance_id, + self.wet_landing_estimated_distance.output().get::(), + ); + writer.write( + &self.dry_estimated_distance_id, + self.dry_landing_estimated_distance.output().get::(), + ); + + writer.write( + &self.autobrake_estimated_stop_id, + self.braking_estimated_distance_at_current_decel + .output() + .get::() + + Self::OFFSET_PLANE_REF_POINT_TO_FRONT_METERS, + ); + } + fn read(&mut self, reader: &mut SimulatorReader) { + self.predicted_touchdown_speed = reader.read(&self.predicted_touchdown_speed_id); + } +} + +struct BtvDecelScheduler { + in_flight_btv_stopping_distance_id: VariableIdentifier, + runway_length_id: VariableIdentifier, + distance_to_exit_id: VariableIdentifier, + rot_estimation_id: VariableIdentifier, + turnaround_idle_reverse_estimation_id: VariableIdentifier, + turnaround_max_reverse_estimation_id: VariableIdentifier, + + runway_length: Arinc429Word, + + rolling_distance: Length, + in_flight_btv_stopping_distance: Arinc429Word, + oans_distance_to_exit: Arinc429Word, + + spoilers_active: bool, + + state: BTVState, + + deceleration_request: Acceleration, + end_of_decel_acceleration: Acceleration, + desired_deceleration: Acceleration, + + actual_deceleration: Acceleration, + + final_distance_remaining: Length, + + distance_remaining_at_decel_activation: Length, + + dry_prediction: Length, + wet_prediction: Length, + + distance_to_rwy_end: Length, +} +impl BtvDecelScheduler { + // Target decel when optimizing runway time before braking + const ROT_OPTIMIZATION_TARGET_DECEL_M_S_2: f64 = -0.2; + + // Target decel ratio to switch from ROT optimization to braking phase + const DECEL_RATIO_TO_REACH_TO_START_DECEL: f64 = 0.98; + + const MAX_DECEL_DRY_MS2: f64 = -3.0; + const MAX_DECEL_WET_MS2: f64 = -2.0; + + const MIN_RUNWAY_LENGTH_M: f64 = 1500.; + + const DISTANCE_OFFSET_TO_RELEASE_BTV_M: f64 = 65.5; // Targeted distance for deceleration computation + const DISTANCE_TO_RELEASE_BTV_M: f64 = 50.; // Targeted distance to cut off BTV mode + + const TARGET_SPEED_TO_RELEASE_BTV_M_S: f64 = 5.15; + const SAFETY_RATIO_ON_RELEASE_SPEED: f64 = 0.9; // 0.9 = -10% margin on TARGET_SPEED_TO_RELEASE_BTV_M_S + + const MAX_DECEL_SAFETY_MARGIN_RATIO: f64 = 1.4; + const MIN_DECEL_SAFETY_MARGIN_RATIO: f64 = 1.15; + const DECEL_SAFETY_MARGIN_SHAPING_FACTOR: f64 = 0.4; + + const REMAINING_BRAKING_DISTANCE_END_OF_RUNWAY_OFFSET_METERS: f64 = 300.; + + fn new(context: &mut InitContext) -> Self { + Self { + in_flight_btv_stopping_distance_id: context + .get_identifier("OANS_BTV_REQ_STOPPING_DISTANCE".to_owned()), + runway_length_id: context.get_identifier("OANS_RWY_LENGTH".to_owned()), + distance_to_exit_id: context + .get_identifier("OANS_BTV_REMAINING_DIST_TO_EXIT".to_owned()), + rot_estimation_id: context.get_identifier("BTV_ROT".to_owned()), + turnaround_idle_reverse_estimation_id: context + .get_identifier("BTV_TURNAROUND_IDLE_REVERSE".to_owned()), + turnaround_max_reverse_estimation_id: context + .get_identifier("BTV_TURNAROUND_MAX_REVERSE".to_owned()), + + runway_length: Arinc429Word::new(Length::default(), SignStatus::NoComputedData), + rolling_distance: Length::default(), + in_flight_btv_stopping_distance: Arinc429Word::new( + Length::default(), + SignStatus::NoComputedData, + ), + oans_distance_to_exit: Arinc429Word::new(Length::default(), SignStatus::NoComputedData), + + spoilers_active: false, + + state: BTVState::Disabled, + + deceleration_request: Acceleration::default(), + end_of_decel_acceleration: Acceleration::default(), + desired_deceleration: Acceleration::new::( + Self::MAX_DECEL_DRY_MS2, + ), + actual_deceleration: Acceleration::default(), + + final_distance_remaining: Length::default(), + + distance_remaining_at_decel_activation: Length::default(), + + dry_prediction: Length::default(), + wet_prediction: Length::default(), + + distance_to_rwy_end: Length::default(), + } + } + + fn enable(&mut self) { + if self.state == BTVState::Disabled && self.arming_authorized() { + self.state = BTVState::Armed; + } + } + + fn disarm(&mut self) { + self.state = BTVState::Disabled; + self.deceleration_request = Acceleration::new::(5.); + self.end_of_decel_acceleration = Acceleration::new::(5.); + self.final_distance_remaining = Length::default(); + self.distance_remaining_at_decel_activation = Length::default(); + self.desired_deceleration = + Acceleration::new::(Self::MAX_DECEL_DRY_MS2); + } + + fn decel(&self) -> Acceleration { + match self.state { + BTVState::Decel => self.deceleration_request, + BTVState::EndOfBraking => self.end_of_decel_acceleration, + BTVState::RotOptimization => self.accel_during_rot_opti(), + BTVState::Disabled | BTVState::Armed => { + Acceleration::new::(5.) + } + } + } + + fn update( + &mut self, + context: &UpdateContext, + spoilers_active: bool, + braking_distance: &BrakingDistanceCalculator, + ground_speed: Velocity, + rop: &AutobrakeRunwayOverrunProtection, + ) { + self.distance_to_rwy_end = rop.distance_to_runway_end(); + + self.wet_prediction = braking_distance.wet_landing(); + self.dry_prediction = braking_distance.dry_landing(); + + self.spoilers_active = spoilers_active; + self.actual_deceleration = context.long_accel(); + + self.integrate_distance(context, ground_speed); + + self.compute_decel(ground_speed); + + self.state = self.update_state(ground_speed); + } + + fn braking_distance_remaining(&self) -> Length { + let distance_remaining_raw = if self.is_oans_fallback_mode() { + self.in_flight_btv_stopping_distance.value() - self.rolling_distance + } else { + self.oans_distance_to_exit.value() + }; + + let distance_from_btv_exit = Length::new::(Self::DISTANCE_OFFSET_TO_RELEASE_BTV_M); + + // Max distance clamped to end of rwy minus a margin + (distance_remaining_raw - distance_from_btv_exit).clamp( + Length::default(), + self.distance_to_rwy_end + - Length::new::( + Self::REMAINING_BRAKING_DISTANCE_END_OF_RUNWAY_OFFSET_METERS, + ), + ) + } + + fn compute_decel(&mut self, ground_speed: Velocity) { + match self.state { + BTVState::RotOptimization | BTVState::Decel | BTVState::EndOfBraking => { + let speed_at_btv_release = + Velocity::new::(Self::TARGET_SPEED_TO_RELEASE_BTV_M_S) + * Self::SAFETY_RATIO_ON_RELEASE_SPEED; + + self.final_distance_remaining = self.braking_distance_remaining(); + + let delta_speed_to_achieve = ground_speed - speed_at_btv_release; + + let target_deceleration_raw = + -delta_speed_to_achieve.get::().powi(2) + / (2. * self.final_distance_remaining.get::()); + + let target_deceleration_safety_corrected = + target_deceleration_raw * self.safety_margin(); + + self.deceleration_request = Acceleration::new::( + target_deceleration_safety_corrected.clamp( + self.desired_deceleration.get::(), + 5., + ), + ); + } + BTVState::Armed | BTVState::Disabled => { + self.deceleration_request = Acceleration::new::(5.); + } + } + } + + fn arming_authorized(&self) -> bool { + self.runway_length.is_normal_operation() + && self.runway_length.value().get::() >= Self::MIN_RUNWAY_LENGTH_M + && self.in_flight_btv_stopping_distance.is_normal_operation() + && self.runway_length.value().get::() > self.dry_prediction.get::() + } + + fn accel_to_reach_to_decelerate(&self) -> Acceleration { + self.desired_deceleration * Self::DECEL_RATIO_TO_REACH_TO_START_DECEL + } + + fn accel_during_rot_opti(&self) -> Acceleration { + Acceleration::new::(Self::ROT_OPTIMIZATION_TARGET_DECEL_M_S_2) + } + + // Safety margin gives a dynamic ratio on targeted decel based on remaining distance + fn safety_margin(&self) -> f64 { + match self.state { + BTVState::Decel | BTVState::EndOfBraking => { + let ratio_of_decel_distance = + self.braking_distance_remaining() / self.distance_remaining_at_decel_activation; + + (1. + (ratio_of_decel_distance.get::().sqrt() + * Self::DECEL_SAFETY_MARGIN_SHAPING_FACTOR)) + .clamp( + Self::MIN_DECEL_SAFETY_MARGIN_RATIO, + Self::MAX_DECEL_SAFETY_MARGIN_RATIO, + ) + } + + BTVState::Disabled | BTVState::Armed | BTVState::RotOptimization => { + Self::MAX_DECEL_SAFETY_MARGIN_RATIO + } + } + } + + fn update_state(&mut self, ground_speed: Velocity) -> BTVState { + match self.state { + BTVState::Armed => { + if self.spoilers_active { + self.update_desired_btv_deceleration(); + BTVState::RotOptimization + } else if !self.arming_authorized() { + BTVState::Disabled + } else { + self.state + } + } + BTVState::RotOptimization => { + let accel_min = self.accel_to_reach_to_decelerate(); + + if self.deceleration_request < accel_min { + self.distance_remaining_at_decel_activation = self.braking_distance_remaining(); + self.end_of_decel_acceleration = self.deceleration_request; + BTVState::Decel + } else { + self.state + } + } + BTVState::Decel => { + if self.final_distance_remaining.get::() < Self::DISTANCE_TO_RELEASE_BTV_M + || ground_speed.get::() + <= Self::TARGET_SPEED_TO_RELEASE_BTV_M_S + { + self.end_of_decel_acceleration = self.deceleration_request; + BTVState::EndOfBraking + } else { + BTVState::Decel + } + } + BTVState::EndOfBraking => { + if ground_speed.get::() <= Self::TARGET_SPEED_TO_RELEASE_BTV_M_S { + self.disarm(); + BTVState::Disabled + } else { + BTVState::EndOfBraking + } + } + BTVState::Disabled => self.state, + } + } + + fn integrate_distance(&mut self, context: &UpdateContext, ground_speed: Velocity) { + match self.state { + BTVState::RotOptimization | BTVState::Decel | BTVState::EndOfBraking => { + let distance_this_tick = ground_speed * context.delta_as_time(); + self.rolling_distance += distance_this_tick; + } + + BTVState::Disabled | BTVState::Armed => self.rolling_distance = Length::default(), + } + } + + fn is_oans_fallback_mode(&self) -> bool { + !self.oans_distance_to_exit.is_normal_operation() + } + + fn is_armed(&self) -> bool { + self.state != BTVState::Disabled + } + + fn rot_estimation_for_distance(&self) -> Arinc429Word { + let distance_valid = self.in_flight_btv_stopping_distance.is_normal_operation(); + + if distance_valid { + let distance = self.in_flight_btv_stopping_distance.value(); + + // Magic statistical function: basic regression on a landing attempts database + let rot_duration = + Duration::from_secs_f64((distance.get::() * 0.0335).clamp(30., 200.)); + Arinc429Word::new(rot_duration.as_secs(), SignStatus::NormalOperation) + } else { + Arinc429Word::new(0, SignStatus::NoComputedData) + } + } + + fn turnaround_estimation_from_time_on_runway( + &self, + rot_seconds: f64, + ) -> [Arinc429Word; 2] { + let distance_valid = self.in_flight_btv_stopping_distance.is_normal_operation(); + + if distance_valid && rot_seconds > 0. { + let is_max_braking = self.braking_distance_remaining() < self.wet_prediction; + + // Magic statistical function for max turnaound. Idle is max+15%. 10% penalty if max braking is used + let mut max_reverse_duration_minutes = + (0.00495 * rot_seconds.powi(2) - 1.2244 * rot_seconds + 204.).clamp(10., 500.); + + if is_max_braking { + max_reverse_duration_minutes *= 1.1; + } + + let idle_reverse_duration_minutes = + (max_reverse_duration_minutes * 1.15).clamp(10., 500.); + + [ + Arinc429Word::new( + max_reverse_duration_minutes as u64, + SignStatus::NormalOperation, + ), + Arinc429Word::new( + idle_reverse_duration_minutes as u64, + SignStatus::NormalOperation, + ), + ] + } else { + [ + Arinc429Word::new(0, SignStatus::NoComputedData), + Arinc429Word::new(0, SignStatus::NoComputedData), + ] + } + } + + fn predicted_decel(&self) -> Acceleration { + match self.state { + BTVState::Disabled | BTVState::Armed => Acceleration::default(), + BTVState::RotOptimization => self.deceleration_request, + BTVState::Decel | BTVState::EndOfBraking => self.actual_deceleration, + } + } + + fn update_desired_btv_deceleration(&mut self) { + self.desired_deceleration = if self.braking_distance_remaining() < self.wet_prediction { + Acceleration::new::(Self::MAX_DECEL_DRY_MS2) + } else { + Acceleration::new::(Self::MAX_DECEL_WET_MS2) + }; + } +} +impl SimulationElement for BtvDecelScheduler { + fn write(&self, writer: &mut SimulatorWriter) { + let rot_arinc = self.rot_estimation_for_distance(); + let turnaround_time_estimated_in_minutes = + self.turnaround_estimation_from_time_on_runway(rot_arinc.value() as f64); + + writer.write_arinc429(&self.rot_estimation_id, rot_arinc.value(), rot_arinc.ssm()); + + writer.write_arinc429( + &self.turnaround_idle_reverse_estimation_id, + turnaround_time_estimated_in_minutes[1].value(), + turnaround_time_estimated_in_minutes[1].ssm(), + ); + writer.write_arinc429( + &self.turnaround_max_reverse_estimation_id, + turnaround_time_estimated_in_minutes[0].value(), + turnaround_time_estimated_in_minutes[0].ssm(), + ); + } + + fn read(&mut self, reader: &mut SimulatorReader) { + let raw_in_flight_btv_stopping_distance_arinc: Arinc429Word = + reader.read_arinc429(&self.in_flight_btv_stopping_distance_id); + + self.in_flight_btv_stopping_distance = Arinc429Word::new( + Length::new::(raw_in_flight_btv_stopping_distance_arinc.value()), + raw_in_flight_btv_stopping_distance_arinc.ssm(), + ); + + let raw_feet_runway_length_arinc: Arinc429Word = + reader.read_arinc429(&self.runway_length_id); + + self.runway_length = Arinc429Word::new( + Length::new::(raw_feet_runway_length_arinc.value()), + raw_feet_runway_length_arinc.ssm(), + ); + + let raw_feet_exit_length_arinc: Arinc429Word = + reader.read_arinc429(&self.distance_to_exit_id); + + self.oans_distance_to_exit = Arinc429Word::new( + Length::new::(raw_feet_exit_length_arinc.value()), + raw_feet_exit_length_arinc.ssm(), + ); + } +} + +#[cfg(test)] +mod braking_distance_tests { + use systems::simulation::test::TestBed; + + use super::*; + use crate::systems::simulation::test::{ElementCtorFn, SimulationTestBed}; + + #[test] + fn landing_140_knot_dry_line() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(BrakingDistanceCalculator::new)) + .with_update_after_power_distribution(|e, context| { + e.update_braking_estimations( + context, + Velocity::new::(140.), + Acceleration::default(), + ) + }); + + test_bed.set_on_ground(true); + test_bed.run_multiple_frames(Duration::from_secs(5)); + + assert!( + test_bed.query_element(|e| e.dry_landing().get::() > 1200. + && test_bed.query_element(|e| e.dry_landing().get::() < 1500.)) + ); + } + + #[test] + fn landing_140_knot_wet_line() { + let mut test_bed = SimulationTestBed::from(ElementCtorFn(BrakingDistanceCalculator::new)) + .with_update_after_power_distribution(|e, context| { + e.update_braking_estimations( + context, + Velocity::new::(140.), + Acceleration::default(), + ) + }); + + test_bed.run_multiple_frames(Duration::from_secs(5)); + + assert!( + test_bed.query_element(|e| e.wet_landing().get::() > 1700. + && test_bed.query_element(|e| e.wet_landing().get::() < 2300.)) + ); + } +} diff --git a/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs b/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs index 7e53642ee03..c092d907104 100644 --- a/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs +++ b/fbw-a380x/src/wasm/systems/a380_systems/src/hydraulic/mod.rs @@ -3988,6 +3988,7 @@ impl A380HydraulicBrakeSteerComputerUnit { lgciu1, lgciu2, placeholder_ground_spoilers_out, + self.ground_speed, ); let is_in_flight_gear_lever_up = !(lgciu1.left_and_right_gear_compressed(true) @@ -6193,6 +6194,8 @@ struct SpoilerGroup { spoiler_positions: [f64; 8], } impl SpoilerGroup { + const PLACE_HOLDER_POSITION_DEMAND_THRESHOLD_TO_DECLARE_GROUND_SPOILER_RATIO: f64 = 0.55; + fn new(context: &mut InitContext, spoiler_side: &str, spoilers: [SpoilerElement; 8]) -> Self { Self { spoilers, @@ -6278,8 +6281,15 @@ impl SpoilerGroup { } fn ground_spoilers_are_requested(&self) -> bool { - self.hydraulic_controllers[0].requested_position() > Ratio::new::(0.1) - && self.hydraulic_controllers[1].requested_position() > Ratio::new::(0.1) + // Placeholder to decide if ground spoilers are requested. TODO use actual signal from flight controls + self.hydraulic_controllers[0].requested_position() + > Ratio::new::( + Self::PLACE_HOLDER_POSITION_DEMAND_THRESHOLD_TO_DECLARE_GROUND_SPOILER_RATIO, + ) + && self.hydraulic_controllers[1].requested_position() + > Ratio::new::( + Self::PLACE_HOLDER_POSITION_DEMAND_THRESHOLD_TO_DECLARE_GROUND_SPOILER_RATIO, + ) } } impl SimulationElement for SpoilerGroup { @@ -9353,7 +9363,7 @@ mod tests { } #[test] - fn autobrakes_arms_in_flight_btv_to_hi() { + fn autobrakes_arms_in_flight_lo_to_hi() { let mut test_bed = test_bed_on_ground_with() .set_cold_dark_inputs() .in_flight() @@ -9362,11 +9372,12 @@ mod tests { assert!(test_bed.autobrake_mode() == A380AutobrakeMode::DISARM); + // BTV not programmed cannot arm test_bed = test_bed .set_autobrake_btv() .run_waiting_for(Duration::from_secs(1)); - assert!(test_bed.autobrake_mode() == A380AutobrakeMode::BTV); + assert!(test_bed.autobrake_mode() == A380AutobrakeMode::DISARM); test_bed = test_bed .set_autobrake_low() @@ -9513,6 +9524,34 @@ mod tests { assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); } + #[test] + fn autobrakes_activates_on_ground_and_deactivates_pressing_rto_again() { + let mut test_bed = test_bed_on_ground_with() + .set_cold_dark_inputs() + .on_the_ground() + .set_park_brake(false) + .start_eng1(Ratio::new::(100.)) + .start_eng2(Ratio::new::(100.)) + .run_waiting_for(Duration::from_secs(10)); + + test_bed = test_bed + .set_autobrake_rto() + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.autobrake_mode() == A380AutobrakeMode::RTO); + + test_bed = test_bed + .set_autobrake_rto() + .run_waiting_for(Duration::from_secs(1)); + + assert!(test_bed.autobrake_mode() == A380AutobrakeMode::DISARM); + assert!(test_bed.get_brake_left_green_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_green_pressure() < Pressure::new::(50.)); + + assert!(test_bed.get_brake_left_yellow_pressure() < Pressure::new::(50.)); + assert!(test_bed.get_brake_right_yellow_pressure() < Pressure::new::(50.)); + } + #[test] fn autobrakes_disengage_on_spoiler_retract() { let mut test_bed = test_bed_on_ground_with() diff --git a/fbw-common/src/wasm/systems/systems/src/shared/mod.rs b/fbw-common/src/wasm/systems/systems/src/shared/mod.rs index 1c0ab20b249..245998d42f6 100644 --- a/fbw-common/src/wasm/systems/systems/src/shared/mod.rs +++ b/fbw-common/src/wasm/systems/systems/src/shared/mod.rs @@ -18,6 +18,7 @@ use uom::si::{ ratio::ratio, thermodynamic_temperature::{degree_celsius, kelvin}, velocity::knot, + Quantity, }; pub mod low_pass_filter; @@ -1053,6 +1054,40 @@ pub enum FireDetectionLoopID { B, } +pub trait Clamp { + /// Restrict a value to a certain interval unless it is NaN. + /// + /// Returns `max` if `self` is greater than `max`, and `min` if `self` is + /// less than `min`. Otherwise this returns `self`. + /// + /// Note that this function returns NaN if the initial value was NaN as + /// well. + /// + /// # Panics + /// + /// Panics if `min > max`, `min` is NaN, or `max` is NaN. + fn clamp(self, min: Self, max: Self) -> Self; +} + +// TODO: remove when uom implements clamp on floating point quantities +impl + ?Sized> Clamp + for Quantity +{ + fn clamp(mut self, min: Self, max: Self) -> Self { + assert!( + min <= max, + "min > max, or either was NaN. min = {min:?}, max = {max:?}" + ); + if self < min { + self = min; + } + if self > max { + self = max; + } + self + } +} + #[cfg(test)] mod delayed_true_logic_gate_tests { use super::*; diff --git a/fbw-common/src/wasm/systems/systems/src/simulation/test.rs b/fbw-common/src/wasm/systems/systems/src/simulation/test.rs index 0491db0c8c8..90ddf830c69 100644 --- a/fbw-common/src/wasm/systems/systems/src/simulation/test.rs +++ b/fbw-common/src/wasm/systems/systems/src/simulation/test.rs @@ -398,6 +398,7 @@ impl SimulationTestBed { let mut gear_compression = Ratio::new::(0.5); if on_ground { gear_compression = Ratio::new::(0.8); + self.write_by_name(UpdateContext::ALT_ABOVE_GROUND_KEY, 0.); } self.write_by_name(LandingGear::GEAR_CENTER_COMPRESSION, gear_compression);