diff --git a/web/src/citation.css b/web/src/citation.css index 8b32a93..ebfc9a9 100644 --- a/web/src/citation.css +++ b/web/src/citation.css @@ -33,8 +33,8 @@ font-family: 'Roboto Mono','Consolas',monospace; } .cit-pfd { aspect-ratio: 800 / 940; } -.cit-mfd { aspect-ratio: 1 / 1; } -.cit-eicas { aspect-ratio: 760 / 900; } +.cit-mfd { aspect-ratio: 800 / 940; } +.cit-eicas { aspect-ratio: 800 / 940; } .cit-pfd text, .cit-mfd text, .cit-eicas text { font-family: 'Roboto Mono','Consolas',monospace; } /* ---- bezel (soft-key / knob strip beneath a display) ---- */ diff --git a/web/src/components/citation/CitMFD.jsx b/web/src/components/citation/CitMFD.jsx index 059e56a..514cd67 100644 --- a/web/src/components/citation/CitMFD.jsx +++ b/web/src/components/citation/CitMFD.jsx @@ -32,6 +32,8 @@ export default function CitMFD({ xp }) { const [ov, setOv] = useState({ traffic: true, terrain: false, apts: true, vor: true }); const [setup, setSetup] = useState(null); // null | 'mfd' | 'eicas' | 'pfd' const [vspd, setVspd] = useState(false); + const [baroUnit, setBaroUnit] = useState('in'); // PFD SETUP: IN / HPA + const [eicasSub, setEicasSub] = useState('fuel'); // EICAS SYS subset page const [et, setEt] = useState(0); const etRun = useRef(false); useEffect(() => { const id = setInterval(() => etRun.current && setEt((t) => t + 1), 1000); return () => clearInterval(id); }, []); @@ -41,8 +43,9 @@ export default function CitMFD({ xp }) { const lon = useEased(num(V.lon), 0.14); const hdg = useEasedAngle(num(V.heading), 0.10); const trk = num(V.track); - // arc map geometry: ownship near bottom, ~120° forward arc - const W = 760, H = 760, cx = W / 2, cy = 600, R = 470; // compass radius + // arc map geometry: portrait DU-870 tube (identical to the PFD); ownship low, + // ~120° forward arc filling the upper two-thirds, data blocks along the bottom. + const W = 800, H = 940, cx = 400, cy = 740, R = 540; // compass radius const pxPerNm = R / rng; const project = (d, brg) => { // heading-up const rel = toRad(brg - hdg); @@ -71,6 +74,10 @@ export default function CitMFD({ xp }) { } } + // coupled nav source (Nav Source Selector): FMS flight plan vs VOR1/VOR2. + const cdiSrc = num(V.cdiSrc); + const src = cdiSrc === 2 ? 'fms' : 'nav'; + const srcLabel = cdiSrc === 2 ? 'FMS1' : cdiSrc === 1 ? 'VOR2' : 'VOR1'; const gs = Math.round(num(V.groundspeed) * 1.94384); const tas = Math.round(num(V.tas)); const sat = Math.round(num(V.oat)); @@ -84,16 +91,16 @@ export default function CitMFD({ xp }) { return (
- - - + + + - {/* heading box (#2,#20) + FMS source (#4) */} + {/* heading box (#2,#20) + FMS/NAV source (#4) */} HDG {String(Math.round(mod360(num(V.apHdgBug)))).padStart(3, '0')} - - {String(Math.round(mod360(hdg))).padStart(3, '0')}° - FMS1 + + {String(Math.round(mod360(hdg))).padStart(3, '0')}° + {srcLabel} {/* compass arc (#3) */} @@ -137,7 +144,7 @@ export default function CitMFD({ xp }) { {/* data group (#8) bottom-right */} - + NM {rng} ETE{eteMin > 0 ? fmt(eteMin * 60) : '– –'} @@ -145,9 +152,29 @@ export default function CitMFD({ xp }) { TAS{tas} GSPD{gs} + {/* EICAS SYS subset (#11) — a sub-set of the dedicated EICAS display */} + {setup === 'eicas' && (() => { + const a = (x, i) => (Array.isArray(x) ? num(x[i]) : num(x)); + const rows = eicasSub === 'elec' ? [['DC VOLTS', `${a(V.volts, 0).toFixed(0)} / ${a(V.volts, 1).toFixed(0)}`], ['DC AMPS', `${Math.round(a(V.genAmps, 0))} / ${Math.round(a(V.genAmps, 1) || a(V.genAmps, 0))}`], ['BATT', `${a(V.battVolt, 0).toFixed(1)}V`]] + : eicasSub === 'apu' ? [['APU RPM', '0%'], ['APU EGT', '— °C'], ['BLEED', 'OFF']] + : eicasSub === 'eng' ? [['N1 L/R', `${a(V.n1, 0).toFixed(1)} / ${a(V.n1, 1).toFixed(1)}`], ['N2 L/R', `${a(V.n2, 0).toFixed(0)} / ${a(V.n2, 1).toFixed(0)}`], ['ITT L/R', `${Math.round(a(V.itt, 0))} / ${Math.round(a(V.itt, 1))}`]] + : [['FUEL QTY', `${Math.round((a(V.fuelQty, 0) + a(V.fuelQty, 1)) * 2.20462)} LB`], ['FLOW L/R', `${Math.round(a(V.fuelFlow, 0) * 7936.6)} / ${Math.round(a(V.fuelFlow, 1) * 7936.6)}`], ['HYD A/B', `${Math.round(a(V.hydPress, 0))} / ${Math.round(a(V.hydPress, 1))}`]]; + return ( + + + EICAS · {eicasSub.toUpperCase()} + {rows.map(([k, v], i) => ( + + {k} + {v} + + ))} + + ); + })()} {/* V-SPEEDS reference card (#10) — Citation X operating speeds, manual p80 */} {vspd && ( - + V-SPEEDS · CITATION X {[['Vr (rotate)', '145'], ['Vfe (flaps)', '180'], ['Vmo SL-8000', '270'], ['Vmo >8000', '350'], @@ -161,7 +188,7 @@ export default function CitMFD({ xp }) { )} {/* clock / ET + WX status (#12,#16) bottom-left */} - + {now.toTimeString().slice(0, 8)} CLOCK @@ -180,6 +207,20 @@ export default function CitMFD({ xp }) { setOv((o) => ({ ...o, vor: !o.vor }))} /> setSetup(null)} /> + ) : setup === 'pfd' ? ( + <> + setBaroUnit('in')} /> + setBaroUnit('hpa')} /> + setSetup(null)} /> + + ) : setup === 'eicas' ? ( + <> + setEicasSub('fuel')} /> + setEicasSub('elec')} /> + setEicasSub('apu')} /> + setEicasSub('eng')} /> + setSetup(null)} /> + ) : ( <> diff --git a/web/src/components/citation/CitPFD.jsx b/web/src/components/citation/CitPFD.jsx index 4a0cd77..3077adb 100644 --- a/web/src/components/citation/CitPFD.jsx +++ b/web/src/components/citation/CitPFD.jsx @@ -22,7 +22,7 @@ const hz2mhz = (hz) => (num(hz) / 100).toFixed(2); // Citation X operating speeds (manual p80): Vso 115 · Vs1 136 · Vfe 180 · // Vmo 270 (SL-8000') / 350 (above) · Mmo 0.935. const VSO = 115, VS1 = 136, VFE = 180; -function SpeedTape({ ias, mach, bug, alt }) { +function SpeedTape({ ias, mach, bug, alt, trendKt }) { const H = 560, mid = H / 2, pxkt = 3.4; // 3.4 px per knot const y = (s) => mid + (ias - s) * pxkt; const vmo = alt > 8000 ? 350 : 270; @@ -49,6 +49,13 @@ function SpeedTape({ ias, mach, bug, alt }) { {/* Vfe flap-limit marker */} {marks} + {/* airspeed trend vector (#3): magenta line from the index up/down */} + {Math.abs(trendKt) > 1 && ( + + + 0 ? 8 : -8)} 75,${mid - trendKt * pxkt + (trendKt > 0 ? 8 : -8)}`} fill="#d24bd2" /> + + )} @@ -208,7 +215,7 @@ function Attitude({ pitch, roll, slip, fdP, fdR, fdOn }) { } // ── HSI (rotating compass with CDI + bearing pointers) ───────────────────────── -function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) { +function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel, srcColor }) { const R = 150; const card = []; for (let d = 0; d < 360; d += 5) { @@ -237,15 +244,15 @@ function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) { {card} {/* heading bug (magenta) */} - {/* course pointer + CDI deviation (cyan), #5 + #15 */} + {/* course pointer + CDI deviation (#5 + #15) — FMS magenta / VOR green */} - - - + + + {/* CDI bar */} - + {[-2, -1, 1, 2].map((d) => )} - {toFrom !== 0 && 0 ? '0,-14 -8,2 8,2' : '0,14 -8,-2 8,-2'} fill="#19c3e0" />} + {toFrom !== 0 && 0 ? '0,-14 -8,2 8,2' : '0,14 -8,-2 8,-2'} fill={srcColor} />} {/* bearing pointers — #6 secondary NAV (cyan circle = BRG1, white diamond = BRG2) */} {brg1 != null && ptr(brg1, '#19c3e0', false)} @@ -253,7 +260,7 @@ function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel }) { {/* fixed lubber line + aircraft */} {String(Math.round(mod360(hdg))).padStart(3, '0')} - {srcLabel} + {srcLabel} ); } @@ -289,8 +296,20 @@ export default function CitPFD({ xp }) { // bearing pointers only when a station is received (finite, nonzero) const brg1 = (num(V.nav1Dme) > 0 || num(V.nav1Brg) > 0) ? brg1e : null; const brg2 = (num(V.nav2Dme) > 0 || num(V.nav2Brg) > 0) ? brg2e : null; - const srcLabel = num(V.cdiSrc) === 2 ? 'FMS1' : num(V.cdiSrc) === 1 ? 'VOR2' : 'VOR1'; - const dme = num(V.cdiSrc) === 1 ? num(V.nav2Dme) : num(V.nav1Dme); + // Nav source (Nav Source Selector, manual p24): 0 VOR1 · 1 VOR2 · 2 FMS. + // FMS course is magenta, VOR course is green (Honeywell convention). + const cdiSrc = num(V.cdiSrc); + const srcLabel = cdiSrc === 2 ? 'FMS1' : cdiSrc === 1 ? 'VOR2' : 'VOR1'; + const srcColor = cdiSrc === 2 ? '#d24bd2' : '#13e000'; + const cycleNav = () => xp.setDataref('cdiSrc', cdiSrc === 1 ? 0 : 1); // toggle VOR1↔VOR2 + const setFms = () => xp.setDataref('cdiSrc', 2); + const dme = cdiSrc === 1 ? num(V.nav2Dme) : num(V.nav1Dme); + // airspeed trend vector (#3): smoothed acceleration projected 10 s ahead + const t = trend.current, nowS = (typeof performance !== 'undefined' ? performance.now() : Date.now()) / 1000; + const dt = Math.min(0.5, Math.max(0.001, nowS - (t.t || nowS))); + const rate = (ias - (t.ias != null ? t.ias : ias)) / dt; // kt/s + t.s = (t.s || 0) * 0.92 + rate * 0.08; t.ias = ias; t.t = nowS; + const trendKt = clamp(t.s * 10, -45, 45); return (
@@ -299,7 +318,7 @@ export default function CitPFD({ xp }) { {/* attitude */} {/* speed tape (#2,#3) */} - + KIAS {/* AOA index (#manual p22) */} @@ -308,7 +327,7 @@ export default function CitPFD({ xp }) { {/* VSI (#13,#14) */} {/* HSI (#10) */} - + {/* CRS / HDG digital (#5,#7) */} @@ -334,8 +353,14 @@ export default function CitPFD({ xp }) { FT - {/* bezel buttons — MINIMUMS rotary (#8), RA/BARO (#9), STD (#11), BARO SET (#12) */} + {/* bezel buttons — Nav Source Selector (p24, sits under the PFD), MINIMUMS + rotary (#8), RA/BARO (#9), STD (#11), BARO SET (#12), CRS, HDG */}
+
+ NAV SRC + + +
MINIMUMS