diff --git a/server/bridge.js b/server/bridge.js
index 5367c02..4271de2 100644
--- a/server/bridge.js
+++ b/server/bridge.js
@@ -279,7 +279,7 @@ function startDemo() {
apMode: 2, hdgStatus: 2, gpssStatus: 1, altStatus: 2,
lat: 47.45, lon: -122.31, track: 90, groundspeed: 64, gpsDistNm: 18.4, gpsBearing: 92,
// radios (XP freq units: nav/com in 10 kHz, e.g. 11030 = 110.30)
- nav1: 11030, nav1Sb: 11150, nav2: 11380, nav2Sb: 10890,
+ nav1: 11380, nav1Sb: 11150, nav2: 11030, nav2Sb: 10890,
com1: 12190, com1Sb: 13000, com2: 12475, com2Sb: 12180,
// HSI / data fields
obsCrs: 175, hsiDef: -0.6, hsiToFrom: 1, navBearing: 168, gsDef: 0.7,
diff --git a/server/flightplan.js b/server/flightplan.js
index 57c0090..3b24ecf 100644
--- a/server/flightplan.js
+++ b/server/flightplan.js
@@ -18,7 +18,7 @@ export function setPlan(next) {
const wps = Array.isArray(next?.waypoints)
? next.waypoints
.filter((w) => isFinite(w.lat) && isFinite(w.lon))
- .map((w) => ({ id: String(w.id || 'WPT'), lat: +w.lat, lon: +w.lon, type: w.type || 'WPT', alt: w.alt ?? null }))
+ .map((w) => ({ id: String(w.id || 'WPT'), lat: +w.lat, lon: +w.lon, type: w.type || 'WPT', alt: w.alt ?? null, ...(w.dsgn != null ? { dsgn: !!w.dsgn } : {}), ...(w.appr ? { appr: true } : {}) }))
: [];
const wantLeg = Number.isFinite(next?.activeLeg) ? next.activeLeg : 1;
plan = { name: next?.name || 'ACTIVE', waypoints: wps, activeLeg: Math.max(1, Math.min(wps.length - 1, wantLeg)) || 1 };
diff --git a/web/src/components/FplPage.jsx b/web/src/components/FplPage.jsx
index a82d9fc..9b87c83 100644
--- a/web/src/components/FplPage.jsx
+++ b/web/src/components/FplPage.jsx
@@ -74,7 +74,7 @@ export default function FplPage({ xp, full = false, onClose }) {
for (let i = Math.max(1, active); i < wps.length; i++) {
c += distNm({ lat: pl, lon: po }, wps[i]); pl = wps[i].lat; po = wps[i].lon;
const t = num(wps[i].alt);
- if (t > 0 && t < alt - 50) {
+ if (t > 0 && t < alt - 50 && (wps[i].dsgn ?? true)) {
const tan = Math.tan((3 * Math.PI) / 180);
const vsTgt = -gs * tan * 101.27;
const vsReq = c > 0 ? (t - alt) / (c / gs * 60) : 0;
@@ -106,7 +106,13 @@ export default function FplPage({ xp, full = false, onClose }) {
{dtk == null ? '___' : `${String(dtk).padStart(3, '0')}°`}
{orig ? '—' : d.toFixed(1)}
{orig ? '—' : cum.toFixed(0)}
- {w.alt ? `${w.alt}FT` : '_____'}
+ {
+ e.stopPropagation(); if (!w.alt) return;
+ const next = wps.map((x, j) => (j === i ? { ...x, dsgn: !(x.dsgn ?? true) } : x));
+ fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
+ }}>{w.alt ? `${w.alt}FT` : '_____'}
))}
diff --git a/web/src/components/PFD.jsx b/web/src/components/PFD.jsx
index a5bc5b6..85a3511 100644
--- a/web/src/components/PFD.jsx
+++ b/web/src/components/PFD.jsx
@@ -74,7 +74,7 @@ function vnavInfo(V, fp) {
cum += brgDist(prevLat, prevLon, wps[i].lat, wps[i].lon).dist;
prevLat = wps[i].lat; prevLon = wps[i].lon;
const tgt = num(wps[i].alt);
- if (tgt > 0 && tgt < alt - 50) {
+ if (tgt > 0 && tgt < alt - 50 && (wps[i].dsgn ?? true)) {
const tan = Math.tan((VNAV_FPA * Math.PI) / 180);
const tMin = (cum / gs) * 60;
const vsReq = tMin > 0 ? (tgt - alt) / tMin : 0; // fpm to make the fix
@@ -181,6 +181,16 @@ export default function PFD({ values: V, command, connected = true, svt = true,
const nav = activeNav(V, flightPlan);
const vnav = vnavInfo(V, flightPlan);
+ // GPS phase annunciation: APR when an approach leg is active, TERM within 30 nm
+ // of the destination, otherwise ENR (manual).
+ const gpsPhase = (() => {
+ const wps = flightPlan?.waypoints || [];
+ if (!wps.length) return 'ENR';
+ if (wps.some((w) => w.appr)) return 'APR';
+ const d = wps[wps.length - 1];
+ const dd = brgDist(num(V.lat), num(V.lon), d.lat, d.lon).dist;
+ return dd < 30 ? 'TERM' : 'ENR';
+ })();
const [tune, setTune] = useState(null); // radio being tuned (tap a freq)
// Eased values so the tapes + heading rose glide between X-Plane's ~20 Hz
// samples (VSI a touch softer; attitude is smoothed separately, imperatively).
@@ -219,9 +229,9 @@ export default function PFD({ values: V, command, connected = true, svt = true,
-
+
-
+
@@ -386,7 +396,7 @@ function AFCS({ V }) {
]);
const vrt = pick([
['GS', st('gsStatus')], ['ALT', st('altStatus')], ['VS', st('vsStatus')],
- ['FLC', st('flcStatus')], ['VNV', st('vnavStatus')],
+ ['FLC', st('flcStatus')], ['VPTH', st('vnavStatus')],
]);
if (!lat.act && fd) lat.act = 'ROL'; // wings-level default
if (!vrt.act && fd) vrt.act = 'PIT'; // pitch-hold default
@@ -599,7 +609,7 @@ function AirspeedTape({ V, ias: iasProp }) {
}
/* ---------------- altitude tape + VSI + baro ---------------- */
-function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums }) {
+function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums, vnav }) {
const alt = altProp != null ? altProp : num(V.altitude);
const vs = vsProp != null ? vsProp : num(V.vspeed);
const altBug = num(V.apAltBug), baro = num(V.baro, 29.92);
@@ -683,17 +693,37 @@ function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums }
)}
)}
- {/* VSI to the right */}
-
+ {/* VNAV: magenta flight-plan target altitude (upper-right of the scale, S.110)
+ + V DEV chevron on a small deviation scale left of the tape (S.113) */}
+ {vnav && (
+
+ {vnav.tgtAlt}FT
+ {/* V DEV scale (left) — only in VNAV, not on an ILS (where the GS shows there) */}
+ {!isILS(V.nav1) && (() => {
+ const dy = Math.max(-1, Math.min(1, -vnav.vDev / 300)) * (h / 2 - 26);
+ return (
+
+ {[-1, 1].map((k) => )}
+ V
+
+ );
+ })()}
+
+ )}
+ {/* VSI to the right (+ magenta VS TGT chevron in VNAV) */}
+
);
}
-function VSI({ x, cy, h, vs, bug }) {
+function VSI({ x, cy, h, vs, bug, vsTgt }) {
const max = 2000, top = cy - h / 2 + 10, bot = cy + h / 2 - 10;
const yOf = (v) => cy - (Math.max(-max, Math.min(max, v)) / max) * (h / 2 - 10);
return (
+ {vsTgt != null && Math.abs(vsTgt) > 20 && (
+
+ )}
{[2000, 1000, 0, -1000, -2000].map((v) => (
@@ -709,7 +739,7 @@ function VSI({ x, cy, h, vs, bug }) {
}
/* ---------------- HSI compass rose ---------------- */
-function HSI({ V, nav, hdg: hdgProp, obs = false }) {
+function HSI({ V, nav, hdg: hdgProp, obs = false, phase = 'ENR' }) {
const hdg = hdgProp != null ? hdgProp : ((num(V.heading) % 360) + 360) % 360;
const bug = num(V.apHdgBug);
// CDI source mirrors the in-sim G1000: 2 = GPS (magenta), 0/1 = VLOC1/2 (green).
@@ -762,7 +792,7 @@ function HSI({ V, nav, hdg: hdgProp, obs = false }) {
{/* CDI source label (GPS magenta / VLOC green) */}
{srcLabel}
- {isGps && ENR}
+ {isGps && {phase}}
{/* course pointer + CDI */}
diff --git a/web/src/styles.css b/web/src/styles.css
index 3e617e5..de274d4 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -248,7 +248,9 @@ body {
.r-wpt b { font-weight: 700; } .r-wpt b.cur { background: #19b8e6; color: #042230; padding: 0 4px; border-radius: 1px; }
.r-dtk, .r-dis, .r-cum { color: #e7edf2; text-align: right; }
.r-alt { color: #6f808d; text-align: right; }
+.r-alt { cursor: pointer; }
.r-alt.dsgn { color: #4fa8ff; } /* designated (VNAV) altitude = blue, per manual S.105 */
+.r-alt.refr { color: #e7edf2; } /* reference altitude = white (not in the VNAV profile) */
/* CURRENT VNV PROFILE panel (MFD flight-plan page) */
.fpl-vnav { border-top: 1px solid #2c343c; padding: 6px 12px 8px; font-family: 'Roboto Mono', monospace; }
.fpl-vnav-h { color: #36d2ff; font-size: 11px; letter-spacing: 1px; margin-bottom: 5px; }