Skip to content

Commit

Permalink
feat(fms/efb): Pause at distance before top of descent (flybywiresim#…
Browse files Browse the repository at this point in the history
…7165)

* feat(efb): added settings for tod pause

* feat(fms): t/d pause with ap guard and custom t/d pause distance

* meta: changelog

* fix: incorporated suggested improvements
  • Loading branch information
2hwk authored and BlueberryKing committed Jun 18, 2022
1 parent 2b808e0 commit 757b134
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 33 deletions.
1 change: 1 addition & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
1. [SFCC] Add SFCC bus outputs - @lukecologne (luke)
1. [EWD] Use FPPU angles for flaps/slats display - @lukecologne (luke)
1. [FMGC] Basic RNP at or below 0.3 support - @tracernz (Mike)
1. [EFB] Added pause at T/D function - @2hwk (2Cas#1022)

## 0.8.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4875,6 +4875,10 @@ class FMCMainDisplay extends BaseAirliners {
return this.thrustReductionAltitude;
}

getOriginTransitionAltitude() {
return this.flightPlanManager.getOriginTransitionAltitude();
}

getCruiseAltitude() {
return this.cruiseFlightLevel * 100;
}
Expand Down
1 change: 1 addition & 0 deletions src/fmgc/src/guidance/GuidanceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface Fmgc {
getManagedClimbSpeedMach(): Mach;
getAccelerationAltitude(): Feet,
getThrustReductionAltitude(): Feet,
getOriginTransitionAltitude(): Feet | undefined,
getCruiseAltitude(): Feet,
getFlightPhase(): FmgcFlightPhase,
getManagedCruiseSpeed(): Knots,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface VerticalProfileComputationParameters {
destinationAirfieldElevation: Feet,
accelerationAltitude: Feet,
thrustReductionAltitude: Feet,
originTransitionAltitude?: Feet,
cruiseAltitude: Feet,
climbSpeedLimit: SpeedLimit,
descentSpeedLimit: SpeedLimit,
Expand Down Expand Up @@ -83,6 +84,7 @@ export class VerticalProfileComputationParametersObserver {
destinationAirfieldElevation: SimVar.GetSimVarValue('L:A32NX_PRESS_AUTO_LANDING_ELEVATION', 'feet'),
accelerationAltitude: this.fmgc.getAccelerationAltitude(),
thrustReductionAltitude: this.fmgc.getThrustReductionAltitude(),
originTransitionAltitude: this.fmgc.getOriginTransitionAltitude(),
cruiseAltitude: Number.isFinite(this.fmgc.getCruiseAltitude()) ? this.fmgc.getCruiseAltitude() : this.parameters.cruiseAltitude,
climbSpeedLimit: this.fmgc.getClimbSpeedLimit(),
descentSpeedLimit: this.fmgc.getDescentSpeedLimit(),
Expand Down
2 changes: 1 addition & 1 deletion src/fmgc/src/guidance/vnav/VnavDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class VnavDriver implements GuidanceComponent {
}

this.updateTimeMarkers();
this.descentGuidance.update();
this.descentGuidance.update(deltaTime);
} catch (e) {
console.error('[FMS] Failed to calculate vertical profil. See exception below.');
console.error(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export class AircraftToDescentProfileRelation {
return !this.topOfDescent || this.inertialDistanceAlongTrack.get() > this.topOfDescent.distanceFromStart;
}

distanceToTopOfDescent(): number | null {
if (this.topOfDescent) {
return this.topOfDescent.distanceFromStart - this.inertialDistanceAlongTrack.get();
}
return null;
}

isOnGeometricPath(): boolean {
return this.inertialDistanceAlongTrack.get() > this.geometricPathStart.distanceFromStart;
}
Expand Down
22 changes: 6 additions & 16 deletions src/fmgc/src/guidance/vnav/descent/DescentGuidance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { VerticalMode } from '@shared/autopilot';
import { FmgcFlightPhase } from '@shared/flightphase';
import { VnavConfig } from '@fmgc/guidance/vnav/VnavConfig';
import { SpeedMargin } from './SpeedMargin';
import { TodGuidance } from './TodGuidance';

enum DescentVerticalGuidanceState {
InvalidProfile,
Expand Down Expand Up @@ -41,9 +42,9 @@ export class DescentGuidance {

private speedMargin: SpeedMargin;

private speedTarget: Knots | Mach;
private todGuidance: TodGuidance;

private tdReached: boolean;
private speedTarget: Knots | Mach;

// An "overspeed condition" just means we are above the speed margins, not that we are in the red band.
// We use a boolean here for hysteresis
Expand All @@ -57,6 +58,7 @@ export class DescentGuidance {
private atmosphericConditions: AtmosphericConditions,
) {
this.speedMargin = new SpeedMargin(this.observer);
this.todGuidance = new TodGuidance(this.aircraftToDescentProfileRelation, this.observer, this.atmosphericConditions);

this.writeToSimVars();
}
Expand Down Expand Up @@ -91,7 +93,7 @@ export class DescentGuidance {
this.isInOverspeedCondition = false;
}

update() {
update(deltaTime: number) {
this.aircraftToDescentProfileRelation.update();

if (!this.aircraftToDescentProfileRelation.isValid) {
Expand All @@ -113,19 +115,7 @@ export class DescentGuidance {
}

this.writeToSimVars();
this.updateTdReached();
}

updateTdReached() {
const { flightPhase } = this.observer.get();
const isPastTopOfDescent = this.aircraftToDescentProfileRelation.isPastTopOfDescent();
const isInManagedSpeed = Simplane.getAutoPilotAirspeedManaged();

const tdReached = flightPhase >= FmgcFlightPhase.Climb && flightPhase <= FmgcFlightPhase.Cruise && isPastTopOfDescent && isInManagedSpeed;
if (tdReached !== this.tdReached) {
this.tdReached = tdReached;
SimVar.SetSimVarValue('L:A32NX_PFD_MSG_TD_REACHED', 'boolean', this.tdReached);
}
this.todGuidance.update(deltaTime);
}

private updateLinearDeviation() {
Expand Down
22 changes: 6 additions & 16 deletions src/fmgc/src/guidance/vnav/descent/LatchedDescentGuidance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NavGeometryProfile } from '@fmgc/guidance/vnav/profile/NavGeometryProfi
import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
import { VerticalMode } from '@shared/autopilot';
import { FmgcFlightPhase } from '@shared/flightphase';
import { TodGuidance } from './TodGuidance';
import { SpeedMargin } from './SpeedMargin';

enum DescentVerticalGuidanceState {
Expand Down Expand Up @@ -38,9 +39,9 @@ export class LatchedDescentGuidance {

private speedMargin: SpeedMargin;

private speedTarget: Knots | Mach;
private todGuidance: TodGuidance;

private tdReached: boolean;
private speedTarget: Knots | Mach;

// An "overspeed condition" just means we are above the speed margins, not that we are in the red band.
// We use a boolean here for hysteresis
Expand All @@ -52,6 +53,7 @@ export class LatchedDescentGuidance {
private atmosphericConditions: AtmosphericConditions,
) {
this.speedMargin = new SpeedMargin(this.observer);
this.todGuidance = new TodGuidance(this.aircraftToDescentProfileRelation, this.observer, this.atmosphericConditions);

this.writeToSimVars();
}
Expand Down Expand Up @@ -86,7 +88,7 @@ export class LatchedDescentGuidance {
this.isInOverspeedCondition = false;
}

update() {
update(deltaTime: number) {
this.aircraftToDescentProfileRelation.update();

if (!this.aircraftToDescentProfileRelation.isValid) {
Expand All @@ -108,19 +110,7 @@ export class LatchedDescentGuidance {
}

this.writeToSimVars();
this.updateTdReached();
}

updateTdReached() {
const { flightPhase } = this.observer.get();
const isPastTopOfDescent = this.aircraftToDescentProfileRelation.isPastTopOfDescent();
const isInManagedSpeed = Simplane.getAutoPilotAirspeedManaged();

const tdReached = flightPhase <= FmgcFlightPhase.Cruise && isPastTopOfDescent && isInManagedSpeed;
if (tdReached !== this.tdReached) {
this.tdReached = tdReached;
SimVar.SetSimVarValue('L:A32NX_PFD_MSG_TD_REACHED', 'boolean', this.tdReached);
}
this.todGuidance.update(deltaTime);
}

private updateLinearDeviation() {
Expand Down
106 changes: 106 additions & 0 deletions src/fmgc/src/guidance/vnav/descent/TodGuidance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { AtmosphericConditions } from '@fmgc/guidance/vnav/AtmosphericConditions';
import { AircraftToDescentProfileRelation } from '@fmgc/guidance/vnav/descent/AircraftToProfileRelation';
import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
import { LateralMode } from '@shared/autopilot';
import { FmgcFlightPhase } from '@shared/flightphase';
import { NXDataStore } from '@shared/persistence';
import { PopUp } from '@shared/popup';

const TIMEOUT = 10_000;

export class TodGuidance {
private tdReached: boolean;

private tdPaused: boolean;

private apEngaged: boolean;

private cooldown: number;

constructor(
private aircraftToDescentProfileRelation: AircraftToDescentProfileRelation,
private observer: VerticalProfileComputationParametersObserver,
private atmosphericConditions: AtmosphericConditions,
) {
this.cooldown = 0;
this.apEngaged = false;
this.tdReached = false;
this.tdPaused = false;
}

showPausePopup(title: string, message: string) {
this.cooldown = TIMEOUT;
SimVar.SetSimVarValue('K:PAUSE_SET', 'number', 1);
let popup = new PopUp();
popup.showInformation(title, message, 'small',
() => {
SimVar.SetSimVarValue('K:PAUSE_SET', 'number', 0);
this.cooldown = TIMEOUT;
popup = null;
});
}

update(deltaTime: number) {
this.updateTdReached(deltaTime);
this.updateTdPause(deltaTime);
}

updateTdPause(deltaTime: number) {
if (
this.cooldown <= 0
&& NXDataStore.get('PAUSE_AT_TOD', 'DISABLED') === 'ENABLED'
) {
// Only watching if T/D pause untriggered + between flight phase CLB and CRZ
if (!this.tdPaused
&& this.observer.get().flightPhase >= FmgcFlightPhase.Climb
&& this.observer.get().flightPhase <= FmgcFlightPhase.Cruise
&& Simplane.getAutoPilotAirspeedManaged()
) {
// Check T/D pause first, then AP mode reversion
if ((this.aircraftToDescentProfileRelation.distanceToTopOfDescent() ?? Number.POSITIVE_INFINITY) < parseFloat(NXDataStore.get('PAUSE_AT_TOD_DISTANCE', '10'))) {
this.tdPaused = true;
this.showPausePopup(
'TOP OF DESCENT',
`Paused before the calculated top of descent. System Time was ${new Date().toLocaleTimeString()}.`,
);
// Only guard AP above transitional altitude
} else if (this.atmosphericConditions.currentAltitude ? this.atmosphericConditions.currentAltitude > this.observer.get().originTransitionAltitude : false) {
const apActive = SimVar.GetSimVarValue('L:A32NX_AUTOPILOT_ACTIVE', 'boolean') && SimVar.GetSimVarValue('L:A32NX_FMA_LATERAL_MODE', 'Enum') === LateralMode.NAV;

if (this.apEngaged && !apActive) {
this.showPausePopup(
'AP PROTECTION',
`Autopilot or lateral guidance disengaged before the calculated top of descent. System Time was ${new Date().toLocaleTimeString()}.`,
);
}

if (this.apEngaged !== apActive) {
this.apEngaged = apActive;
}
}
}

// Reset flags on turnaround
if (this.observer.get().flightPhase === FmgcFlightPhase.Done || this.observer.get().flightPhase === FmgcFlightPhase.Preflight) {
this.tdPaused = false;
this.apEngaged = false;
}
}

if (this.cooldown > 0) {
this.cooldown = Math.max(0, this.cooldown - deltaTime);
}
}

updateTdReached(_deltaTime: number) {
const tdReached = this.observer.get().flightPhase >= FmgcFlightPhase.Climb
&& this.observer.get().flightPhase <= FmgcFlightPhase.Cruise
&& Simplane.getAutoPilotAirspeedManaged()
&& this.aircraftToDescentProfileRelation.isPastTopOfDescent();

if (tdReached !== this.tdReached) {
this.tdReached = tdReached;
SimVar.SetSimVarValue('L:A32NX_PFD_MSG_TD_REACHED', 'boolean', this.tdReached);
}
}
}
24 changes: 24 additions & 0 deletions src/instruments/src/EFB/Settings/Pages/RealismPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const RealismPage = () => {
const [dmcSelfTestTime, setDmcSelfTestTime] = usePersistentProperty('CONFIG_SELF_TEST_TIME', '12');
const [mcduInput, setMcduInput] = usePersistentProperty('MCDU_KB_INPUT', 'DISABLED');
const [mcduTimeout, setMcduTimeout] = usePersistentProperty('CONFIG_MCDU_KB_TIMEOUT', '60');
const [pauseAtTod, setPauseAtTod] = usePersistentProperty('PAUSE_AT_TOD', 'DISABLED');
const [todOffset, setTodOffset] = usePersistentNumberProperty('PAUSE_AT_TOD_DISTANCE', 10);
const [boardingRate, setBoardingRate] = usePersistentProperty('CONFIG_BOARDING_RATE', 'REAL');
const [realisticTiller, setRealisticTiller] = usePersistentNumberProperty('REALISTIC_TILLER_ENABLED', 0);
const [homeCockpit, setHomeCockpit] = usePersistentProperty('HOME_COCKPIT_ENABLED', '0');
Expand Down Expand Up @@ -121,6 +123,28 @@ export const RealismPage = () => {
)}
</SettingGroup>

<SettingGroup>
<SettingItem name={t('Settings.Realism.PauseAtTod')} unrealistic groupType="parent">
<Toggle value={pauseAtTod === 'ENABLED'} onToggle={(value) => setPauseAtTod(value ? 'ENABLED' : 'DISABLED')} />
</SettingItem>
{pauseAtTod === 'ENABLED' && (
<SettingItem name={t('Settings.Realism.PauseAtTodDistance')} groupType="sub">
<SimpleInput
className="text-center w-30"
value={todOffset}
min={0}
max={50.0}
disabled={(pauseAtTod !== 'ENABLED')}
onChange={(event) => {
if (!Number.isNaN(event) && parseInt(event) >= 0 && parseInt(event) <= 50.0) {
setTodOffset(parseFloat(event.trim()));
}
}}
/>
</SettingItem>
)}
</SettingGroup>

</SettingsPage>
);
};

0 comments on commit 757b134

Please sign in to comment.