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 Jan 20, 2023
1 parent 643ef53 commit 878a9da
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 31 deletions.
3 changes: 2 additions & 1 deletion .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
1. [FLIGHTMODEL] Update gear drag - @donstim (donbikes#4084)
1. [FLIGHT MODEL/EFB] Modified empty weight cg and loading station/fuel tank locations - @donstim (donbikes#4084)
1. [HYD] Fix gear sequence starting when failing prox sensor - @Crocket63 (crocket)
1. [MISC] Added aircraft version check and uer notification - @frankkopp (Frank Kopp)
1. [MISC] Added aircraft version check and uer notification - @frankkopp (Frank Kopp)
1. [EFB] Added QuickControls to flyPad StatusBar - @Benjozork (Benjamin Dupont) @frankkopp (Frank Kopp)
1. [SOUND] Fix announcements playing twice and adding check for power to PA - @frankkopp (Frank Kopp)
1. [EFB] Added pause at T/D function - @2hwk (2Cas#1022)

## 0.9.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5126,6 +5126,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
20 changes: 5 additions & 15 deletions src/fmgc/src/guidance/vnav/descent/DescentGuidance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vna
import { VerticalMode } from '@shared/autopilot';
import { FmgcFlightPhase } from '@shared/flightphase';
import { SpeedMargin } from './SpeedMargin';
import { TodGuidance } from './TodGuidance';

enum DescentVerticalGuidanceState {
InvalidProfile,
Expand Down Expand Up @@ -40,9 +41,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 @@ -56,6 +57,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 @@ -112,19 +114,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
20 changes: 5 additions & 15 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 @@ -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);
}
}
}
23 changes: 23 additions & 0 deletions src/instruments/src/EFB/Settings/Pages/RealismPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const RealismPage = () => {
const [boardingRate, setBoardingRate] = usePersistentProperty('CONFIG_BOARDING_RATE', 'REAL');
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 [realisticTiller, setRealisticTiller] = usePersistentNumberProperty('REALISTIC_TILLER_ENABLED', 0);
const [homeCockpit, setHomeCockpit] = usePersistentProperty('HOME_COCKPIT_ENABLED', '0');
const [autoFillChecklists, setAutoFillChecklists] = usePersistentNumberProperty('EFB_AUTOFILL_CHECKLISTS', 0);
Expand Down Expand Up @@ -136,6 +138,27 @@ export const RealismPage = () => {
<Toggle value={!!firstOfficerAvatar} onToggle={(value) => setFirstOfficerAvatar(value ? 1 : 0)} />
</SettingItem>

<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 878a9da

Please sign in to comment.