Citation: ADF/VOR/FMS bearing pointers wired through + PFD FMA mode bar

- Nav Source Selector (p24) now fully drives the PFD: shared brg1/brg2 state
  (App ↔ RMU ↔ PFD). RMU has both pointer knobs — ◯ blue (OFF/VOR1/ADF1/FMS1)
  and ◇ white (OFF/VOR2/ADF2/FMS2). The PFD resolves each to a magnetic bearing
  (VOR bearing, ADF relative+heading, GPS bearing) and the HSI legend reflects
  the selected sources.
- PFD FMA / AFCS mode annunciation bar across the top (lateral · AP/FD ·
  vertical) reading the per-mode *_status datarefs — active green, armed white,
  matching the real Primus PFD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:10:13 +02:00
parent e8dfa84266
commit 0ceb1dede3
4 changed files with 61 additions and 20 deletions
+6 -3
View File
@@ -74,6 +74,9 @@ export default function App() {
const xp = useXplane();
// Active cockpit profile — persisted; switches the whole avionics suite.
const [profile, setProfile] = useState(() => localStorage.getItem('cockpitProfile') || 'g1000');
// Citation Nav Source Selector bearing-pointer sources (p24): pointer 1 = cyan
// circle, pointer 2 = white diamond. Each OFF/VORn/ADFn/FMSn. Shared PFD↔RMU.
const [navSrc, setNavSrc] = useState({ brg1: 'VOR1', brg2: 'VOR2' });
const [profMenu, setProfMenu] = useState(false);
const PROF = PROFILES[profile] || PROFILES.g1000;
const TABS = PROF.tabs;
@@ -236,12 +239,12 @@ export default function App() {
)}
{/* ---- Cessna Citation X suite (Honeywell Primus 2000) ---- */}
{profile === 'citation' && tab === 'duo' && <CitDuo xp={xp} />}
{profile === 'citation' && tab === 'pfd' && <CitPFD xp={xp} />}
{profile === 'citation' && tab === 'duo' && <CitDuo xp={xp} navSrc={navSrc} />}
{profile === 'citation' && tab === 'pfd' && <CitPFD xp={xp} navSrc={navSrc} />}
{profile === 'citation' && tab === 'mfd' && <CitMFD xp={xp} />}
{profile === 'citation' && tab === 'eicas' && <CitEICAS xp={xp} />}
{profile === 'citation' && tab === 'ap' && <CitAP xp={xp} />}
{profile === 'citation' && tab === 'rmu' && <CitRMU xp={xp} />}
{profile === 'citation' && tab === 'rmu' && <CitRMU xp={xp} navSrc={navSrc} onNavSrc={setNavSrc} />}
{/* ---- shared tabs ---- */}
{tab === 'map' && <MapView values={xp.values} flightPlan={xp.flightPlan} fp={xp.fp} />}
+2 -2
View File
@@ -5,10 +5,10 @@ import CitMFD from './CitMFD.jsx';
// Side-by-side PFD + MFD — the two pilot tubes of the Citation X panel on one
// tablet screen (landscape). Each keeps its own bezel/soft-keys; they scale to
// fill half the width like the real instrument panel (DU-870 displays).
export default function CitDuo({ xp }) {
export default function CitDuo({ xp, navSrc }) {
return (
<div className="cit-duo">
<div className="cit-duo-half"><CitPFD xp={xp} /></div>
<div className="cit-duo-half"><CitPFD xp={xp} navSrc={navSrc} /></div>
<div className="cit-duo-half"><CitMFD xp={xp} /></div>
</div>
);
+42 -11
View File
@@ -265,8 +265,9 @@ function HSI({ hdg, trk, crs, hdgBug, cdi, toFrom, brg1, brg2, srcLabel, srcColo
);
}
export default function CitPFD({ xp }) {
export default function CitPFD({ xp, navSrc }) {
const V = xp.values || {};
const bsrc = navSrc || { brg1: 'VOR1', brg2: 'VOR2' };
const [std, setStd] = React.useState(false);
const [raBaro, setRaBaro] = React.useState(false); // #9 RA/BARO minimums source
const [min, setMin] = React.useState({ on: false, ft: 200 });
@@ -287,15 +288,37 @@ export default function CitPFD({ xp }) {
const cdi = useEased(num(V.hsiDef), 0.12);
const mach = useEased(num(V.mach), 0.2);
const aoa = useEased(num(V.aoa), 0.12);
const brg1e = useEasedAngle(num(V.nav1Brg), 0.12);
const brg2e = useEasedAngle(num(V.nav2Brg), 0.12);
const hdgRaw = num(V.heading);
const vor1e = useEasedAngle(num(V.nav1Brg), 0.12);
const vor2e = useEasedAngle(num(V.nav2Brg), 0.12);
const adf1e = useEasedAngle(mod360(hdgRaw + num(V.adf1Brg)), 0.12); // ADF relative → mag bearing
const adf2e = useEasedAngle(mod360(hdgRaw + num(V.adf2Brg)), 0.12);
const gpsBrgE = useEasedAngle(num(V.gpsBearing), 0.12);
const trk = num(V.track), toFrom = num(V.hsiToFrom);
const baro = num(V.baro, 29.92);
const radAlt = num(V.radioAlt, 99999);
const fdOn = num(V.apMode) >= 1 || num(V.apEngaged) > 0;
// 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;
// bearing pointer source (Nav Source Selector): resolve each pointer to a
// magnetic bearing or null (no station / OFF). Pointer 1 = cyan ◯, 2 = white ◇.
const pickBrg = (sel) => {
if (sel === 'VOR1') return (num(V.nav1Dme) > 0 || num(V.nav1Brg) > 0) ? vor1e : null;
if (sel === 'VOR2') return (num(V.nav2Dme) > 0 || num(V.nav2Brg) > 0) ? vor2e : null;
if (sel === 'ADF1') return num(V.adf1Brg) ? adf1e : null;
if (sel === 'ADF2') return num(V.adf2Brg) ? adf2e : null;
if (sel === 'FMS1' || sel === 'FMS') return num(V.gpsBearing) ? gpsBrgE : null;
return null;
};
const brg1 = pickBrg(bsrc.brg1);
const brg2 = pickBrg(bsrc.brg2);
// FMA / AFCS mode annunciation (active = green, armed = white)
const st = (k) => num(V[k]);
const latM = st('aprStatus') ? ['LOC', st('aprStatus')] : (st('navStatus') || st('gpssStatus')) ? ['NAV', Math.max(st('navStatus'), st('gpssStatus'))]
: st('bcStatus') ? ['BC', st('bcStatus')] : st('hdgStatus') ? ['HDG', st('hdgStatus')] : ['ROLL', 2];
const vertM = st('gsStatus') ? ['GS', st('gsStatus')] : st('vnavStatus') ? ['VNAV', st('vnavStatus')]
: st('flcStatus') ? ['FLC', st('flcStatus')] : st('vsStatus') ? ['VS', st('vsStatus')]
: st('altStatus') ? ['ALT', st('altStatus')] : ['PITCH', 2];
const apTxt = num(V.apEngaged) > 0 || num(V.apMode) >= 2 ? 'AP' : fdOn ? 'FD' : '';
const fmaColor = (s) => (s >= 2 ? '#16e000' : '#fff');
// 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);
@@ -315,6 +338,14 @@ export default function CitPFD({ xp }) {
<div className="cit-screen">
<svg className="cit-pfd" viewBox="0 0 800 940" preserveAspectRatio="xMidYMid meet">
<rect x={0} y={0} width={800} height={940} fill="#05080b" />
{/* FMA / AFCS mode annunciation bar (active green · armed white) */}
<g transform="translate(204 8)">
<rect x={0} y={0} width={392} height={26} fill="#0a0e12" stroke="#2a3138" />
<line x1={130} y1={2} x2={130} y2={24} stroke="#2a3138" /><line x1={262} y1={2} x2={262} y2={24} stroke="#2a3138" />
<text x={65} y={18} fontSize="14" fill={fmaColor(latM[1])} textAnchor="middle" fontWeight="700">{latM[0]}</text>
<text x={196} y={18} fontSize="14" fill="#16e000" textAnchor="middle" fontWeight="700">{apTxt}</text>
<text x={328} y={18} fontSize="14" fill={fmaColor(vertM[1])} textAnchor="middle" fontWeight="700">{vertM[0]}</text>
</g>
{/* attitude */}
<g transform="translate(400 270)"><Attitude pitch={pitch} roll={roll} slip={slip} fdP={num(V.fdPitch)} fdR={num(V.fdRoll)} fdOn={fdOn} /></g>
{/* speed tape (#2,#3) */}
@@ -336,12 +367,12 @@ export default function CitPFD({ xp }) {
<text x={20} y={642} fill="#d24bd2">HDG</text>
<text x={20} y={662} fill="#d24bd2" fontSize="20">{String(Math.round(mod360(hdgBug))).padStart(3, '0')}</text>
</g>
{/* secondary NAV legend (#6) */}
{/* secondary NAV legend (#6) — reflects the Nav Source Selector */}
<g fontSize="13">
<circle cx={28} cy={726} r="6" fill="none" stroke="#19c3e0" strokeWidth="2" />
<text x={42} y={731} fill="#19c3e0">VOR1</text>
<rect x={22} y={744} width={12} height={12} fill="none" stroke="#cfd6dc" strokeWidth="2" transform="rotate(45 28 750)" />
<text x={42} y={755} fill="#cfd6dc">VOR2</text>
{bsrc.brg1 !== 'OFF' && <><circle cx={28} cy={726} r="6" fill="none" stroke="#19c3e0" strokeWidth="2" />
<text x={42} y={731} fill="#19c3e0">{bsrc.brg1}</text></>}
{bsrc.brg2 !== 'OFF' && <><rect x={22} y={744} width={12} height={12} fill="none" stroke="#cfd6dc" strokeWidth="2" transform="rotate(45 28 750)" />
<text x={42} y={755} fill="#cfd6dc">{bsrc.brg2}</text></>}
</g>
{/* DME (#16) */}
{dme > 0 && <text x={760} y={620} fontSize="16" fill="#13e000" textAnchor="end">{dme.toFixed(1)}NM</text>}
+11 -4
View File
@@ -15,14 +15,15 @@ const XPDR = ['OFF', 'STBY', 'ON', 'ALT'];
const TCAS_RNG = [6, 12, 20, 40];
const TCAS_MODE = ['NORMAL', 'ABOVE', 'BELOW'];
export default function CitRMU({ xp }) {
export default function CitRMU({ xp, navSrc, onNavSrc }) {
const V = xp.values || {};
const cmd = xp.command, sd = xp.setDataref;
const [bank, setBank] = useState(1); // tuning radio: 1 or 2
const [sel, setSel] = useState('com'); // which standby is armed for tuning: com|nav|adf|xpdr
const [tcasR, setTcasR] = useState(1); // index into TCAS_RNG
const [tcasM, setTcasM] = useState(0);
const [navSrc, setNavSrc] = useState('VOR1'); // bearing-pointer source (#6 secondary)
const bsrc = navSrc || { brg1: 'VOR1', brg2: 'VOR2' };
const setBrg = (k, v) => onNavSrc && onNavSrc((s) => ({ ...s, [k]: v }));
const r = bank; // 1 / 2
const tuneUp = () => cmd(`${sel}${r}CoarseUp`);
@@ -116,12 +117,18 @@ export default function CitRMU({ xp }) {
<div className="citnav-h2">BRG POINTER (blue)</div>
<div className="citnav-row">
{['OFF', 'VOR1', 'ADF1', 'FMS1'].map((s) => (
<button key={s} className={`citnav-b sm ${navSrc === s ? 'on' : ''}`} onClick={() => setNavSrc(s)}>{s}</button>
<button key={s} className={`citnav-b sm ${bsrc.brg1 === s ? 'on' : ''}`} onClick={() => setBrg('brg1', s)}>{s}</button>
))}
</div>
<div className="citnav-h2">BRG POINTER (white)</div>
<div className="citnav-row">
{['OFF', 'VOR2', 'ADF2', 'FMS2'].map((s) => (
<button key={s} className={`citnav-b sm ${bsrc.brg2 === s ? 'on' : ''}`} onClick={() => setBrg('brg2', s)}>{s}</button>
))}
</div>
<div className="citnav-note">
NAV CDI source (green) on the PFD · FMS = flight-plan guidance ·
BRG pointers ( blue / white) show VOR / ADF / FMS bearings.
BRG pointers ( blue VOR1/ADF1/FMS · white VOR2/ADF2) appear on the PFD HSI.
</div>
</div>
</div>