diff --git a/fbw-a380x/mach.config.js b/fbw-a380x/mach.config.js index 1a751a10370..25792b37d9a 100644 --- a/fbw-a380x/mach.config.js +++ b/fbw-a380x/mach.config.js @@ -36,11 +36,12 @@ module.exports = { msfsAvionicsInstrument('ND'), msfsAvionicsInstrument('PFD'), msfsAvionicsInstrument('RMP'), + msfsAvionicsInstrument('OIT'), reactInstrument('BAT'), reactInstrument('EFB', ['/Pages/VCockpit/Instruments/Shared/Map/MapInstrument.html']), reactInstrument('ISISlegacy'), - reactInstrument('OIT'), + reactInstrument('OITlegacy'), reactInstrument('RTPI'), reactInstrument('SD'), ], diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_COCKPIT.xml b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_COCKPIT.xml index 1e1b9771bb1..1dabff7dddf 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_COCKPIT.xml +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/model/A380_COCKPIT.xml @@ -357,14 +357,26 @@ - - + - - EFB - 0.75 + + SCREEN_OIT_LEFT + 81 + (L:A32NX_ELEC_AC_2_BUS_IS_POWERED, Bool) + + + + + + + + + + SCREEN_OIT_RIGHT + 82 + (L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED, Bool) @@ -388,7 +400,6 @@ - @@ -1245,11 +1256,18 @@ KNOB_EFIS_CS_MFD - ND_Brightness TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_L_MFD_DECREASE TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_L_MFD_INCREASE 98 + + + + KNOB_EFIS_CS_OIT + TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_L_OIT_DECREASE + TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_L_OIT_INCREASE + 81 + @@ -1392,6 +1410,15 @@ TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_R_MFD_INCREASE 99 + + + + KNOB_EFIS_FO_OIT + OIT_Brightness_FO + TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_L_OIT_DECREASE + TT:COCKPIT.TOOLTIPS.LIGHTING_KNOB_L_OIT_INCREASE + 82 + diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/panel/panel.cfg b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/panel/panel.cfg index 60212725b7b..e96bc594c9a 100644 --- a/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/panel/panel.cfg +++ b/fbw-a380x/src/base/flybywire-aircraft-a380-842/SimObjects/AirPlanes/FlyByWire_A380_842/panel/panel.cfg @@ -150,6 +150,23 @@ texture=SCREEN_DU_RMP_3 htmlgauge00=A380X/RMP/rmp.html?Index=3, 0,0,1664,1024 [VCockpit19] +size_mm=1333,1000 +pixel_size=1333,1000 +texture=$SCREEN_OIT_LEFT + +htmlgauge00=A380X/OIT/oit.html?Index=1, 0,0,1333,1000 +htmlgauge01=A380X/OITlegacy/oitlegacy.html, 0,0,1333,1000 + +[VCockpit20] +size_mm=1333,1000 +pixel_size=1333,1000 +texture=$SCREEN_OIT_RIGHT + +; Right OIT UV mapping broken; disable for now +;htmlgauge00=A380X/OIT/oit.html?Index=2, 0,0,1333,1000 +;htmlgauge01=A380X/OITlegacy/oitlegacy.html, 0,0,1333,1000 + +[VCockpit21] size_mm=0,0 pixel_size=0,0 texture=NO_TEXTURE @@ -160,7 +177,7 @@ htmlgauge01=WasmInstrument/WasmInstrument.html?wasm_module=fbw.wasm&wasm_gauge=f htmlgauge02=WasmInstrument/WasmInstrument.html?wasm_module=fadec-a380x.wasm&wasm_gauge=Gauge_Fadec,0,0,1,1 htmlgauge03=WasmInstrument/WasmInstrument.html?wasm_module=extra-backend-a380x.wasm&wasm_gauge=Gauge_Extra_Backend,0,0,1,1 -[VCockpit20] +[VCockpit22] size_mm=0,0 pixel_size=0,0 texture=NO_TEXTURE @@ -168,7 +185,7 @@ background_color=0,0,0 htmlgauge00=A380X/SystemsHost/systems-host.html,0,0,1,1 -[VCockpit21] +[VCockpit23] size_mm=0,0 pixel_size=0,0 texture=NO_TEXTURE diff --git a/fbw-a380x/src/base/flybywire-aircraft-a380-842/html_ui/Images/fbw-a380x/Common/fbw-tail.png b/fbw-a380x/src/base/flybywire-aircraft-a380-842/html_ui/Images/fbw-a380x/Common/fbw-tail.png new file mode 100644 index 00000000000..5eec9ef045e Binary files /dev/null and b/fbw-a380x/src/base/flybywire-aircraft-a380-842/html_ui/Images/fbw-a380x/Common/fbw-tail.png differ diff --git a/fbw-a380x/src/systems/failures/src/a380.ts b/fbw-a380x/src/systems/failures/src/a380.ts index e30eab60051..4d622ead301 100644 --- a/fbw-a380x/src/systems/failures/src/a380.ts +++ b/fbw-a380x/src/systems/failures/src/a380.ts @@ -167,6 +167,14 @@ export const A380Failure = Object.freeze({ Transponder1: 34003, Transponder2: 34004, + + NssAnsu1: 46001, + NssAnsu2: 46002, + FltOpsAnsu: 46003, + CaptainLaptop: 46004, + FirstOfficerLaptop: 46005, + CaptainOit: 46006, + FirstOfficerOit: 46007, }); export const A380FailureDefinitions: FailureDefinition[] = [ @@ -329,4 +337,12 @@ export const A380FailureDefinitions: FailureDefinition[] = [ [34, A380Failure.RadioAntennaDirectCoupling3, 'RA SYS C Direct Coupling'], [34, A380Failure.Transponder1, 'XPDR 1'], [34, A380Failure.Transponder2, 'XPDR 2'], + + [46, A380Failure.NssAnsu1, 'NSS AVNCS ANSU 1'], + [46, A380Failure.NssAnsu2, 'NSS AVNCS ANSU 2'], + [46, A380Failure.FltOpsAnsu, 'FLT OPS ANSU'], + [46, A380Failure.CaptainLaptop, 'Captain Laptop'], + [46, A380Failure.FirstOfficerLaptop, 'F/O Laptop'], + [46, A380Failure.CaptainOit, 'Captain OIT'], + [46, A380Failure.FirstOfficerOit, 'F/O OIT'], ]; diff --git a/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts b/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts index 4e528df54c6..e1c224cef4a 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts +++ b/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0 import { ConsumerValue, EventBus, GameStateProvider, SimVarValueType, Subject, UnitType } from '@microsoft/msfs-sdk'; -import { Arinc429SignStatusMatrix, Arinc429Word, FmsOansData, MathUtils } from '@flybywiresim/fbw-sdk'; +import { Arinc429SignStatusMatrix, Arinc429Word, FmsData, MathUtils } from '@flybywiresim/fbw-sdk'; import { FlapConf } from '@fmgc/guidance/vnav/common'; import { FlightPlanService } from '@fmgc/index'; import { MmrRadioTuningStatus } from '@fmgc/navigation/NavaidTuner'; @@ -495,29 +495,49 @@ export class FmcAircraftInterface { return phase > FmgcFlightPhase.Cruise || (phase === FmgcFlightPhase.Cruise && isCloseToDestination); } - updateOansAirports() { - if (this.flightPlanService.hasActive) { - const pub = this.bus.getPublisher(); - if (this.flightPlanService.active?.originAirport?.ident) { - pub.pub('fmsOrigin', this.flightPlanService.active.originAirport.ident, true); - } + updateFmsData() { + const pub = this.bus.getPublisher(); + pub.pub( + 'fmsOrigin', + this.flightPlanService.hasActive && this.flightPlanService.active?.originAirport?.ident + ? this.flightPlanService.active.originAirport.ident + : null, + true, + ); - if (this.flightPlanService.active?.originRunway?.ident) { - pub.pub('fmsDepartureRunway', this.flightPlanService.active.originRunway.ident, true); - } + pub.pub( + 'fmsDepartureRunway', + this.flightPlanService.hasActive && this.flightPlanService.active?.originRunway?.ident + ? this.flightPlanService.active.originRunway.ident + : null, + true, + ); - if (this.flightPlanService.active?.destinationAirport?.ident) { - pub.pub('fmsDestination', this.flightPlanService.active.destinationAirport.ident, true); - } + pub.pub( + 'fmsDestination', + this.flightPlanService.hasActive && this.flightPlanService.active?.destinationAirport?.ident + ? this.flightPlanService.active.destinationAirport.ident + : null, + true, + ); - if (this.flightPlanService.active?.destinationRunway?.ident) { - pub.pub('fmsLandingRunway', this.flightPlanService.active.destinationRunway.ident, true); - } + pub.pub( + 'fmsLandingRunway', + this.flightPlanService.hasActive && this.flightPlanService.active?.destinationRunway?.ident + ? this.flightPlanService.active.destinationRunway.ident + : null, + true, + ); - if (this.flightPlanService.active?.alternateDestinationAirport?.ident) { - pub.pub('fmsAlternate', this.flightPlanService.active.alternateDestinationAirport.ident, true); - } - } + pub.pub( + 'fmsAlternate', + this.flightPlanService.hasActive && this.flightPlanService.active?.alternateDestinationAirport?.ident + ? this.flightPlanService.active.alternateDestinationAirport.ident + : null, + true, + ); + + pub.pub('fmsFlightNumber', this.fmgc.data.atcCallsign.get(), true); } activatePreSelSpeedMach(preSel: number) { diff --git a/fbw-a380x/src/systems/instruments/src/MFD/MFD.tsx b/fbw-a380x/src/systems/instruments/src/MFD/MFD.tsx index cd60b114481..d3cb80b7de3 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/MFD.tsx +++ b/fbw-a380x/src/systems/instruments/src/MFD/MFD.tsx @@ -27,10 +27,11 @@ import { DisplayInterface } from '@fmgc/flightplanning/interface/DisplayInterfac import { FmsErrorType } from '@fmgc/FmsError'; import { FmcServiceInterface } from 'instruments/src/MFD/FMC/FmcServiceInterface'; import { CdsDisplayUnit, DisplayUnitID } from '../MsfsAvionicsCommon/CdsDisplayUnit'; -import { InteractionMode, InternalKccuKeyEvent, MfdSimvars } from './shared/MFDSimvarPublisher'; +import { InternalKccuKeyEvent, MfdSimvars } from './shared/MFDSimvarPublisher'; import { MfdFmsPageNotAvail } from 'instruments/src/MFD/pages/FMS/MfdFmsPageNotAvail'; import './pages/common/style.scss'; +import { InteractionMode } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; export const getDisplayIndex = () => { const url = document.getElementsByTagName('a380x-mfd')[0].getAttribute('url'); diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/DestinationWindow.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/DestinationWindow.tsx index 87bd8027230..9fa5367af43 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/DestinationWindow.tsx +++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/DestinationWindow.tsx @@ -32,7 +32,7 @@ export class DestinationWindow extends DisplayComponent this.props.fmcService.master.revisedWaypointPlanIndex.get() ?? undefined, this.props.fmcService.master.revisedWaypointIsAltn.get() ?? undefined, ); - this.props.fmcService.master?.acInterface.updateOansAirports(); + this.props.fmcService.master?.acInterface.updateFmsData(); } this.props.visible.set(false); this.newDest.set(''); diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx index 2ddda72d32a..218d3bf21cb 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx +++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx @@ -156,7 +156,7 @@ export function getRevisionsMenu(fpln: MfdFmsFpln, type: FplnRevisionsMenuType): disabled: false, onPressed: () => { fpln.props.fmcService.master?.flightPlanService.enableAltn(legIndex, planIndex); - fpln.props.fmcService.master?.acInterface.updateOansAirports(); + fpln.props.fmcService.master?.acInterface.updateFmsData(); }, }, { diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/MfdFmsInit.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/MfdFmsInit.tsx index cea64df01a4..c452c762d70 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/MfdFmsInit.tsx +++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/MfdFmsInit.tsx @@ -144,6 +144,8 @@ export class MfdFmsInit extends FmsPage { } else { this.disconnectFromNetworks(); } + this.props.fmcService.master?.acInterface.updateFmsData(); + this.loadedFlightPlan?.setFlightNumber(c); }); } @@ -235,7 +237,7 @@ export class MfdFmsInit extends FmsPage { toIcao, this.altnIcao.get() ?? undefined, ); - this.props.fmcService.master?.acInterface.updateOansAirports(); + this.props.fmcService.master?.acInterface.updateFmsData(); } } @@ -250,7 +252,7 @@ export class MfdFmsInit extends FmsPage { console.error(e); logTroubleshootingError(this.props.bus, e); } - this.props.fmcService.master?.acInterface.updateOansAirports(); + this.props.fmcService.master?.acInterface.updateFmsData(); this.props.fmcService.master.fmgc.data.atcCallsign.set(this.simBriefOfp?.callsign ?? '----------'); // Don't insert weights for now, something seems broken here @@ -387,7 +389,7 @@ export class MfdFmsInit extends FmsPage { this.altnIcao.set(v); if (v) { await this.props.fmcService.master?.flightPlanService.setAlternate(v); - this.props.fmcService.master?.acInterface.updateOansAirports(); + this.props.fmcService.master?.acInterface.updateFmsData(); } }} mandatory={Subject.create(true)} diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/common/FmsPage.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/common/FmsPage.tsx index 70918485d4a..579210e5b3e 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/pages/common/FmsPage.tsx +++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/common/FmsPage.tsx @@ -188,7 +188,7 @@ export abstract class FmsPage extends DisplayCom } } - this.props.fmcService.master?.acInterface.updateOansAirports(); + this.props.fmcService.master?.acInterface.updateFmsData(); } public destroy(): void { diff --git a/fbw-a380x/src/systems/instruments/src/MFD/shared/MFDSimvarPublisher.tsx b/fbw-a380x/src/systems/instruments/src/MFD/shared/MFDSimvarPublisher.tsx index ec0095b8be4..e4915ad4d04 100644 --- a/fbw-a380x/src/systems/instruments/src/MFD/shared/MFDSimvarPublisher.tsx +++ b/fbw-a380x/src/systems/instruments/src/MFD/shared/MFDSimvarPublisher.tsx @@ -38,11 +38,6 @@ export type InternalKccuKeyEvent = { kccuKeyEvent: string; }; -export enum InteractionMode { - Touchscreen, - Kccu, -} - export enum MfdVars { coldDark = 'L:A32NX_COLD_AND_DARK_SPAWN', elec = 'L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED', diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu.tsx b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu.tsx index dcc75a188be..8f4a4f8e8f9 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu.tsx +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu.tsx @@ -14,9 +14,8 @@ import { Subscription, VNode, } from '@microsoft/msfs-sdk'; -import { InputField } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; +import { InputField, InteractionMode } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; import { DropdownFieldFormat } from 'instruments/src/MFD/pages/common/DataEntryFormats'; -import { InteractionMode } from 'instruments/src/MFD/shared/MFDSimvarPublisher'; interface DropdownMenuProps extends ComponentProps { values: SubscribableArray; diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton.tsx b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton.tsx index aabf89b551f..758f7c00b55 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton.tsx +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton.tsx @@ -18,6 +18,10 @@ interface IconButtonProps extends ComponentProps { | 'double-down' | 'double-left' | 'double-right' + | 'single-up' + | 'single-down' + | 'single-left' + | 'single-right' | 'ecl-single-up' | 'ecl-single-down' | 'ecl-check' @@ -118,6 +122,34 @@ export class IconButton extends DisplayComponent { )} + {this.props.icon === 'single-up' && ( + + + + + + )} + {this.props.icon === 'single-down' && ( + + + + + + )} + {this.props.icon === 'single-left' && ( + + + + + + )} + {this.props.icon === 'single-right' && ( + + + + + + )} {this.props.icon === 'ecl-single-up' && ( diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/InputField.tsx b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/InputField.tsx index 5dbba669b24..ff3bfeb7c29 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/InputField.tsx +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/InputField.tsx @@ -14,7 +14,11 @@ import { } from '@microsoft/msfs-sdk'; import { DataEntryFormat } from 'instruments/src/MFD/pages/common/DataEntryFormats'; import { FmsError, FmsErrorType } from '@fmgc/FmsError'; -import { InteractionMode } from 'instruments/src/MFD/shared/MFDSimvarPublisher'; + +export enum InteractionMode { + Touchscreen, + Kccu, +} interface InputFieldProps extends ComponentProps { dataEntryFormat: DataEntryFormat; @@ -48,6 +52,8 @@ interface InputFieldProps extends ComponentProps { hEventConsumer: Consumer; /** Kccu uses the HW keys, and doesn't focus input fields */ interactionMode: Subscribable; + /** Used for OIT, where placeholders are [] for mandatory fields */ + overrideEmptyMandatoryPlaceholder?: string; // inViewEvent?: Consumer; // Consider activating when we have a larger collision mesh for the screens } @@ -308,7 +314,8 @@ export class InputField extends DisplayComponent> { this.trailingUnit.set(unitTrailing ?? ''); if (this.props.mandatory?.get() && !this.props.inactive?.get() && !this.props.disabled?.get()) { - this.textInputRef.instance.innerHTML = formatted?.replace(/-/gi, '\u25AF') ?? ''; + this.textInputRef.instance.innerHTML = + formatted?.replace(/-/gi, this.props.overrideEmptyMandatoryPlaceholder ?? '\u25AF') ?? ''; } else { this.textInputRef.instance.innerText = formatted ?? ''; } diff --git a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/PageSelectorDropdownMenu.tsx b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/PageSelectorDropdownMenu.tsx index 4a832fba9c2..88ecaa13a10 100644 --- a/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/PageSelectorDropdownMenu.tsx +++ b/fbw-a380x/src/systems/instruments/src/MsfsAvionicsCommon/UiWidgets/PageSelectorDropdownMenu.tsx @@ -16,6 +16,7 @@ type PageSelectorMenuItem = { label: string; action(): void; disabled?: boolean; + separatorBelow?: boolean; }; interface PageSelectorDropdownMenuProps extends ComponentProps { @@ -25,6 +26,7 @@ interface PageSelectorDropdownMenuProps extends ComponentProps { idPrefix: string; containerStyle?: string; labelStyle?: string; + dropdownMenuStyle?: string; } export class PageSelectorDropdownMenu extends DisplayComponent { // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. @@ -108,10 +110,14 @@ export class PageSelectorDropdownMenu extends DisplayComponent + - + {this.props.label} @@ -126,13 +132,13 @@ export class PageSelectorDropdownMenu extends DisplayComponent {this.props.menuItems.map( (el, idx) => ( {el.label} diff --git a/fbw-a380x/src/systems/instruments/src/ND/OansControlPanel.tsx b/fbw-a380x/src/systems/instruments/src/ND/OansControlPanel.tsx index bf8db36a391..2c62f78ca39 100644 --- a/fbw-a380x/src/systems/instruments/src/ND/OansControlPanel.tsx +++ b/fbw-a380x/src/systems/instruments/src/ND/OansControlPanel.tsx @@ -48,14 +48,14 @@ import { Button } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/Button'; import { OansRunwayInfoBox } from './OANSRunwayInfoBox'; import { DropdownMenu } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu'; import { RadioButtonGroup } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/RadioButtonGroup'; -import { InputField } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; +import { InputField, InteractionMode } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; import { LengthFormat } from 'instruments/src/MFD/pages/common/DataEntryFormats'; import { IconButton } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton'; import { TopTabNavigator, TopTabNavigatorPage } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/TopTabNavigator'; import { Coordinates, distanceTo, placeBearingDistance } from 'msfs-geo'; import { AdirsSimVars } from 'instruments/src/MsfsAvionicsCommon/SimVarTypes'; import { NavigationDatabase, NavigationDatabaseBackend, NavigationDatabaseService } from '@fmgc/index'; -import { InteractionMode, InternalKccuKeyEvent } from 'instruments/src/MFD/shared/MFDSimvarPublisher'; +import { InternalKccuKeyEvent } from 'instruments/src/MFD/shared/MFDSimvarPublisher'; import { NDSimvars } from 'instruments/src/ND/NDSimvarPublisher'; import { Position } from '@turf/turf'; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/.eslintrc.js b/fbw-a380x/src/systems/instruments/src/OIT/.eslintrc.js new file mode 100644 index 00000000000..49eb6e1a6ed --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/.eslintrc.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + extends: ['../../../../../../.eslintrc.js', 'plugin:jsdoc/recommended-typescript-error'], + + // overrides airbnb, use sparingly + rules: { + 'react/no-unknown-property': 'off', + 'react/style-prop-object': 'off', + 'arrow-body-style': 'off', + camelcase: 'off', + }, +}; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Components/Button.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Components/Button.tsx deleted file mode 100644 index 137983c78a5..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Components/Button.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { Children, FC, isValidElement } from 'react'; -import { useHover } from 'use-events'; -import { Layer } from '@instruments/common/utils'; - -type ButtonProps = { - x?: number; - y?: number; - width?: number; - height?: number; - onClick?: () => void; - fill?: string; - disabled?: boolean; - highlighted?: boolean; - gradient?: boolean; - forceHover?: boolean; -}; -const strokeWidth = 3; -export const Button: FC = ({ - x = 0, - y = 0, - width = 0, - height = 41, - children, - onClick, - disabled, - fill, - highlighted, - gradient, - forceHover, -}) => { - const [hovered, hoverProps] = useHover(); - - return ( - - - - - {gradient && ( - <> - - - - > - )} - - - {Children.map(children, (child) => { - if (isValidElement(child) && child.type !== 'tspan') { - return child; - } - return ( - - {child} - - ); - })} - - ); -}; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Components/Dropdown.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Components/Dropdown.tsx deleted file mode 100644 index 4f18f0bd8cc..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Components/Dropdown.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import React, { - Children, - isValidElement, - ReactNode, - useState, - FC, - useRef, - useEffect, - Dispatch, - SetStateAction, -} from 'react'; -import { useHover } from 'use-events'; -import { useHistory, useRouteMatch } from 'react-router-dom'; -import { useInputManager } from '@instruments/common/input'; -import { Layer } from './Layer'; -import { Button } from './Button'; - -type DropdownProps = { - x: number; - y?: number; - width?: number; - height?: number; - fill?: string; - selectable?: boolean; - title: string | ReactNode; - dropDownWidth?: number; - active?: boolean; - disabled?: boolean; - scrollable?: boolean; - clipItems?: boolean; - maxHeight?: number; -}; -const scrollBarWidth = 15; -let lastMousePosition = 0; - -// eslint-disable-next-line max-len -export const Dropdown: FC = ({ - x, - y = 0, - width = 192, - height = 60, - dropDownWidth = 192, - fill, - selectable, - title, - children, - active, - disabled, - scrollable, - clipItems = true, - maxHeight = Infinity, -}) => { - const [open, setOpen] = useState(false); - const textRef = useRef(null); - const [textBbox, setTextBbox] = useState(); - const [scrollPosition, setScrollPosition] = useState(0); - const [clipPathId] = useState((Math.random() * 1000).toString()); - const [hovered, hoverProps] = useHover(); - const childYPositions = useRef([2]); - - Children.forEach(children, (child, index) => { - if (isValidElement(child)) { - console.log(childYPositions.current); - childYPositions.current[index + 1] = child.props.height + childYPositions.current[index]; - } - }); - - useEffect(() => setTextBbox(textRef.current?.getBBox()), [textRef]); - if (open && disabled) setOpen(false); - - let totalHeight = 1; - Children.forEach(children, (child: React.ReactElement) => { - totalHeight += child.props.height ?? 0; - }); - - return ( - - setOpen(!open)} - fill={selectable ? 'black' : '#666'} - disabled={disabled} - highlighted={open} - gradient - forceHover={hovered} - > - {selectable ? ( - - {title} - - ) : ( - <> - - - {title} - - - - > - )} - {selectable && ( - setOpen(!open)} - fill="#666" - disabled={disabled} - highlighted={open} - forceHover={hovered} - gradient - > - - - )} - - - - - {open && ( - setOpen(false)}> - - - {Children.map(children, (child, index) => { - if (isValidElement(child)) { - return React.cloneElement(child, { - y: height + childYPositions.current[index], - width: child.props.width - ? child.props.width - : (dropDownWidth ?? width) - (scrollable ? scrollBarWidth + 3 : 0), - centered: child.props.centered ?? !selectable, - }); - } - return <>>; - })} - - {scrollable && ( - - )} - - )} - - ); -}; - -export type ScrollBarProps = { - x: number; - y: number; - width?: number; - maxHeight: number; - totalChildHeight: number; - scrollPosition: number; - setScrollPosition: Dispatch>; -}; -export const ScrollBar: FC = ({ - x, - y, - width = 48, - maxHeight, - totalChildHeight, - scrollPosition, - setScrollPosition, -}) => { - const inputManager = useInputManager(); - const [dragging, setDragging] = useState(false); - const [hovered, hoverRef] = useHover(); - const handleMouseDown = (e: any) => { - inputManager.setMouseUpHandler(handleMouseUp); - inputManager.setMouseMoveHandler(handleMouseMove); - lastMousePosition = e.pageY; - }; - - const handleMouseUp = () => { - setDragging(false); - inputManager.clearHandlers(); - }; - - const handleMouseMove = (e: MouseEvent) => { - const delta = e.pageY - lastMousePosition; - setScrollPosition((p) => { - let newPos = p + delta; - newPos = Math.max(0, newPos); - newPos = Math.min(newPos, totalChildHeight - maxHeight); - return newPos; - }); - lastMousePosition = e.pageY; - }; - - return ( - - ); -}; - -type DropdownItemProps = { - y?: number; - onSelect?: () => void; - width?: number; - height?: number; - centered?: boolean; -}; - -export const DropdownItem: FC = ({ y = 0, onSelect, width = 0, height = 0, centered, children }) => { - const [hovered, hoverProps] = useHover(); - - return ( - - - - {children} - - - ); -}; - -export const DropdownLink: FC = (props) => { - const history = useHistory(); - const { path } = useRouteMatch(); - return ( - history.push(path + props.link)}> - {props.children} - - ); -}; - -export const DropdownDivider = ({ y = 0, width = 0, height = 1 }: DropdownItemProps) => ( - - - -); diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Components/Layer.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Components/Layer.tsx deleted file mode 100644 index c1fa1f25d54..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Components/Layer.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { SVGProps, FC } from 'react'; - -export const Layer: FC & { angle?: number }> = (props) => ( - - {props.children} - -); diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OIT.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OIT.tsx new file mode 100644 index 00000000000..af7b4955d07 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OIT.tsx @@ -0,0 +1,146 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { + ClockEvents, + ComponentProps, + DisplayComponent, + EventBus, + FSComponent, + MappedSubject, + SimVarValueType, + Subject, + VNode, +} from '@microsoft/msfs-sdk'; + +import './style.scss'; +import { InternalKbdKeyEvent, OitSimvars } from './OitSimvarPublisher'; +import { OitUiService, OitUriInformation } from './OitUiService'; +import { OitNotFound } from './Pages/OitNotFound'; +import { pageForUrl } from './OitPageDirectory'; +import { OitHeader } from './OitHeader'; +import { OitFooter } from './OitFooter'; +import { getDisplayIndex, OitDisplayUnit, OitDisplayUnitID } from './OitDisplayUnit'; +import { FailuresConsumer } from '@flybywiresim/fbw-sdk'; +import { OisLaptop } from './OisLaptop'; +import { InteractionMode } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; + +export interface AbstractOitPageProps extends ComponentProps { + bus: EventBus; + oit: OIT; +} + +export type OisOperationMode = 'nss' | 'flt-ops'; + +export interface OitProps { + readonly bus: EventBus; + readonly instrument: BaseInstrument; + readonly captOrFo: 'CAPT' | 'FO'; + readonly failuresConsumer: FailuresConsumer; + readonly laptop: OisLaptop; +} + +export class OIT extends DisplayComponent { + private readonly sub = this.props.bus.getSubscriber(); + + #uiService = new OitUiService(this.props.captOrFo, this.props.bus); + + get uiService() { + return this.#uiService; + } + + get laptop() { + return this.props.laptop; + } + + get laptopData() { + return this.props.laptop.data; + } + + public readonly operationMode = Subject.create('flt-ops'); + + public readonly hEventConsumer = this.props.bus.getSubscriber().on('kbdKeyEvent'); + + public readonly interactionMode = Subject.create(InteractionMode.Touchscreen); + + private readonly topRef = FSComponent.createRef(); + + private readonly displayUnitRef = FSComponent.createRef(); + + private readonly activePageRef = FSComponent.createRef(); + + private activePage: VNode = (); + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + this.uiService.activeUri.sub((uri) => { + this.activeUriChanged(uri); + }); + + MappedSubject.create( + ([uri, displayFailed, displayPowered, operationMode]) => { + // Activate EFB overlay if on charts or flt-folder page + SimVar.SetSimVarValue( + `L:A32NX_OIS_${getDisplayIndex()}_SHOW_CHARTS`, + SimVarValueType.Bool, + uri.uri === 'flt-ops/charts' && operationMode === 'flt-ops' && !displayFailed && displayPowered, + ); + SimVar.SetSimVarValue( + `L:A32NX_OIS_${getDisplayIndex()}_SHOW_OFP`, + SimVarValueType.Bool, + uri.uri === 'flt-ops/flt-folder' && operationMode === 'flt-ops' && !displayFailed && displayPowered, + ); + }, + this.uiService.activeUri, + this.displayUnitRef.instance.failed, + this.displayUnitRef.instance.powered, + this.operationMode, + ); + + this.uiService.navigateTo('flt-ops/sts'); // should be /sts + } + + private activeUriChanged(uri: OitUriInformation) { + // Remove and destroy old OIT page + if (this.activePageRef.getOrDefault()) { + while (this.activePageRef.instance.firstChild) { + this.activePageRef.instance.removeChild(this.activePageRef.instance.firstChild); + } + } + if (this.activePage && this.activePage.instance instanceof DisplayComponent) { + this.activePage.instance.destroy(); + } + + // Mapping from URL to page component + if (uri.page) { + this.activePage = pageForUrl(`${uri.sys}/${uri.page}`, this.props.bus, this); + } else { + this.activePage = pageForUrl(`${uri.sys}`, this.props.bus, this); + } + + FSComponent.render(this.activePage, this.activePageRef?.getOrDefault()); + } + + destroy(): void { + super.destroy(); + } + + render(): VNode | null { + return ( + + + + + + + + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OisInternalPublisher.ts b/fbw-a380x/src/systems/instruments/src/OIT/OisInternalPublisher.ts new file mode 100644 index 00000000000..1a942276c93 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OisInternalPublisher.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +export interface OisInternalData { + /** SYNCHRO AVIONICS is pressed (transmitted to EFB overlay) */ + synchroAvncs: boolean; +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OisLaptop.ts b/fbw-a380x/src/systems/instruments/src/OIT/OisLaptop.ts new file mode 100644 index 00000000000..2dc4dd52a82 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OisLaptop.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { ConsumerSubject, EventBus, Instrument, SimVarValueType, Subject, Subscribable } from '@microsoft/msfs-sdk'; +import { FailuresConsumer, FmsData } from '@flybywiresim/fbw-sdk'; +import { A380Failure } from '@failures'; +import { OisInternalData } from './OisInternalPublisher'; + +type LaptopIndex = 1 | 2; + +export class OisLaptop implements Instrument { + public readonly data = new OisLaptopData(); + + private readonly failureKey = this.index === 1 ? A380Failure.CaptainLaptop : A380Failure.FirstOfficerLaptop; + + private readonly powered = Subject.create(false); + + private readonly _isHealthy = Subject.create(false); + public readonly isHealthy = this._isHealthy as Subscribable; + + private readonly sub = this.bus.getSubscriber(); + + private readonly fltNumberBus = ConsumerSubject.create(this.sub.on('fmsFlightNumber'), null); + private readonly fromAirportBus = ConsumerSubject.create(this.sub.on('fmsOrigin'), null); + private readonly toAirportBus = ConsumerSubject.create(this.sub.on('fmsDestination'), null); + + public synchroAvionics() { + this.data.fltNumber.set(this.fltNumberBus.get()); + this.data.fromAirport.set(this.fromAirportBus.get()); + this.data.toAirport.set(this.toAirportBus.get()); + + // Workaround since synced messages in the same VCockpit don't work + SimVar.SetSimVarValue('L:A32NX_OIS_SYNCHRO_AVIONICS', SimVarValueType.Number, Math.random()); + } + + constructor( + private readonly bus: EventBus, + private readonly index: LaptopIndex, + private readonly failuresConsumer: FailuresConsumer, + ) {} + + /** @inheritdoc */ + init(): void { + this.failuresConsumer.register(this.failureKey); + + this.sub.on('synchroAvncs').handle(() => this.synchroAvionics()); + } + + /** @inheritdoc */ + onUpdate(): void { + const failed = this.failuresConsumer.isActive(this.failureKey); + + if (this.index === 1) { + this.powered.set(SimVar.GetSimVarValue('L:A32NX_ELEC_DC_1_BUS_IS_POWERED', SimVarValueType.Bool)); + } else { + this.powered.set( + SimVar.GetSimVarValue('L:A32NX_ELEC_DC_2_BUS_IS_POWERED', SimVarValueType.Bool) || + SimVar.GetSimVarValue('L:A32NX_ELEC_DC_HOT_2_BUS_IS_POWERED', SimVarValueType.Bool), + ); + } + + this._isHealthy.set(!failed && this.powered.get()); + SimVar.SetSimVarValue( + `L:A32NX_FLTOPS_LAPTOP_${this.index.toFixed(0)}_IS_HEALTHY`, + SimVarValueType.Bool, + this._isHealthy.get(), + ); + } +} + +export class OisLaptopData { + public readonly fltNumber = Subject.create(null); + public readonly fromAirport = Subject.create(null); + public readonly toAirport = Subject.create(null); +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OitDisplayUnit.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OitDisplayUnit.tsx new file mode 100644 index 00000000000..e5b681cc234 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OitDisplayUnit.tsx @@ -0,0 +1,299 @@ +// Copyright (c) 2025 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { + ClockEvents, + ComponentProps, + ConsumerSubject, + DisplayComponent, + EventBus, + FSComponent, + MappedSubject, + Subject, + Subscribable, + SubscribableMapFunctions, + Subscription, + VNode, +} from '@microsoft/msfs-sdk'; +import { FailuresConsumer, NXDataStore } from '@flybywiresim/fbw-sdk'; +import { AcElectricalBus, DcElectricalBus } from '@shared/electrical'; + +import './oit-display-unit.scss'; +import { OitSimvars } from './OitSimvarPublisher'; +import { A380Failure } from '@failures'; +import { OisOperationMode } from './OIT'; + +export const getDisplayIndex = () => { + const url = Array.from(document.querySelectorAll('vcockpit-panel > *')) + ?.find((it) => it.tagName.toLowerCase() !== 'wasm-instrument') + ?.getAttribute('url'); + + return url ? parseInt(url.substring(url.length - 1), 10) : 0; +}; + +export enum OitDisplayUnitID { + CaptOit, + FoOit, +} + +const DisplayUnitToDCBus: { [k in OitDisplayUnitID]: (DcElectricalBus | AcElectricalBus)[] } = { + [OitDisplayUnitID.CaptOit]: [AcElectricalBus.Ac2, DcElectricalBus.Dc2], + [OitDisplayUnitID.FoOit]: [AcElectricalBus.AcEss, DcElectricalBus.DcEssInFlight, DcElectricalBus.Dc1], +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const DisplayUnitToPotentiometer: { [k in OitDisplayUnitID]: number } = { + [OitDisplayUnitID.CaptOit]: 81, + [OitDisplayUnitID.FoOit]: 82, +}; + +interface DisplayUnitProps { + readonly bus: EventBus; + readonly displayUnitId: OitDisplayUnitID; + readonly failuresConsumer: FailuresConsumer; + readonly nssOrFltOps: Subscribable; + readonly test?: Subscribable; +} + +enum DisplayUnitState { + On, + Off, + Bootup, + Selftest, + Standby, + Failed, +} + +export class OitDisplayUnit extends DisplayComponent { + private readonly subs: Subscription[] = []; + + private readonly sub = this.props.bus.getSubscriber(); + + private readonly state = Subject.create( + SimVar.GetSimVarValue('L:A32NX_COLD_AND_DARK_SPAWN', 'Bool') ? DisplayUnitState.Off : DisplayUnitState.Standby, + ); + + private readonly failureKey = + this.props.displayUnitId === OitDisplayUnitID.CaptOit ? A380Failure.CaptainOit : A380Failure.FirstOfficerOit; + + private timeOut: number = 0; + + private readonly selfTestRef = FSComponent.createRef(); + + private oitRef = FSComponent.createRef(); + + public readonly powered = Subject.create(false); + + /** in seconds */ + private readonly remainingStartupTime = Subject.create(24); + private readonly totalStartupTime = Subject.create(24); + + private readonly progressBarFillWidth = MappedSubject.create( + ([remaining, total]) => (1 - remaining / total) * 40, + this.remainingStartupTime, + this.totalStartupTime, + ).map((t) => `${t}%`); + + private readonly brightness = ConsumerSubject.create( + this.sub.on(this.props.displayUnitId === OitDisplayUnitID.CaptOit ? 'potentiometerCaptain' : 'potentiometerFo'), + 75, + ); + + private readonly nssAnsu1Failed = ConsumerSubject.create(this.sub.on('nssAnsu1Healthy'), true).map( + SubscribableMapFunctions.not(), + ); + + private readonly nssAnsu2Failed = ConsumerSubject.create(this.sub.on('nssAnsu2Healthy'), true).map( + SubscribableMapFunctions.not(), + ); + + private readonly allNssAnsuFailed = MappedSubject.create( + SubscribableMapFunctions.and(), + this.nssAnsu1Failed, + this.nssAnsu2Failed, + ); + + private readonly fltOpsAnsuFailed = ConsumerSubject.create(this.sub.on('fltOpsAnsu1Healthy'), true).map( + SubscribableMapFunctions.not(), + ); + + private readonly fltOpsLaptopFailed = ConsumerSubject.create( + this.sub.on(this.props.displayUnitId === OitDisplayUnitID.CaptOit ? 'laptopCaptHealthy' : 'laptopFoHealthy'), + true, + ).map(SubscribableMapFunctions.not()); + + private readonly fltOpsFailed = MappedSubject.create( + SubscribableMapFunctions.or(), + this.fltOpsAnsuFailed, + this.fltOpsLaptopFailed, + ); + + public readonly failed = MappedSubject.create( + ([opMode, state, nssFail, fltOpsFail]) => + state === DisplayUnitState.On && ((opMode === 'nss' && nssFail) || (opMode === 'flt-ops' && fltOpsFail)), + this.props.nssOrFltOps, + this.state, + this.allNssAnsuFailed, + this.fltOpsFailed, + ); + + private readonly failedDisplay = this.failed.map((v) => (v ? 'block' : 'none')); + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + this.props.failuresConsumer.register(this.failureKey); + + this.subs.push(this.sub.on('realTime').handle(() => this.update())); + this.subs.push( + this.sub + .on('realTime') + .atFrequency(4) + .handle(() => { + // override MSFS menu animations setting for this instrument + if (!document.documentElement.classList.contains('animationsEnabled')) { + document.documentElement.classList.add('animationsEnabled'); + } + + // Update loading progress bar + if (this.powered.get()) { + this.remainingStartupTime.set(Math.max(0, this.remainingStartupTime.get() - 0.25)); + } + }), + ); + + this.subs.push( + MappedSubject.create( + () => { + this.updateState(); + }, + this.brightness, + this.powered, + this.failed, + ), + ); + + this.subs.push( + this.progressBarFillWidth, + this.brightness, + this.nssAnsu1Failed, + this.nssAnsu2Failed, + this.allNssAnsuFailed, + this.fltOpsAnsuFailed, + this.fltOpsLaptopFailed, + this.fltOpsFailed, + this.failed, + this.failedDisplay, + ); + } + + setTimer(time: number) { + this.timeOut = window.setTimeout(() => { + if (this.state.get() === DisplayUnitState.Standby) { + this.state.set(DisplayUnitState.Off); + } + if (this.state.get() === DisplayUnitState.Selftest) { + this.state.set(DisplayUnitState.On); + } + this.updateState(); + }, time * 1000); + } + + public update() { + const poweredByBus1 = DisplayUnitToDCBus[this.props.displayUnitId][0] + ? SimVar.GetSimVarValue(`L:A32NX_ELEC_${DisplayUnitToDCBus[this.props.displayUnitId][0]}_BUS_IS_POWERED`, 'Bool') + : true; + const poweredByBus2 = DisplayUnitToDCBus[this.props.displayUnitId][1] + ? SimVar.GetSimVarValue(`L:A32NX_ELEC_${DisplayUnitToDCBus[this.props.displayUnitId][1]}_BUS_IS_POWERED`, 'Bool') + : true; + const poweredByBus3 = DisplayUnitToDCBus[this.props.displayUnitId][2] + ? SimVar.GetSimVarValue(`L:A32NX_ELEC_${DisplayUnitToDCBus[this.props.displayUnitId][2]}_BUS_IS_POWERED`, 'Bool') + : true; + this.powered.set( + (poweredByBus1 || poweredByBus2 || poweredByBus3) && !this.props.failuresConsumer.isActive(this.failureKey), + ); + } + + updateState() { + if (this.state.get() === DisplayUnitState.On && (this.brightness.get() === 0 || !this.powered.get())) { + this.state.set(DisplayUnitState.Standby); + this.setTimer(10); + } else if (this.state.get() === DisplayUnitState.Standby && this.brightness.get() !== 0 && this.powered.get()) { + this.state.set(DisplayUnitState.On); + clearTimeout(this.timeOut); + } else if (this.state.get() === DisplayUnitState.Off && this.brightness.get() !== 0 && this.powered.get()) { + this.state.set(DisplayUnitState.Bootup); + this.setTimer(0.25 + Math.random() * 0.2); + } else if (this.state.get() === DisplayUnitState.Bootup && this.brightness.get() !== 0 && this.powered.get()) { + this.state.set(DisplayUnitState.Selftest); + this.totalStartupTime.set(parseInt(NXDataStore.get('CONFIG_SELF_TEST_TIME', '12')) * 2); + this.remainingStartupTime.set(this.totalStartupTime.get()); + this.setTimer(this.remainingStartupTime.get()); + } else if ( + (this.state.get() === DisplayUnitState.Selftest || this.state.get() === DisplayUnitState.Bootup) && + (this.brightness.get() === 0 || !this.powered.get()) + ) { + this.state.set(DisplayUnitState.Off); + clearTimeout(this.timeOut); + } + + if (this.state.get() === DisplayUnitState.Selftest) { + this.selfTestRef.instance.style.display = 'block'; + this.oitRef.instance.style.display = 'none'; + } else if (this.state.get() === DisplayUnitState.Bootup) { + this.selfTestRef.instance.style.display = 'block'; + this.oitRef.instance.style.display = 'none'; + } else if (this.state.get() === DisplayUnitState.On) { + this.selfTestRef.instance.style.display = 'none'; + this.oitRef.instance.style.display = 'block'; + } else if (this.state.get() === DisplayUnitState.Failed) { + this.selfTestRef.instance.style.display = 'none'; + this.oitRef.instance.style.display = 'none'; + } else { + this.selfTestRef.instance.style.display = 'none'; + this.oitRef.instance.style.display = 'none'; + } + } + + destroy(): void { + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + <> + + + + + + + + {this.props.children} + + + + + Not available. + + + > + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OitFooter.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OitFooter.tsx new file mode 100644 index 00000000000..924a84343de --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OitFooter.tsx @@ -0,0 +1,56 @@ +import { DisplayComponent, FSComponent, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { Button } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/Button'; +import { IconButton } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton'; +import { OIT } from './OIT'; +import { OitUiService } from './OitUiService'; + +interface OiFooterHeaderProps { + uiService: OitUiService; + oit: OIT; +} + +/* + * Complete header for the ATCCOM system + */ +export abstract class OitFooter extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + protected readonly subs = [] as Subscription[]; + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + + + this.props.oit.uiService.navigateTo('flt-ops/sts')} + buttonStyle="width: 225px; font-size: 28px; height: 50px;" + /> + this.props.oit.uiService.navigateTo('flt-ops/charts')} + buttonStyle="width: 225px; font-size: 28px; height: 50px;" + /> + this.props.oit.uiService.navigateTo('flt-ops/flt-folder')} + buttonStyle="width: 225px; font-size: 28px; height: 50px;" + /> + + + + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OitHeader.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OitHeader.tsx new file mode 100644 index 00000000000..90ef0b62277 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OitHeader.tsx @@ -0,0 +1,144 @@ +import { DisplayComponent, FSComponent, Subject, SubscribableUtils, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { PageSelectorDropdownMenu } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/PageSelectorDropdownMenu'; +import { Button } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/Button'; +import { IconButton } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/IconButton'; +import { OIT } from './OIT'; +import { OitUiService } from './OitUiService'; + +interface OitHeaderHeaderProps { + uiService: OitUiService; + oit: OIT; +} + +/* + * Complete header for the ATCCOM system + */ +export abstract class OitHeader extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + protected readonly subs = [] as Subscription[]; + + private readonly oitHeading = this.props.uiService.activeUri.map((uri) => heading[uri.uri] ?? 'FIXME'); + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + this.subs.push(this.oitHeading); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + + this.props.uiService.navigateTo('flt-ops'), separatorBelow: true }, + { + label: 'FLT FOLDER', + action: () => this.props.uiService.navigateTo('flt-ops/flt-folder'), + }, + { + label: 'TERML CHART', + action: () => this.props.uiService.navigateTo('flt-ops/charts'), + separatorBelow: true, + }, + { + label: 'OPS LIBRARY', + action: () => this.props.uiService.navigateTo('flt-ops/ops-library'), + disabled: true, + separatorBelow: true, + }, + { label: 'T.O PERF', action: () => this.props.uiService.navigateTo('flt-ops/to-perf'), disabled: true }, + { label: 'LOADSHEET', action: () => this.props.uiService.navigateTo('flt-ops/loadsheet'), disabled: true }, + { label: 'LDG PERF', action: () => this.props.uiService.navigateTo('flt-ops/ldg-perf'), disabled: true }, + { + label: 'IN-FLT PERF', + action: () => this.props.uiService.navigateTo('flt-ops/in-flt-perf'), + disabled: true, + separatorBelow: true, + }, + { label: 'FLT OPS STS', action: () => this.props.uiService.navigateTo('flt-ops/sts') }, + { label: 'LOAD BOX', action: () => this.props.uiService.navigateTo('flt-ops/load-box'), disabled: true }, + { + label: 'EXPORT BOX', + action: () => this.props.uiService.navigateTo('flt-ops/export-box'), + disabled: true, + separatorBelow: true, + }, + { + label: 'EXIT SESSION', + action: () => this.props.uiService.navigateTo('flt-ops/exit-session'), + disabled: true, + }, + ]} + idPrefix={`${this.props.uiService.captOrFo}_OIT_menu_menu`} + dropdownMenuStyle="width: 300px;" + /> + {this.oitHeading} + + { + if (this.props.oit.operationMode.get() === 'flt-ops') { + this.props.uiService.navigateTo('flt-ops'); + } + }, + }, + { + label: 'PREVIOUS', + action: () => { + if (this.props.uiService.canGoBack()) { + this.props.uiService.navigateTo('back'); + } + }, + }, + { label: 'NEXT', action: () => {}, disabled: true, separatorBelow: true }, + { label: 'PRINT', action: () => {}, disabled: true }, + { label: 'STORE', action: () => {}, disabled: true }, + { label: 'UPDATE', action: () => {}, disabled: true, separatorBelow: true }, + { label: 'UNDO', action: () => {}, disabled: true }, + { label: 'REDO', action: () => {}, disabled: true, separatorBelow: true }, + { label: 'HELP', action: () => {}, disabled: true, separatorBelow: true }, + { label: 'HIDE SWITCHING BAR', action: () => {}, disabled: true, separatorBelow: true }, + { label: 'CLOSE APPLICATION', action: () => {}, disabled: true, separatorBelow: true }, + ]} + idPrefix={`${this.props.uiService.captOrFo}_OIT_menu_functions`} + dropdownMenuStyle="width: 300px;" + /> + 0 MSG + + + {}} + buttonStyle="font-size: 28px; height: 60px;" + disabled={Subject.create(true)} + /> + + ); + } +} + +const heading: Record = { + 'flt-ops': 'FLT OPS MENU', + 'flt-ops/flt-folder': 'FLT FOLDER', + 'flt-ops/charts': 'CHARTS', + 'flt-ops/sts': 'STATUS PAGE', + 'flt-ops/to-perf': 'T.O PERF', + 'flt-ops/ldg-perf': 'LDG PERF', + 'flt-ops/load-box': 'LOAD BOX', + 'flt-ops/export-box': 'EXPORT BOX', +}; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OitPageDirectory.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OitPageDirectory.tsx new file mode 100644 index 00000000000..83331b9aeed --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OitPageDirectory.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, FSComponent, VNode } from '@microsoft/msfs-sdk'; +import { OIT } from './OIT'; +import { OitFltOpsEfbOverlay } from './Pages/flt-ops/OitFltOpsEfbOverlay'; +import { OitFltOpsMenuPage } from './Pages/flt-ops/OitFltOpsMenuPage'; +import { OitFltOpsPerformance } from './Pages/flt-ops/OitFltOpsPerformance'; +import { OitFltOpsStatus } from './Pages/flt-ops/OitFltOpsStatus'; +import { OitNotFound } from './Pages/OitNotFound'; + +// Page imports +// eslint-disable-next-line jsdoc/require-jsdoc +export function pageForUrl(url: string, bus: EventBus, oit: OIT): VNode { + switch (url) { + case 'flt-ops': + return ; + case 'flt-ops/sts': + return ; + case 'flt-ops/to-perf': + return ; + case 'flt-ops/ldg-perf': + return ; + case 'flt-ops/charts': + return ; + case 'flt-ops/flt-folder': + return ; + + default: + return ; + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OitSimvarPublisher.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OitSimvarPublisher.tsx new file mode 100644 index 00000000000..e2e9f5def6e --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OitSimvarPublisher.tsx @@ -0,0 +1,53 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, SimVarDefinition, SimVarPublisher, SimVarValueType } from '@microsoft/msfs-sdk'; + +export type OitSimvars = { + coldDark: number; + elec: boolean; + elecFo: boolean; + potentiometerCaptain: number; + potentiometerFo: number; + nssAnsu1Healthy: boolean; + nssAnsu2Healthy: boolean; + fltOpsAnsu1Healthy: boolean; + laptopCaptHealthy: boolean; + laptopFoHealthy: boolean; +}; + +export type InternalKbdKeyEvent = { + kbdKeyEvent: string; +}; + +export enum OitVars { + coldDark = 'L:A32NX_COLD_AND_DARK_SPAWN', + elec = 'L:A32NX_ELEC_AC_2_BUS_IS_POWERED', + elecFo = 'L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED', + potentiometerCaptain = 'LIGHT POTENTIOMETER:81', + potentiometerFo = 'LIGHT POTENTIOMETER:82', + nssAnsu1Healthy = 'L:A32NX_NSS_ANSU_1_IS_HEALTHY', + nssAnsu2Healthy = 'L:A32NX_NSS_ANSU_2_IS_HEALTHY', + fltOpsAnsu1Healthy = 'L:A32NX_FLTOPS_ANSU_1_IS_HEALTHY', + laptopCaptHealthy = 'L:A32NX_FLTOPS_LAPTOP_1_IS_HEALTHY', + laptopFoHealthy = 'L:A32NX_FLTOPS_LAPTOP_2_IS_HEALTHY', +} + +/** A publisher to poll and publish nav/com simvars. */ +export class OitSimvarPublisher extends SimVarPublisher { + private static simvars = new Map([ + ['elec', { name: OitVars.elec, type: SimVarValueType.Bool }], + ['elecFo', { name: OitVars.elecFo, type: SimVarValueType.Bool }], + ['potentiometerCaptain', { name: OitVars.potentiometerCaptain, type: SimVarValueType.Number }], + ['potentiometerFo', { name: OitVars.potentiometerFo, type: SimVarValueType.Number }], + ['nssAnsu1Healthy', { name: OitVars.nssAnsu1Healthy, type: SimVarValueType.Bool }], + ['nssAnsu2Healthy', { name: OitVars.nssAnsu2Healthy, type: SimVarValueType.Bool }], + ['fltOpsAnsu1Healthy', { name: OitVars.fltOpsAnsu1Healthy, type: SimVarValueType.Bool }], + ['laptopCaptHealthy', { name: OitVars.laptopCaptHealthy, type: SimVarValueType.Bool }], + ['laptopFoHealthy', { name: OitVars.laptopFoHealthy, type: SimVarValueType.Bool }], + ]); + + public constructor(bus: EventBus) { + super(OitSimvarPublisher.simvars, bus); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OitUiService.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OitUiService.tsx new file mode 100644 index 00000000000..eeddeee4bf8 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/OitUiService.tsx @@ -0,0 +1,91 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, Subject } from '@microsoft/msfs-sdk'; + +export enum OitSystem { + None = '', + Nss = 'nss', + FltOps = 'flt-ops', +} + +/* + * Hierarchy of URIs: sys / category / page / extra + */ +export interface OitUriInformation { + uri: string; // Full URI to OIT page, e.g. FLT-OPS/TO-PERF + sys: OitSystem; // 1st part of URI (system), can be nss or flt-ops + page: string; // page within system + extra?: string; // Can be used for deep-linking within a page +} + +/* + * Handles navigation (and potentially other aspects) for OIT pages + */ +export class OitUiService { + constructor( + public captOrFo: 'CAPT' | 'FO', + private readonly bus: EventBus, + ) {} + + public readonly activeUri = Subject.create({ + uri: '', + sys: OitSystem.None, + page: '', + extra: '', + }); + + private navigationStack: string[] = []; + + public parseUri(uri: string): OitUriInformation { + const uriParts = uri.split('/'); + return { + uri, + sys: uriParts[0] as OitSystem, + page: uriParts[1], + extra: uriParts.slice(2).join('/'), + }; + } + + /** + * Navigate to OIT page. + * @param uri The URI to navigate to. Format: sys/category/page, e.g. fms/active/init represents ACTIVE/INIT page from the FMS. Use URI 'back' for returning to previous page. + * In theory, one can use anything after a third slash for intra-page deep linking: fms/active/perf/appr could link to the approach PERF page. + */ + public navigateTo(uri: string): void { + let nextUri: string; + + const forceReloadUrls: string[] = []; + if (uri === this.activeUri.get().uri && !forceReloadUrls.includes(uri)) { + // Same URL, don't navigate. Except for some URLs defined in forceReloadUrls + console.info('Navigate to same URL, ignored.'); + return; + } + + // Before navigating, make sure that all input fields are un-focused + Coherent.trigger('UNFOCUS_INPUT_FIELD'); + + if (uri === 'back') { + if (this.navigationStack.length < 2) { + return; + } + console.info('Navigate back'); + this.navigationStack.pop(); + nextUri = this.navigationStack[this.navigationStack.length - 1]; + } else { + console.info(`Navigate to ${uri}`); + this.navigationStack.push(uri); + nextUri = uri; + } + + const parsedUri = this.parseUri(nextUri); + this.activeUri.set(parsedUri); + } + + /* + * Whether one can navigate back + */ + public canGoBack() { + return this.navigationStack.length > 1; + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/OnboardInformationTerminal.tsx b/fbw-a380x/src/systems/instruments/src/OIT/OnboardInformationTerminal.tsx deleted file mode 100644 index 762bd7a5c3c..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/OnboardInformationTerminal.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { createContext, useContext } from 'react'; -import { useHistory, useRouteMatch, Redirect, Route, Switch } from 'react-router-dom'; -import { Pages } from './Pages'; - -import './index.scss'; -import { Dropdown, DropdownDivider, DropdownItem, DropdownLink } from './Components/Dropdown'; -import { Button } from './Components/Button'; - -enum OITDisplayPosition { - Captain = 'CAPTAIN', - FO = 'F/O', - Backup = 'BACKUP', -} -type OITContextType = { - displayPosition: OITDisplayPosition; -}; - -export const OITContext = createContext(undefined as any); - -export const useOITContext = () => useContext(OITContext); - -export const OnboardInformationTerminal: React.FC = () => { - const history = useHistory(); - const { path } = useRouteMatch(); - return ( - - - - - {/* fix prefixes for this */} - - {history.location.pathname.toUpperCase().substring(path.length)} - - - - - - - ); -}; - -const PagesContainer: React.FC = () => ( - - - - - - - - - - - -); - -const MenuDropdown: React.FC = () => ( - <> - - - FLT OPS MENU - - - - - - FLT FOLDER - - - TERML CHART - - - OPS LIBRARY - - - - - - T.O PERF - - - LOADSHEET - - - LDG PERF - - - IN-FLT PERF - - - - - - FLT OPS STS - - - LOAD BOX - - - EXPORT BOX - - - - - - EXIT SESSION - - - > -); - -const FunctionDropdown: React.FC = () => ( - <> - - - HOME - - - PREVIOUS - - - NEXT - - - - - - PRINT - - - STORE - - - UPDATE - - - - - - UNDO - - - REDO - - - - - - HELP - - - - - - HIDE SWITCHING BAR - - - - - - CLOSE APPLICATION - - - > -); - -const MessageDropdown: React.FC = () => ( - <> - - 0 MSG - - - - MESSAGE - - - - CLEAR - - > -); diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/LoadBox.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/LoadBox.tsx deleted file mode 100644 index 42c5b5297d6..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/LoadBox.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { FC } from 'react'; - -export const LoadBoxPage: FC = () => { - return ( - <> - - LOAD BOX - - > - ); -}; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/Login.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/Login.tsx deleted file mode 100644 index 4b85242516c..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/Login.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { FC } from 'react'; -import { useOITContext } from '../../OnboardInformationTerminal'; -import { Button } from '../../Components/Button'; - -export const LoginPage: FC = () => { - const { displayPosition } = useOITContext(); - return ( - <> - - FLT OPS Domain - - - Login Page - - - {displayPosition} - - - PILOT - - - MAINTAINER - - - SWITCH OFF - LAPTOP - - > - ); -}; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/Menu.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/Menu.tsx deleted file mode 100644 index 25b4b3125f4..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/Menu.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { FC } from 'react'; - -export const MenuPage: FC = () => { - return ( - <> - - FLT OPS MENU - - > - ); -}; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/STS.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/STS.tsx deleted file mode 100644 index e9e37164cbe..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Pages/FlightOps/STS.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { FC } from 'react'; -import { Button } from '../../Components/Button'; -import { Dropdown, DropdownItem } from '../../Components/Dropdown'; - -export const STSPage: FC = () => { - return ( - <> - {/* Main Section */} - - - - - - - - - - - > - ); -}; - -const AircraftInfo: React.FC = () => ( - <> - - ACFT REGISTRATION - - - - - idk what goes here - - - - - SYNCHRO - AVIONICS - - - - MSN - - - 9804 - - - - OIS VERSION - - - 14-JUL-08 V2.0 - - > -); - -const NavInfo: React.FC = () => ( - <> - - ACTIVE - - - CHARTS - - - 07-AUG-08 27-AUG-08 - - > -); - -const FlightInfo: React.FC = () => ( - <> - - FLT NBR - - - - AIB123 - - - {/* The style for this dropdown is not finished, don't have enough references to be able to correctly stylize it */} - - FROM - - - - - TO - - - > -); diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/OitNotFound.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/OitNotFound.tsx new file mode 100644 index 00000000000..825fee52e93 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/Pages/OitNotFound.tsx @@ -0,0 +1,37 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { DisplayComponent, FSComponent, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { AbstractOitPageProps } from '../OIT'; + +interface OitNotFoundProps extends AbstractOitPageProps {} + +export class OitNotFound extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + private readonly subs = [] as Subscription[]; + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + new Promise((resolve) => setTimeout(resolve, 500)).then(() => this.props.oit.uiService.navigateTo('back')); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + <> + {/* begin page content */} + + {/* end page content */} + > + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsEfbOverlay.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsEfbOverlay.tsx new file mode 100644 index 00000000000..9dee4f667ce --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsEfbOverlay.tsx @@ -0,0 +1,40 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { DisplayComponent, FSComponent, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { AbstractOitPageProps } from '../../OIT'; + +interface OitFltOpsEfbOverlayPageProps extends AbstractOitPageProps {} + +export class OitFltOpsEfbOverlay extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + private readonly subs = [] as Subscription[]; + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + <> + {/* begin page content */} + + + EFB OVERLAY FAILED + + Please reload aircraft after linking Navigraph through the EFB. + + {/* end page content */} + > + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsMenuPage.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsMenuPage.tsx new file mode 100644 index 00000000000..98b7e838288 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsMenuPage.tsx @@ -0,0 +1,115 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { DisplayComponent, FSComponent, Subject, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { Button } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/Button'; +import { AbstractOitPageProps } from '../../OIT'; + +interface OitFltOpsMenuPageProps extends AbstractOitPageProps {} + +export class OitFltOpsMenuPage extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + private readonly subs = [] as Subscription[]; + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + <> + {/* begin page content */} + + + + MISSION + this.props.oit.uiService.navigateTo('flt-ops/flt-folder')} + /> + this.props.oit.uiService.navigateTo('flt-ops/charts')} + /> + + + DOCUMENTATION + this.props.oit.uiService.navigateTo('flt-ops/ops-library')} + disabled={Subject.create(true)} + /> + + + PERFORMANCE + this.props.oit.uiService.navigateTo('flt-ops/to-perf')} + disabled={Subject.create(true)} + /> + this.props.oit.uiService.navigateTo('flt-ops/loadsheet')} + disabled={Subject.create(true)} + /> + this.props.oit.uiService.navigateTo('flt-ops/ldg-perf')} + disabled={Subject.create(true)} + /> + this.props.oit.uiService.navigateTo('flt-ops/in-flt-perf')} + disabled={Subject.create(true)} + /> + + + UTILITIES + this.props.oit.uiService.navigateTo('flt-ops/sts')} + /> + this.props.oit.uiService.navigateTo('flt-ops/load-box')} + disabled={Subject.create(true)} + /> + this.props.oit.uiService.navigateTo('flt-ops/export-box')} + disabled={Subject.create(true)} + /> + + + + {}} + disabled={Subject.create(true)} + /> + + {/* end page content */} + > + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsPerformance.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsPerformance.tsx new file mode 100644 index 00000000000..9eb27362f85 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsPerformance.tsx @@ -0,0 +1,39 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { DisplayComponent, FSComponent, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { AbstractOitPageProps } from '../../OIT'; + +interface OitFltOpsPerformancePageProps extends AbstractOitPageProps {} + +export class OitFltOpsPerformance extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + private readonly subs = [] as Subscription[]; + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + <> + {/* begin page content */} + + + NOT YET IMPLEMENTED + + + {/* end page content */} + > + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsStatus.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsStatus.tsx new file mode 100644 index 00000000000..faa52569972 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/Pages/flt-ops/OitFltOpsStatus.tsx @@ -0,0 +1,122 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { ArraySubject, DisplayComponent, FSComponent, Subject, Subscription, VNode } from '@microsoft/msfs-sdk'; +import { AbstractOitPageProps } from '../../OIT'; +import { DropdownMenu } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu'; +import { InputField } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField'; +import { AirportFormat, LongAlphanumericFormat } from 'instruments/src/MFD/pages/common/DataEntryFormats'; +import { Button } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/Button'; +import { OisInternalData } from '../../OisInternalPublisher'; + +interface OitFltOpsStatusPageProps extends AbstractOitPageProps {} + +export class OitFltOpsStatus extends DisplayComponent { + // Make sure to collect all subscriptions here, otherwise page navigation doesn't work. + private readonly subs = [] as Subscription[]; + + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + } + + public destroy(): void { + // Destroy all subscriptions to remove all references to this instance. + for (const s of this.subs) { + s.destroy(); + } + + super.destroy(); + } + + render(): VNode { + return ( + <> + {/* begin page content */} + + + + ACFT REGISTRATION + + (0)} + values={ArraySubject.create(['F-FBWA'])} + freeTextAllowed={false} + containerStyle="width: 600px;" + alignLabels="center" + numberOfDigitsForInputField={6} + tmpyActive={Subject.create(false)} + hEventConsumer={this.props.oit.hEventConsumer} + interactionMode={this.props.oit.interactionMode} + /> + + this.props.bus.getPublisher().pub('synchroAvncs', true, true, false)} + containerStyle="width: 175px;" + /> + + + + + MSN + 225 + + + OIS VERSION + 20-JAN 25 V1.0 + + + + + FLT NUMBER + + + dataEntryFormat={new LongAlphanumericFormat()} + mandatory={Subject.create(true)} + value={this.props.oit.laptopData.fltNumber} + containerStyle="width: 600px; margin-right: 5px;" + alignText="center" + hEventConsumer={this.props.oit.hEventConsumer} + interactionMode={this.props.oit.interactionMode} + overrideEmptyMandatoryPlaceholder="[]" + /> + + + + + FROM + + + dataEntryFormat={new AirportFormat()} + mandatory={Subject.create(true)} + value={this.props.oit.laptopData.fromAirport} + containerStyle="width: 250px; margin-right: 5px;" + alignText="center" + hEventConsumer={this.props.oit.hEventConsumer} + interactionMode={this.props.oit.interactionMode} + overrideEmptyMandatoryPlaceholder="[]" + /> + + TO + + + dataEntryFormat={new AirportFormat()} + mandatory={Subject.create(true)} + value={this.props.oit.laptopData.toAirport} + containerStyle="width: 250px; margin-right: 5px;" + alignText="center" + hEventConsumer={this.props.oit.hEventConsumer} + interactionMode={this.props.oit.interactionMode} + overrideEmptyMandatoryPlaceholder="[]" + /> + + + + + + {/* end page content */} + > + ); + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/Pages/index.tsx b/fbw-a380x/src/systems/instruments/src/OIT/Pages/index.tsx deleted file mode 100644 index 0c2aef562df..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/Pages/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { LoginPage } from './FlightOps/Login'; -import { STSPage } from './FlightOps/STS'; -import { MenuPage } from './FlightOps/Menu'; -import { LoadBoxPage } from './FlightOps/LoadBox'; - -export const Pages = { LoginPage, STSPage, MenuPage, LoadBoxPage }; diff --git a/fbw-a380x/src/systems/instruments/src/OIT/config.json b/fbw-a380x/src/systems/instruments/src/OIT/config.json index dd22b8a2bdb..df67f160af9 100644 --- a/fbw-a380x/src/systems/instruments/src/OIT/config.json +++ b/fbw-a380x/src/systems/instruments/src/OIT/config.json @@ -1,8 +1,5 @@ { - "index": "./index.tsx", - "isInteractive": true, - "dimensions": { - "width": 1024, - "height": 768 - } + "index": "./instrument.tsx", + "name": "OIT", + "isInteractive": true } diff --git a/fbw-a380x/src/systems/instruments/src/OIT/index.scss b/fbw-a380x/src/systems/instruments/src/OIT/index.scss deleted file mode 100644 index 6f83e3816b4..00000000000 --- a/fbw-a380x/src/systems/instruments/src/OIT/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -text { - font-family: "Helvetica"; -} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/instrument.tsx b/fbw-a380x/src/systems/instruments/src/OIT/instrument.tsx new file mode 100644 index 00000000000..f8b97ea6740 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/instrument.tsx @@ -0,0 +1,129 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { + FSComponent, + EventBus, + HEventPublisher, + InstrumentBackplane, + FsInstrument, + FsBaseInstrument, + ClockPublisher, +} from '@microsoft/msfs-sdk'; +import { FailuresConsumer } from '@flybywiresim/fbw-sdk'; +import { OIT } from './OIT'; +import { OitSimvarPublisher } from './OitSimvarPublisher'; +import { OisLaptop } from './OisLaptop'; + +class OitInstrument implements FsInstrument { + private readonly bus = new EventBus(); + + private readonly backplane = new InstrumentBackplane(); + + private readonly clockPublisher = new ClockPublisher(this.bus); + + private readonly oitSimvarPublisher = new OitSimvarPublisher(this.bus); + + private readonly hEventPublisher = new HEventPublisher(this.bus); + + private readonly failuresConsumer = new FailuresConsumer('A32NX'); + + private readonly laptop = new OisLaptop( + this.bus, + this.instrument.instrumentIndex === 1 ? 1 : 2, + this.failuresConsumer, + ); + + constructor(public readonly instrument: BaseInstrument) { + this.hEventPublisher = new HEventPublisher(this.bus); + + this.backplane.addPublisher('hEvent', this.hEventPublisher); + this.backplane.addPublisher('clock', this.clockPublisher); + this.backplane.addPublisher('oitSimvar', this.oitSimvarPublisher); + this.backplane.addInstrument('Laptop', this.laptop); + + this.doInit(); + } + + public doInit(): void { + this.backplane.init(); + + const oit = document.getElementById('OIT_CONTENT'); + + FSComponent.render( + , + document.getElementById('OIT_CONTENT'), + ); + + // Remove "instrument didn't load" text + oit?.querySelector(':scope > h1')?.remove(); + } + + /** + * A callback called when the instrument gets a frame update. + */ + public Update(): void { + this.backplane.onUpdate(); + this.failuresConsumer.update(); + } + + public onInteractionEvent(args: string[]): void { + this.hEventPublisher.dispatchHEvent(args[0]); + } + + public onGameStateChanged(_oldState: GameState, _newState: GameState): void { + // noop + } + + public onFlightStart(): void { + // noop + } + + public onSoundEnd(_soundEventId: Name_Z): void { + // noop + } + + public onPowerOn(): void { + // noop + } + + public onPowerOff(): void { + // noop + } +} + +class A380X_OIT extends FsBaseInstrument { + public constructInstrument(): OitInstrument { + return new OitInstrument(this); + } + + public get isInteractive(): boolean { + return true; + } + + public get templateID(): string { + return 'A380X_OIT'; + } + + /** @inheritdoc */ + public onPowerOn(): void { + super.onPowerOn(); + + this.fsInstrument.onPowerOn(); + } + + /** @inheritdoc */ + public onShutDown(): void { + super.onShutDown(); + + this.fsInstrument.onPowerOff(); + } +} + +registerInstrument('a380x-oit', A380X_OIT); diff --git a/fbw-a380x/src/systems/instruments/src/OIT/oit-display-unit.scss b/fbw-a380x/src/systems/instruments/src/OIT/oit-display-unit.scss new file mode 100644 index 00000000000..5eade557883 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/oit-display-unit.scss @@ -0,0 +1,66 @@ + +@import "../MsfsAvionicsCommon/definitions.scss"; + +@font-face { + font-family: "OIT"; + //noinspection CssUnknownTarget + src: url("/Fonts/fbw-a380x/EFB/Inter-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +.AnsuFailed { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 100%; + border: none; + background-color: $display-background; +} + +.AnsuFailedText { + font-size: 32px; + fill: $display-amber; + visibility: visible !important; + text-anchor: middle; + font-family: "OIT"; +} + +.SelfTest { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 100%; + border: none; + background-color: $display-background; +} + +.LoadingProgressBarBackground { + position: absolute; + width: 40%; + height: 25px; + left: 30%; + top: 597.5px; + border: 1px solid #ffffff; +} +.LoadingProgressBarFill { + position: absolute; + width: 0%; + height: 25px; + left: 30%; + top: 597.5px; + border: 1px solid #ffffff; + background-color: rgb(0, 224, 254); +} +.FbwTail { + position: absolute; + top: 377.5px; + left: 591.5px; + background-image: url('/Images/fbw-a380x/Common/fbw-tail.png'); + background-repeat: no-repeat; + background-size: contain; + width: 160px; + height: 150px; +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/style.scss b/fbw-a380x/src/systems/instruments/src/OIT/style.scss new file mode 100644 index 00000000000..fc689940b39 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/style.scss @@ -0,0 +1,182 @@ +// Copyright (c) 2024 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +@import "../MsfsAvionicsCommon/definitions.scss"; +@import "./widget-style.scss"; + +@font-face { + font-family: "Ecam"; + //noinspection CssUnknownTarget + src: url("/Fonts/fbw-a380x/FBW-Display-EIS-A380.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "FBW-Display-EIS-A380-SlashedZero"; + //noinspection CssUnknownTarget + src: url("/Fonts/fbw-a380x/FBW-Display-EIS-A380-SlashedZero.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "OIT"; + //noinspection CssUnknownTarget + src: url("/Fonts/fbw-a380x/EFB/Inter-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +.oit-main { + position: absolute; + width: 1333px; + height: 1000px; /* 1:1.33 W:H */ + background: $display-background; + font-family: "OIT"; + display: flex; + flex-direction: column; + padding: 10px; +} + +.oit-page-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; + +} + +.oit-page-container.framed { + border: 3px solid $display-light-grey; + padding: 10px; + margin: 10px; +} + +.oit-heading { + font-size: 28px; + color: $display-cyan; + width: 245px; + padding: 13px; + align-items: center; + text-align: center; +} + +.oit-msg-header { + font-size: 28px; + color: $display-white; + width: 125px; + padding: 13px; + align-items: center; + text-align: center; +} + +.oit-msg-box { + font-size: 28px; + color: $display-white; + width: 430px; + padding: 13px; + align-items: center; + text-align: left; + border: 4px outset $display-light-grey; + background-color: $display-mfd-darker-grey; +} + +.oit-label { + font-size: 24px; + color: $display-white; +} + +.oit-label.green { + color: $display-green; +} + +.oit-label.amber { + color: $display-amber; +} + +.oit-label.cyan { + color: $display-cyan; +} + +.oit-label.bigger { + font-size: 28px; +} + +.oit-label.biggest { + font-size: 32px; +} + +.oit-header-row { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.fr { + display: flex; + flex-direction: row; +} + +.fc { + display: flex; + flex-direction: column; +} + +.oit-flt-ops-menu-column { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; +} + +.oit-flt-ops-menu-column-title { + font-size: 32px; + color: $display-white; + margin-top: 60px; + margin-bottom: 30px; +} + +.oit-flt-ops-sts-upper { + flex: 2; + display: flex; + flex-direction: column; + justify-content: center; + border-bottom: 3px solid $display-light-grey; +} + +.oit-flt-ops-sts-line { + display: flex; + flex-direction: row; + margin-top: 40px; + margin-bottom: 40px; +} + +.oit-flt-ops-sts-lower { + flex: 1; + display: flex; + flex-direction: column; +} + +.oit-flt-ops-sts-line-left { + flex: 2; + display: flex; + justify-content: flex-end; + align-items: center; + margin-right: 20px; +} + +.oit-flt-ops-sts-line-right { + flex: 5; + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; +} + +$font-file-path: '/Fonts/fbw-a380x/EFB'; + +@import "../../../../../../fbw-common/src/systems/instruments/src/EFB/Assets/Efb.scss"; + +.nopointer { + pointer-events: none; +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/tsconfig.json b/fbw-a380x/src/systems/instruments/src/OIT/tsconfig.json index befb760d707..b051de3a024 100644 --- a/fbw-a380x/src/systems/instruments/src/OIT/tsconfig.json +++ b/fbw-a380x/src/systems/instruments/src/OIT/tsconfig.json @@ -1,3 +1,33 @@ - { - "extends": "../../tsconfig.json" - } \ No newline at end of file +{ + "extends": "../../../tsconfig.json", + + "compilerOptions": { + "incremental": false /* Enables incremental builds */, + "target": "es2017" /* Specifies the ES2017 target, compatible with Coherent GT */, + "module": "es2015" /* Ensures that modules are at least es2015 */, + "strict": true /* Enables strict type checking, highly recommended but optional */, + "esModuleInterop": true /* Emits additional JS to work with CommonJS modules */, + "skipLibCheck": true /* Skip type checking on library .d.ts files */, + "forceConsistentCasingInFileNames": true /* Ensures correct import casing */, + "moduleResolution": "node" /* Enables compatibility with MSFS SDK bare global imports */, + "jsxFactory": "FSComponent.buildComponent" /* Required for FSComponent framework JSX */, + "jsxFragmentFactory": "FSComponent.Fragment" /* Required for FSComponent framework JSX */, + "jsx": "react", /* Required for FSComponent framework JSX */ + "paths": { + "@datalink/aoc": ["../../../fbw-common/src/systems/datalink/aoc/src/index.ts"], + "@datalink/atc": ["../../../fbw-common/src/systems/datalink/atc/src/index.ts"], + "@datalink/common": ["../../../fbw-common/src/systems/datalink/common/src/index.ts"], + "@datalink/router": ["../../../fbw-common/src/systems/datalink/router/src/index.ts"], + "@failures": ["./failures/src/index.ts"], + "@fmgc/*": ["./fmgc/src/*"], + "@instruments/common/*": ["./instruments/src/Common/*"], + "@localization/*": ["../localization/*"], + "@sentry/*": ["./sentry-client/src/*"], + "@simbridge/*": ["./simbridge-client/src/*"], + "@shared/*": ["./shared/src/*"], + "@tcas/*": ["./tcas/src/*"], + "@typings/*": ["../../../fbw-common/src/typings/*"], + "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index-no-react.ts"], + } + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/widget-style.scss b/fbw-a380x/src/systems/instruments/src/OIT/widget-style.scss new file mode 100644 index 00000000000..2a7b36d8e6d --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OIT/widget-style.scss @@ -0,0 +1,743 @@ +@import "../MsfsAvionicsCommon/definitions.scss"; + +.mfd-label { + font-size: 20px; + color: $display-white; + font-family: "OIT"; +} + +.mfd-label.green { + color: $display-green; +} + +.mfd-label.amber { + color: $display-amber; +} + +.mfd-label.magenta { + color: $display-magenta; +} + +.mfd-label.bigger { + font-size: 24px; +} + +.mfd-label.biggest { + font-size: 28px; +} + +.mfd-label-unit { + color: $display-dark-blue; + font-size: 20px; +} + +.mfd-value { + color: $display-green; + font-size: 25px; + font-family: "FBW-Display-EIS-A380-SlashedZero", monospace; +} + +.mfd-value.amber { + color: $display-amber; +} + +.mfd-value.cyan { + color: $display-cyan; +} + +.mfd-value.magenta { + color: $display-magenta; + font-size: 25px; +} + +.mfd-value.tmpy { + color: $display-yellow; + font-size: 25px; +} + +.mfd-value.sec { + color: $display-white; + font-size: 25px; +} + +.mfd-value.smaller { + font-size: 20px; +} + +.mfd-value.bigger { + font-size: 30px; +} + +.mfd-button { + display: flex; + font-size: 22px; + background-color: $display-mfd-darker-grey; + color: $display-white; + font-family: "OIT"; + text-align: center; + align-items: center; + justify-content: center; + border: 2px outset $display-light-grey; + padding: 10px 12px 10px 12px; +} + +.mfd-button.opened { + background-color: $display-dark-grey; + border: 2px inset $display-light-grey; +} + +.mfd-button:hover { + border-color: $display-cyan; +} + +.mfd-button.disabled { + color: $display-dark-grey !important; + --color-text: $display-dark-grey; + color: var(--color-text); +} + +.mfd-button.selected { + background-color: $display-dark-grey; + border: 2px inset $display-light-grey; +} + +.mfd-button.disabled:hover { + border-color: $display-light-grey; +} + +.mfd-icon-button { + display: flex; + flex-direction: row; + background-color: $display-mfd-darker-grey; + color: $display-white; + border: 2px outset $display-light-grey; + justify-content: center; + align-items: center; +} + +.mfd-icon-button:hover { + border: 2px outset $display-cyan; +} + +.mfd-dropdown-container { + position: relative; +} + +.mfd-dropdown-outer { + display: flex; + flex-direction: row; + justify-content: space-between; + background-color: $display-mfd-darker-grey; + border: 2px outset $display-light-grey; +} + +.mfd-dropdown-outer.inactive { + background-color: $display-background; + border: 2px outset transparent !important; +} + +.mfd-dropdown-outer:hover { + border-color: $display-cyan; +} + +.mfd-dropdown-inner { + background-color: $display-background; + display: flex; + flex: 1; + justify-content: center; + align-items: center; + height: 60px; +} + +.mfd-dropdown-arrow { + display: flex; + justify-content: center; + align-items: center; + width: 25px; +} + +.mfd-dropdown-arrow.inactive { + visibility: hidden; +} + +.mfd-dropdown-menu::-webkit-scrollbar { + width: 20px; +} + +.mfd-dropdown-menu::-webkit-scrollbar-track { + background: $display-mfd-darker-grey; +} + +.mfd-dropdown-menu::-webkit-scrollbar-thumb { + background: $display-light-grey; +} + +.mfd-dropdown-menu { + background-color: $display-background; + min-width: 100%; + position: absolute; + z-index: 60; + overflow: auto; + max-height: 600px; + display: none; + border: 2px outset $display-light-grey; +} + +.mfd-dropdown-menu-element { + color: $display-white; + font-size: 20px; + padding: 12px 16px; + display: block; + border: 3px solid transparent; + white-space: nowrap; + overflow: hidden; + text-align: left; + padding: 5px 16px; +} + +.mfd-dropdown-menu-element:hover { + border: 3px solid $display-cyan; +} + +.mfd-dropdown-menu-element.disabled { + color: $display-dark-grey; +} + +.mfd-dropdown-menu-element.separator { + border-bottom: 3px solid $display-light-grey; +} + +.mfd-dropdown-menu-element.separator:hover { + border: 3px solid $display-cyan; +} + +.mfd-page-selector-label { + font-size: 28px; + color: $display-white; + font-family: "OIT"; + padding: 2px; + text-align: center; + display: inline; + margin: 7px 0px 7px 0px; + padding: 3px; + border: 1px solid transparent; +} + +.mfd-page-selector-label.active { + border: 1px solid $display-white; +} + +.mfd-page-selector-outer { + display: flex; + flex-direction: row; + align-items: center; + background-color: $display-mfd-darker-grey; + border-top: 2px solid $display-light-grey; + border-left: 2px solid $display-light-grey; + border-bottom: 2px solid rgba(211, 211, 211, 0.432); + border-right: 2px solid rgba(211, 211, 211, 0.432); +} + +.mfd-page-selector-outer:hover { + border-color: $display-cyan; +} + +.mfd-page-selector-label-container { + display: flex; + flex: 8; + justify-content: center; +} + +.mfd-page-selector-label-triangle { + display: flex; +} + +.mfd-page-selector-label-triangle > span { + padding: 8px; +} + +.mfd-page-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + +.mfd-spacing-right { + margin-right: 15px; +} + +.mfd-unit-leading { + margin-right: 6px; +} + +.mfd-unit-trailing { + margin-left: 6px; +} + +.mfd-top-tab-navigator-container { + display: flex; + flex-direction: column; + flex: 1 1 auto; + margin: 2px; +} + +.mfd-navigator-container { + display: flex; + flex-direction: column; + flex: 1; +} + +.mfd-navigator-content { + display: flex; + flex-direction: column; + flex: 1; +} + +.mfd-top-tab-navigator-bar { + display: flex; +} + +.mfd-top-tab-navigator-bar-element-outer { + display: flex; + flex: 1; + border-bottom: 2px solid $display-light-grey; +} + +.mfd-top-tab-navigator-bar-element-label { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + background-color: $display-mfd-darker-grey; + border-top: 2px solid $display-light-grey; + padding: 6px 0px 2px 0px; +} + +.mfd-top-tab-navigator-bar-element-label.active { + background-color: $display-background; +} + +.mfd-top-tab-navigator-bar-element-label:hover { + border: 2px outset $display-cyan; +} + +.mfd-top-tab-navigator-bar-element-outer.active { + background-color: $display-background; + border-bottom-style: none; +} + +.mfd-top-tab-navigator-tab-content { + flex: 1; + display: flex; + flex-direction: column; + border: 2px outset $display-light-grey; + border-top-style: none; + padding: 10px; + overflow: hidden; +} + +.mfd-label-value-container { + padding: 7px; + display: flex; + flex-direction: row; + align-items: center; +} + +.mfd-radio-button{ + -webkit-appearance: none; /* WebKit */ + -moz-appearance: none; /* Mozilla */ + -o-appearance: none; /* Opera */ + -ms-appearance: none; /* Internet Explorer */ + appearance: none; /* CSS3 */ + font-size: 26px; + display: flex; + align-items: center; + border: 2px solid transparent; +} + +.mfd-radio-button + .mfd-radio-button { + margin-top: 5px; +} + +.mfd-radio-button input[type="radio"] { + appearance: none; /* CSS3 */ + background-color: $display-background; + width: 30px; + height: 30px; + border: 2px inset $display-light-grey; + border-radius: 50%; + margin-right: 15px; + display: grid; + place-content: center; +} + +.mfd-radio-button input[type="radio"]::after { + display: none; +} + + +.mfd-radio-button input[type="radio"]:checked { + box-shadow: inset 0 0 0px 4px $display-background; +} + +.mfd-radio-button.yellow input[type="radio"]:checked { + background: $display-yellow; +} + +.mfd-radio-button.green input[type="radio"]:checked { + background: $display-green; +} + +.mfd-radio-button.white input[type="radio"]:checked { + background: $display-white; +} + +.mfd-radio-button.cyan input[type="radio"]:checked { + background: $display-cyan; +} + +.mfd-radio-button input[type="radio"]:disabled { + background: $display-dark-grey !important; +} + + +.mfd-radio-button input[type="radio"]:checked { + background: $display-cyan; + box-shadow: inset 0 0 0px 4px $display-background; +} + +.mfd-radio-button input[type="radio"]:checked ~ * { + color: $display-cyan; +} + +.mfd-radio-button.yellow input[type="radio"]:checked ~ * { + color: $display-yellow; +} + +.mfd-radio-button.green input[type="radio"]:checked ~ * { + color: $display-green; +} + +.mfd-radio-button.white input[type="radio"]:checked ~ * { + color: $display-white; +} + +.mfd-radio-button.cyan input[type="radio"]:checked ~ * { + color: $display-cyan; +} + +.mfd-radio-button input[type="radio"]:disabled ~ * { + color: $display-dark-grey !important; +} + +.mfd-radio-button:hover { + border: 2px outset $display-cyan; +} + +.mfd-context-menu { + font-family: "OIT"; + background-color: $display-mfd-darker-grey; + border: 2px outset $display-light-grey; + position: absolute; + z-index: 10; + overflow: auto; + display: none; +} + +.mfd-context-menu-element { + color: $display-white; + font-size: 22px; + text-align: left; + padding: 7px 2px; + display: block; + border: 3px solid transparent; +} + +.mfd-context-menu-element:hover { + border: 3px solid $display-cyan; +} + +.mfd-context-menu-element.disabled { + color: $display-dark-grey; +} + +.mfd-context-menu-element.disabled:hover { + border: 3px solid transparent; +} + +.mfd-input-field-root { + display: flex; + flex-direction: row; + justify-items: flex-start; + align-items: baseline; +} + +.mfd-input-field-root.overflow { + position: relative; + top: 0px; +} + +.mfd-input-field-container { + display: flex; + flex-direction: row; + justify-items: center; + align-items: baseline; + padding: 4px 2px; + background-color: $display-background; + border: 2px inset $display-light-grey; + overflow: visible; + height: 37px; +} + +.mfd-input-field-container.inactive { + border: 2px inset transparent !important; + background-color: $display-background !important; +} + +.mfd-input-field-container.disabled { + background-color: $display-mfd-darker-grey; +} + +.mfd-input-field-container:hover { + border: 2px inset $display-cyan; +} + +.mfd-input-field-container.overflow { + position: absolute; + top: -18px; + z-index: 5; + border: 1px solid $display-dark-grey !important; +} + +.mfd-input-field-text-input { + background-color: $display-background; + border: none; + font-family: "OIT"; + font-size: 27px; + line-height: 27px; + color: $display-cyan; + padding: 0px; +} + +.mfd-input-field-text-input.inactive { + background-color: $display-background !important; + color: $display-green !important; +} + +.mfd-input-field-text-input.tmpy { + color: $display-yellow; +} + +.mfd-input-field-text-input.validating { + color: $display-light-grey; +} + +.mfd-input-field-text-input.disabled { + background-color: $display-mfd-darker-grey; + color: $display-light-grey; +} + +.mfd-input-field-text-input.mandatory { + color: $display-amber; +} + +.mfd-input-field-text-input.valueSelected { + background-color: $display-cyan; + color: $display-background; +} + +.mfd-input-field-text-input.tmpy.valueSelected { + background-color: $display-yellow; + color: $display-background; +} + +.mfd-input-field-text-input.editing { + font-size: 27px !important; +} + +.mfd-input-field-text-input.computedByFms { + font-size: 22px; +} + +.mfd-input-field-caret { + animation: blinking 1s step-start infinite; + height: 27px; + width: 18px; + background-color: $display-cyan; + color: $display-background; + font-family: "OIT"; + font-size: 27px; + text-align: center; + vertical-align: center; +} + +@keyframes blinking { + 50% { + background-color: $display-background; + color: $display-cyan; + } +} + +.mfd-input-field-unit { + align-self: center; +} + +.mfd-input-field-text-input-container { + display: flex; + flex: 1; + flex-direction: row; + align-self: center; + align-items: center; +} + +.mfd-mouse-cursor { + pointer-events: none; + width: 80px; + height: 80px; + position: absolute; + z-index: 999; +} + +.mfd-dialog { + display: flex; + flex-direction: column; + justify-content: space-between; + z-index: 10; + overflow: auto; + position: absolute; + background-color: $display-background; + border: 2px outset $display-light-grey; + padding: 5px; + margin: 5px; +} + +.mfd-dialog-title { + display: flex; + justify-content: center; + align-items: flex-start; + padding-top: 20px; +} + +.mfd-dialog-buttons { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.fr { + display: flex; + flex-direction: row; +} + +.fc { + display: flex; + flex-direction: column; +} + +.aic { + align-items: center; +} + +.jcc { + justify-content: center; +} + +.mfd-surv-button { + border: 4px inset $display-light-grey; + display: flex; + flex-direction: column; + width: 100px; + justify-content: center; + align-items: center; + padding: 5px; + height: 65px; +} + +.mfd-surv-button.disabled { + background-color: $display-dark-grey; + border: none; +} + +.mfd-surv-label { + font-size: 20px; + color: $display-white; + font-family: "OIT"; + text-align: center; + margin-bottom: 2px; +} + +.mfd-surv-status-button { + background-color: #3c3c3c; + height: 60px; + width: 90px; + position: absolute; + top: -35px; + left: 0px; + right: 0px; + margin: auto; + border: 2px solid $display-light-grey; +} + +.mfd-surv-status-button:hover { + border-color: $display-cyan; +} + +.mfd-surv-status-button-label { + text-align: center; + font-size: 22px; + padding-top: 3px; + color: $display-white; + font-family: "OIT"; +} + +.mfd-surv-status-indicator { + width: 100%; + height: 14.3%; + background-color: $display-light-grey; +} + +.mfd-surv-status-indicator.active { + background-color: $display-green; +} + +.mfd-surv-status-item { + font-size: 24px; +} + +.mfd-surv-status-item.active { + color: $display-green; +} + +.mfd-surv-status-item.failed { + color: $display-amber; +} + +.hidden { + display: none; +} + +.invisible { + visibility: hidden; +} + +.mfd-adsc-button { + border: 4px inset $display-light-grey; + display: flex; + flex-direction: column; + width: 100px; + justify-content: center; + align-items: center; + padding: 5px; + height: 65px; +} + +.mfd-adsc-button.disabled { + background-color: $display-dark-grey; + border: none; +} + +.mfd-adsc-label-off { + font-size: 20px; + color: $display-dark-grey; + font-family: "OIT"; +} diff --git a/fbw-a380x/src/systems/instruments/src/OITlegacy/OitLegacy.tsx b/fbw-a380x/src/systems/instruments/src/OITlegacy/OitLegacy.tsx new file mode 100644 index 00000000000..0d36d4c159d --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OITlegacy/OitLegacy.tsx @@ -0,0 +1,267 @@ +// Copyright (c) 2023-2024 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import React, { useEffect, useState } from 'react'; +import { + AircraftContext, + Dispatch, + ErrorFallback, + EventBusContextProvider, + FailuresOrchestratorProvider, + fetchSimbriefDataAction, + ModalProvider, + Navigation, + PowerContext, + PowerStates, + setAirframeInfo, + setCabinInfo, + setFlypadInfo, + store, + TroubleshootingContextProvider, + useAppDispatch, + useEventBus, + useNavigraphAuthInfo, +} from '@flybywiresim/flypad'; + +import { EventBus, Subscription } from '@microsoft/msfs-sdk'; +import { Provider } from 'react-redux'; +import { ErrorBoundary } from 'react-error-boundary'; +import { MemoryRouter as Router } from 'react-router'; +import { + FmsData, + NavigraphAuthProvider, + NXDataStore, + UniversalConfigProvider, + usePersistentProperty, + useSimVar, +} from '@flybywiresim/fbw-sdk'; +import { ToastContainer } from 'react-toastify'; +import { OisInternalData } from '../OIT/OisInternalPublisher'; +import { simbriefDataFromFms } from 'instruments/src/OITlegacy/utils'; + +export const getDisplayIndex = () => { + const url = Array.from(document.querySelectorAll('vcockpit-panel > *')) + ?.find((it) => it.tagName.toLowerCase() !== 'wasm-instrument') + ?.getAttribute('url'); + + return url ? parseInt(url.substring(url.length - 1), 10) : 0; +}; + +export interface OitEfbWrapperProps { + eventBus: EventBus; +} + +export const OitEfbWrapper: React.FC = ({ eventBus }) => { + const [powerState, setPowerState] = useState(PowerStates.LOADED); + + const [err, setErr] = useState(false); + + useEffect(() => { + UniversalConfigProvider.fetchAirframeInfo(process.env.AIRCRAFT_PROJECT_PREFIX, process.env.AIRCRAFT_VARIANT).then( + (info) => store.dispatch(setAirframeInfo(info)), + ); + + UniversalConfigProvider.fetchFlypadInfo(process.env.AIRCRAFT_PROJECT_PREFIX, process.env.AIRCRAFT_VARIANT).then( + (info) => store.dispatch(setFlypadInfo(info)), + ); + + UniversalConfigProvider.fetchCabinInfo(process.env.AIRCRAFT_PROJECT_PREFIX, process.env.AIRCRAFT_VARIANT).then( + (info) => store.dispatch(setCabinInfo(info)), + ); + }, []); + + useEffect(() => { + document + .getElementsByTagName('a380x-oitlegacy')[0] + .setAttribute('style', 'position: absolute; left: 10px; top: 75px; width: 1313px; height: 860px;'); + }, []); + + return ( + + + + + setErr(false)} resetKeys={[err]}> + + + + + + + + + + + + + + + + + + ); +}; + +export const OitEfbPageWrapper: React.FC = () => { + const dispatch = useAppDispatch(); + + const [showCharts] = useSimVar(`L:A32NX_OIS_${getDisplayIndex()}_SHOW_CHARTS`, 'Bool', 100); + const [showOfp] = useSimVar(`L:A32NX_OIS_${getDisplayIndex()}_SHOW_OFP`, 'Bool', 100); + const [synchroAvionics] = useSimVar('L:A32NX_OIS_SYNCHRO_AVIONICS', 'number', 100); + + const [fromAirport, setFromAirport] = useState(''); + const [toAirport, setToAirport] = useState(''); + const [altnAirport, setAltnAirport] = useState(''); + + const navigraphAuthInfo = useNavigraphAuthInfo(); + const [overrideSimBriefUserID] = usePersistentProperty('CONFIG_OVERRIDE_SIMBRIEF_USERID'); + + const [navigraphToken, setNavigraphToken] = useState(NXDataStore.get('NAVIGRAPH_ACCESS_TOKEN')); + const [reloadAircraft, setReloadAircraft] = useState(false); + + const showEfbOverlay = (showCharts === 1 && !reloadAircraft) || showOfp === 1; + document.getElementsByTagName('a380x-oitlegacy')[0].classList.toggle('nopointer', !showEfbOverlay); + + useEffect(() => { + const cancelSub = NXDataStore.getAndSubscribe('NAVIGRAPH_ACCESS_TOKEN', (_, token) => { + if (!navigraphToken && token) { + setReloadAircraft(true); + } + setNavigraphToken(token); + }); + + return () => cancelSub(); + }, []); + + const bus = useEventBus(); + + const updateSimBriefInfo = async () => { + try { + const action = await fetchSimbriefDataAction( + (navigraphAuthInfo.loggedIn && navigraphAuthInfo.username) || '', + overrideSimBriefUserID ?? '', + ); + const newAction = simbriefDataFromFms(action.payload, fromAirport, toAirport, altnAirport); + dispatch(newAction); + } catch (e) { + console.error(e.message); + } + }; + + useEffect(() => { + updateSimBriefInfo(); + }, [navigraphAuthInfo, synchroAvionics]); + + useEffect(() => { + const sub = bus.getSubscriber(); + const subs: Subscription[] = []; + subs.push( + sub.on('synchroAvncs').handle(() => { + updateSimBriefInfo(); // never triggers, because synced messages in the same VCockpit don't work + }), + ); + subs.push( + sub + .on('fmsOrigin') + .whenChanged() + .handle((icao) => setFromAirport(icao)), + ); + subs.push( + sub + .on('fmsDestination') + .whenChanged() + .handle((icao) => setToAirport(icao)), + ); + subs.push( + sub + .on('fmsAlternate') + .whenChanged() + .handle((icao) => setAltnAirport(icao)), + ); + + return () => { + subs.forEach((s) => s.destroy()); + }; + }, []); + + // Enable this for FMS auto-sync + /* useEffect(() => { + dispatch(simbriefDataFromFms(simBriefData, fromAirport, toAirport, altnAirport)); + }, [fromAirport, toAirport, altnAirport]); + */ + + return ( + <> + {showEfbOverlay && ( + + + + {showCharts === 1 && !reloadAircraft && } + {showOfp === 1 && } + + + + )} + > + ); +}; diff --git a/fbw-a380x/src/systems/instruments/src/OITlegacy/config.json b/fbw-a380x/src/systems/instruments/src/OITlegacy/config.json new file mode 100644 index 00000000000..4a92b27227a --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OITlegacy/config.json @@ -0,0 +1,15 @@ +{ + "index": "./index.tsx", + "name": "OITlegacy", + "isInteractive": true, + "dimensions": { + "width": 1313, + "height": 860 + }, + "additionalImports": [ + "/Pages/VCockpit/Instruments/Shared/Map/MapInstrument.html" + ], + "extraDeps": [ + "/fbw-common/src/systems/instruments/src/EFB" + ] +} diff --git a/fbw-a380x/src/systems/instruments/src/OIT/index.tsx b/fbw-a380x/src/systems/instruments/src/OITlegacy/index.tsx similarity index 62% rename from fbw-a380x/src/systems/instruments/src/OIT/index.tsx rename to fbw-a380x/src/systems/instruments/src/OITlegacy/index.tsx index a196268e2fd..d9df60824d6 100644 --- a/fbw-a380x/src/systems/instruments/src/OIT/index.tsx +++ b/fbw-a380x/src/systems/instruments/src/OITlegacy/index.tsx @@ -1,17 +1,13 @@ import React from 'react'; import { getRootElement } from '@instruments/common/defaults'; -import { HashRouter as Router } from 'react-router-dom'; import ReactDOM from 'react-dom'; import { renderTarget } from '../util.js'; -import { OnboardInformationTerminal } from './OnboardInformationTerminal'; import { render } from '../Common'; +import { OitEfbWrapper } from 'instruments/src/OITlegacy/OitLegacy.js'; +import { EventBus } from '@microsoft/msfs-sdk'; if (renderTarget) { - render( - - - , - ); + render(); } getRootElement().addEventListener('unload', () => { diff --git a/fbw-a380x/src/systems/instruments/src/OITlegacy/tsconfig.json b/fbw-a380x/src/systems/instruments/src/OITlegacy/tsconfig.json new file mode 100644 index 00000000000..269eff19f0d --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OITlegacy/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "resolveJsonModule" : true, + "paths": { + "@instruments/common/*": ["./instruments/src/Common/*"], + "@flybywiresim/fbw-sdk": ["../../../fbw-common/src/systems/index.ts"], + "@flybywiresim/flypad": ["../../../fbw-common/src/systems/instruments/src/EFB/index.ts"], + "@flybywiresim/checklists": ["../../../fbw-common/src/systems/shared/src/checklists/index.ts"] + } + } +} diff --git a/fbw-a380x/src/systems/instruments/src/OITlegacy/utils.ts b/fbw-a380x/src/systems/instruments/src/OITlegacy/utils.ts new file mode 100644 index 00000000000..ece6192d573 --- /dev/null +++ b/fbw-a380x/src/systems/instruments/src/OITlegacy/utils.ts @@ -0,0 +1,15 @@ +import { initialState, setSimbriefData, SimbriefData } from '@flybywiresim/flypad'; +import { PayloadAction } from '@reduxjs/toolkit'; + +export function simbriefDataFromFms( + simBriefData: SimbriefData, + origin?: string, + destination?: string, + altn?: string, +): PayloadAction { + const state = Object.assign({}, simBriefData ?? initialState.data); + state.departingAirport = origin ?? state.departingAirport; + state.arrivingAirport = destination ?? state.arrivingAirport; + state.altIcao = altn ?? state.altIcao; + return setSimbriefData(state); +} diff --git a/fbw-a380x/src/systems/shared/src/electrical.ts b/fbw-a380x/src/systems/shared/src/electrical.ts index 74671dff4b3..f45f9a03ebe 100644 --- a/fbw-a380x/src/systems/shared/src/electrical.ts +++ b/fbw-a380x/src/systems/shared/src/electrical.ts @@ -4,3 +4,12 @@ export enum DcElectricalBus { DcEss = 'DC_ESS', DcEssInFlight = '108PH', } + +export enum AcElectricalBus { + Ac1 = 'AC_1', + Ac2 = 'AC_2', + Ac3 = 'AC_3', + Ac4 = 'AC_4', + AcEss = 'AC_ESS', + AcEmer = 'AC_EMER', +} diff --git a/fbw-a380x/src/systems/systems-host/index.ts b/fbw-a380x/src/systems/systems-host/index.ts index b8ebac5a4de..0d3c6c65e28 100644 --- a/fbw-a380x/src/systems/systems-host/index.ts +++ b/fbw-a380x/src/systems/systems-host/index.ts @@ -35,6 +35,7 @@ import { FwsCore } from 'systems-host/systems/FlightWarningSystem/FwsCore'; import { FuelSystemPublisher } from 'systems-host/systems/FuelSystemPublisher'; import { BrakeToVacateDistanceUpdater } from 'systems-host/systems/BrakeToVacateDistanceUpdater'; import { PseudoFwcSimvarPublisher } from 'instruments/src/MsfsAvionicsCommon/providers/PseudoFwcPublisher'; +import { AircraftNetworkServerUnit } from 'systems-host/systems/InformationSystems/AircraftNetworkServerUnit'; class SystemsHost extends BaseInstrument { private readonly bus = new ArincEventBus(); @@ -104,6 +105,17 @@ class SystemsHost extends BaseInstrument { //FIXME add some deltatime functionality to backplane instruments so we dont have to pass SystemHost private readonly legacyFuel = new LegacyFuel(this.bus, this); + // For now, pass ATSU to the ANSUs. In our target architecture, there should be no ATSU + private readonly nssAnsu1 = new AircraftNetworkServerUnit(this.bus, 1, 'nss', this.failuresConsumer, this.atsu); + private readonly nssAnsu2 = new AircraftNetworkServerUnit(this.bus, 2, 'nss', this.failuresConsumer, this.atsu); + private readonly fltOpsAnsu1 = new AircraftNetworkServerUnit( + this.bus, + 1, + 'flt-ops', + this.failuresConsumer, + this.atsu, + ); + /** * "mainmenu" = 0 * "loading" = 1 @@ -134,6 +146,9 @@ class SystemsHost extends BaseInstrument { this.backplane.addPublisher('StallWarning', this.stallWarningPublisher); this.backplane.addPublisher('PseudoFwc', this.pseudoFwcPublisher); this.backplane.addInstrument('LegacyFuel', this.legacyFuel); + this.backplane.addInstrument('nssAnsu1', this.nssAnsu1, true); + this.backplane.addInstrument('nssAnsu2', this.nssAnsu2, true); + this.backplane.addInstrument('fltOpsAnsu1', this.fltOpsAnsu1, true); this.hEventPublisher = new HEventPublisher(this.bus); this.soundManager = new LegacySoundManager(); diff --git a/fbw-a380x/src/systems/systems-host/systems/InformationSystems/AircraftNetworkServerUnit.ts b/fbw-a380x/src/systems/systems-host/systems/InformationSystems/AircraftNetworkServerUnit.ts new file mode 100644 index 00000000000..3724391141d --- /dev/null +++ b/fbw-a380x/src/systems/systems-host/systems/InformationSystems/AircraftNetworkServerUnit.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2025 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus, Instrument, SimVarValueType, Subject, Subscribable } from '@microsoft/msfs-sdk'; +import { FailuresConsumer } from '@flybywiresim/fbw-sdk'; +import { A380Failure } from '@failures'; +import { AtsuSystem } from 'systems-host/systems/atsu'; + +type AnsuIndex = 1 | 2; + +type AnsuType = 'nss' | 'flt-ops'; + +export class AircraftNetworkServerUnit implements Instrument { + private readonly failureKey = + this.type === 'flt-ops' && this.index === 1 + ? A380Failure.FltOpsAnsu1 + : this.index === 1 + ? A380Failure.NssAnsu1 + : A380Failure.NssAnsu2; + + private readonly powered = Subject.create(false); + + private readonly _isHealthy = Subject.create(false); + public readonly isHealthy = this._isHealthy as Subscribable; + + constructor( + private readonly bus: EventBus, + private readonly index: AnsuIndex, + private readonly type: AnsuType, + private readonly failuresConsumer: FailuresConsumer, + private readonly atsu: AtsuSystem, + ) {} + + /** @inheritdoc */ + init(): void { + this.failuresConsumer.register(this.failureKey); + } + + /** @inheritdoc */ + onUpdate(): void { + const failed = this.failuresConsumer.isActive(this.failureKey); + + if (this.type === 'nss') { + if (this.index === 1) { + this.powered.set( + SimVar.GetSimVarValue('L:A32NX_ELEC_AC_2_BUS_IS_POWERED', SimVarValueType.Bool) || + SimVar.GetSimVarValue('L:A32NX_ELEC_DC_HOT_2_BUS_IS_POWERED', SimVarValueType.Bool), + ); + } else { + this.powered.set( + SimVar.GetSimVarValue('L:A32NX_ELEC_AC_1_BUS_IS_POWERED', SimVarValueType.Bool) || + SimVar.GetSimVarValue('L:A32NX_ELEC_AC_EMER_BUS_IS_POWERED', SimVarValueType.Bool) || + SimVar.GetSimVarValue('L:A32NX_ELEC_DC_HOT_1_BUS_IS_POWERED', SimVarValueType.Bool), + ); + } + } else if (this.type === 'flt-ops') { + this.powered.set( + SimVar.GetSimVarValue('L:A32NX_ELEC_AC_ESS_BUS_IS_POWERED', SimVarValueType.Bool) || + SimVar.GetSimVarValue('L:A32NX_ELEC_DC_HOT_1_BUS_IS_POWERED', SimVarValueType.Bool), + ); + } + + this._isHealthy.set(!failed && this.powered.get()); + SimVar.SetSimVarValue( + `L:A32NX_${this.type === 'nss' ? 'NSS' : 'FLTOPS'}_ANSU_${this.index.toFixed(0)}_IS_HEALTHY`, + SimVarValueType.Bool, + this._isHealthy.get(), + ); + } +} diff --git a/fbw-common/src/systems/instruments/src/EFB/index.ts b/fbw-common/src/systems/instruments/src/EFB/index.ts index 9b6f797f90e..2493bc8d6b3 100644 --- a/fbw-common/src/systems/instruments/src/EFB/index.ts +++ b/fbw-common/src/systems/instruments/src/EFB/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0 export * from './AircraftContext'; +export * from './Apis/Navigraph/Components/Authentication'; export * from './Apis/Simbrief'; export * from './Efb'; export * from './Enum/Airframe'; @@ -11,7 +12,9 @@ export * from './Assets/NoseOutline'; export * from './Assets/FuelOutline'; export * from './Assets/OverWingOutline'; export * from './Assets/A380SeatOutlineBg'; +export * from './Dispatch/Dispatch'; export * from './Localization/translation'; +export * from './Navigation/Navigation'; export * from './Settings/Migration'; export * from './Settings/Settings'; export * from './Settings/sync'; @@ -21,6 +24,7 @@ export * from './Store/features/simBrief'; export * from './Store/features/payload'; export * from './Store/features/config'; export * from './Store/store'; +export * from './TroubleshootingContext'; export * from './UtilComponents/BingMap'; export * from './UtilComponents/Card/Card'; export * from './UtilComponents/Form/Select'; diff --git a/fbw-common/src/systems/instruments/src/ND/shared/BtvRunwayInfo.tsx b/fbw-common/src/systems/instruments/src/ND/shared/BtvRunwayInfo.tsx index a370e6675f0..7f8ea8c7467 100644 --- a/fbw-common/src/systems/instruments/src/ND/shared/BtvRunwayInfo.tsx +++ b/fbw-common/src/systems/instruments/src/ND/shared/BtvRunwayInfo.tsx @@ -3,11 +3,11 @@ import { FSComponent, DisplayComponent, VNode, MappedSubject, ConsumerSubject } from '@microsoft/msfs-sdk'; -import { Arinc429LocalVarConsumerSubject, ArincEventBus, BtvData, FmsOansData } from '@flybywiresim/fbw-sdk'; +import { Arinc429LocalVarConsumerSubject, ArincEventBus, BtvData, FmsOansData, FmsData } from '@flybywiresim/fbw-sdk'; import { Layer } from '../../MsfsAvionicsCommon/Layer'; export class BtvRunwayInfo extends DisplayComponent<{ bus: ArincEventBus }> { - private readonly sub = this.props.bus.getArincSubscriber(); + private readonly sub = this.props.bus.getArincSubscriber(); private readonly fmsRwyIdent = ConsumerSubject.create(this.sub.on('fmsLandingRunway'), null); diff --git a/fbw-common/src/systems/instruments/src/OANC/OancControlPanelUtils.ts b/fbw-common/src/systems/instruments/src/OANC/OancControlPanelUtils.ts index b60886419ea..82305fa4bc5 100644 --- a/fbw-common/src/systems/instruments/src/OANC/OancControlPanelUtils.ts +++ b/fbw-common/src/systems/instruments/src/OANC/OancControlPanelUtils.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0 import { ArraySubject, ConsumerSubject, DmsFormatter2, EventBus, Subject, UnitType } from '@microsoft/msfs-sdk'; -import { AmdbAirportSearchResult, FmsOansData } from '@flybywiresim/fbw-sdk'; +import { AmdbAirportSearchResult, FmsData } from '@flybywiresim/fbw-sdk'; export enum ControlPanelAirportSearchMode { Icao, @@ -57,7 +57,7 @@ export class ControlPanelStore { export class FmsDataStore { constructor(private bus: EventBus) { - const sub = this.bus.getSubscriber(); + const sub = this.bus.getSubscriber(); this.origin.setConsumer(sub.on('fmsOrigin')); this.destination.setConsumer(sub.on('fmsDestination')); this.alternate.setConsumer(sub.on('fmsAlternate')); diff --git a/fbw-common/src/systems/instruments/src/react/index.ts b/fbw-common/src/systems/instruments/src/react/index.ts index deb10379af0..6b8197f23d2 100644 --- a/fbw-common/src/systems/instruments/src/react/index.ts +++ b/fbw-common/src/systems/instruments/src/react/index.ts @@ -1,6 +1,7 @@ export * from './arinc429'; export * from './bitFlags'; export * from './hooks'; +export * from './navigraph'; export * from './persistence'; export * from './simVars'; export * from './useInterval'; diff --git a/fbw-common/src/systems/shared/src/ata.ts b/fbw-common/src/systems/shared/src/ata.ts index 36672356ec1..2ea19792ea0 100644 --- a/fbw-common/src/systems/shared/src/ata.ts +++ b/fbw-common/src/systems/shared/src/ata.ts @@ -103,6 +103,7 @@ export const AtaChaptersDescription = Object.freeze({ 31: 'The cockpit displays give critical flight information to the pilots. In a failure where displays are lost, the pilots must deal with a lack of flight data given to them.', 32: 'The landing gear components are responsible for supporting and steering the aircraft on the ground, and make it possible to retract and store the landing gear in flight. Includes the functioning and maintenance aspects of the landing gear doors.', 34: 'The navigation systems provide data about the position, speed, heading, and altitude of the aircraft. Failures in a system such as the ADIRS can cause a loss of data sent to instrumentation.', + 46: 'Information systems provide means of communication between Airline Operational Control (AOC), Air Traffic Control (ATC), and various applications around the organization of on-board information (e.g. via the OIS)', }); export type AtaChapterNumber = keyof typeof AtaChaptersTitle; diff --git a/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsOansPublisher.ts b/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsOansPublisher.ts index 316cb85bc10..28b15275f1d 100644 --- a/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsOansPublisher.ts +++ b/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsOansPublisher.ts @@ -9,16 +9,6 @@ import { Position } from '@turf/turf'; * Transmitted from FMS to OANS */ export interface FmsOansData { - /** (FMS -> OANS) Selected origin airport. */ - fmsOrigin: string; - /** (FMS -> OANS) Selected destination airport. */ - fmsDestination: string; - /** (FMS -> OANS) Selected alternate airport. */ - fmsAlternate: string; - /** (FMS -> OANS) Identifier of departure runway. */ - fmsDepartureRunway: string; - /** (FMS -> OANS) Identifier of landing runway selected through FMS. */ - fmsLandingRunway: string; /** Identifier of landing runway selected for BTV through OANS. */ oansSelectedLandingRunway: string; /** Arinc429: Length of landing runway selected for BTV through OANS, in meters. */ diff --git a/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsPublisher.ts b/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsPublisher.ts new file mode 100644 index 00000000000..5c08daf04a2 --- /dev/null +++ b/fbw-common/src/systems/shared/src/publishers/OansBtv/FmsPublisher.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2024 FlyByWire Simulations +// SPDX-License-Identifier: GPL-3.0 + +/** + * Transmitted from FMS + */ +export interface FmsData { + /** Selected origin airport. */ + fmsOrigin: string | null; + /** Selected destination airport. */ + fmsDestination: string | null; + /** Selected alternate airport. */ + fmsAlternate: string | null; + /** Identifier of departure runway. */ + fmsDepartureRunway: string | null; + /** Identifier of landing runway selected through FMS. */ + fmsLandingRunway: string | null; + /** Flight number entered on INIT page */ + fmsFlightNumber: string | null; +} diff --git a/fbw-common/src/systems/shared/src/publishers/OansBtv/index.ts b/fbw-common/src/systems/shared/src/publishers/OansBtv/index.ts index d649b32cf33..350dce3ae7b 100644 --- a/fbw-common/src/systems/shared/src/publishers/OansBtv/index.ts +++ b/fbw-common/src/systems/shared/src/publishers/OansBtv/index.ts @@ -1,2 +1,3 @@ export * from './BtvPublisher'; +export * from './FmsPublisher'; export * from './FmsOansPublisher';