From 6738e6085b0b9b63435215ff92250a2b14120868 Mon Sep 17 00:00:00 2001 From: karim Date: Tue, 2 Jun 2026 12:07:06 +0200 Subject: [PATCH] =?UTF-8?q?G1000:=20deepen=20VNAV/PFD=20=E2=80=94=20V-DEV?= =?UTF-8?q?=20&=20VS-TGT=20chevrons,=20GPS=20phase,=20designated=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PFD VNAV: magenta flight-plan target altitude on the alt scale (S.110), V DEV deviation scale + chevron (left, shown in VNAV when not on an ILS), VS TGT chevron on the VSI (S.113) - GPS phase annunciation is now dynamic: APR (approach leg) / TERM (<30 nm to destination) / ENR, instead of a fixed label - flight-plan ALT can be toggled designated(blue) <-> reference(white) by clicking the cell (S.106); only designated altitudes drive the VNAV profile - setPlan now preserves the dsgn/appr waypoint flags across the shared plan - AFCS vertical mode labelled VPTH (manual) instead of VNV Co-Authored-By: Claude Opus 4.8 --- server/bridge.js | 2 +- server/flightplan.js | 2 +- web/src/components/FplPage.jsx | 10 +++++-- web/src/components/PFD.jsx | 50 +++++++++++++++++++++++++++------- web/src/styles.css | 2 ++ 5 files changed, 52 insertions(+), 14 deletions(-) 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; }