Citation X cockpit profile: full Primus 2000 suite (PFD/MFD/EICAS/AP/RMU)

Add a switchable cockpit-profile selector (Garmin G1000 / Cessna Citation X /
GA steam) and recreate the Citation X Honeywell Primus 2000 avionics line-for-
line from the X-Plane Citation X + FMS manuals:

- CitPFD: attitude w/ FD command bars, speed tape (Vmo barber-pole, Vfe, low-
  speed red/amber bands), AOA index, altitude tape + trend, VSI, round HSI with
  CDI/course pointer + VOR/ADF bearing pointers, radar altimeter, minimums,
  STD/BARO/CRS/HDG bezel.
- CitEICAS: twin FAN%/ITT bar gauges, OIL °C/PSI, FUEL (flow/qty PPH·LBS),
  ELECTRICAL, HYDRAULICS, slat chevron, STAB trim, FLAPS, CAS message stack,
  softkeys NORM/FUEL-HYD/ELEC/CTRL-POS/ENG + control-position overlay.
- CitMFD: Honeywell heading-up arc map, FMS route (magenta active/white future),
  TCAS, terrain/WX, range arc, ETE/SAT/TAS/GSPD block, clock + ET/FT timer,
  V-SPEEDS reference card, MFD-setup overlays (TRAFFIC/TERRAIN/APTS/VOR).
- CitAP: HDG/NAV/APP/BC · ALT/VNAV/BANK/STBY · FLC/C-O/VS · pitch wheel ·
  AP/YD/M-TRIM/PFD-SEL, FMA bar + lamps from per-mode *_status datarefs.
- CitRMU: COM/NAV active+standby tuning, transponder, ADF, TCAS range/mode,
  IDENT + Nav Source Selector (NAV1/2/FMS, VOR/ADF/FMS bearing source).

Integration: all avionics stream live via the X-Plane Web API (new datarefs for
N1/N2/ITT, radar-alt, AOA, hydraulics, trim, flaps/slats/gear, control
positions, ADF, mach, yaw-damper); the existing fms-sync.lua drives the
Citation's built-in FMS (aircraft-agnostic XPLM FMS SDK). Demo seeds added so
every panel renders offline. Verified headless via Playwright (no console
errors; G1000/GA profiles unaffected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 12:09:55 +02:00
parent aa64959eea
commit b05ffedbc1
11 changed files with 1298 additions and 17 deletions
+80 -17
View File
@@ -11,6 +11,12 @@ import DirectTo from './components/DirectTo.jsx';
import Proc from './components/Proc.jsx';
import FplPage from './components/FplPage.jsx';
import AudioPanel from './components/AudioPanel.jsx';
import KAP140 from './components/KAP140.jsx';
import CitPFD from './components/citation/CitPFD.jsx';
import CitMFD from './components/citation/CitMFD.jsx';
import CitEICAS from './components/citation/CitEICAS.jsx';
import CitAP from './components/citation/CitAP.jsx';
import CitRMU from './components/citation/CitRMU.jsx';
// Compact line icons for the nav rail (stroke = currentColor).
const ICONS = {
@@ -21,29 +27,58 @@ const ICONS = {
ap: 'M11 4a7 7 0 100 14 7 7 0 000-14zM11 4v3M11 15v3M4 11h3M15 11h3',
vfr: 'M11 4a7 7 0 100 14 7 7 0 000-14zM11 11l4.5-3',
audio: 'M11 4a6 6 0 00-6 6v5M17 15v-5a6 6 0 00-6-6M4 14h2.5v4.5H4zM15.5 14H18v4.5h-2.5z',
eicas: 'M5 4v14M9 4v14M5 11h4M13 7h5M13 11h5M13 15h5',
rmu: 'M4 5h14v12H4zM7 8h8M7 11h8M7 14h4',
};
function Icon({ name }) {
return (
<svg className="snav-ic" viewBox="0 0 22 22" width="22" height="22" fill="none"
stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d={ICONS[name]} />
<path d={ICONS[name] || ICONS.mfd} />
{name === 'map' && <circle cx="11" cy="8" r="2" />}
</svg>
);
}
const TABS = [
{ id: 'pfd', label: 'PFD' },
{ id: 'mfd', label: 'MFD' },
{ id: 'map', label: 'Map' },
{ id: 'fms', label: 'FMS' },
{ id: 'vfr', label: 'VFR' },
{ id: 'ap', label: 'Autopilot' },
{ id: 'audio', label: 'Audio' },
];
// Three selectable cockpit profiles. Each maps the app to a different aircraft's
// avionics suite, sharing the same bridge/datarefs underneath.
const PROFILES = {
g1000: {
label: 'Garmin G1000', short: 'G1000',
tabs: [
{ id: 'pfd', label: 'PFD' }, { id: 'mfd', label: 'MFD' }, { id: 'map', label: 'Map' },
{ id: 'fms', label: 'FMS' }, { id: 'vfr', label: 'VFR' }, { id: 'ap', label: 'Autopilot' },
{ id: 'audio', label: 'Audio' },
],
},
citation: {
label: 'Cessna Citation X', short: 'CITATION X',
tabs: [
{ id: 'pfd', label: 'PFD' }, { id: 'mfd', label: 'MFD' }, { id: 'eicas', label: 'EICAS' },
{ id: 'fms', label: 'CDU/FMS' }, { id: 'ap', label: 'Autopilot' }, { id: 'rmu', label: 'Radios' },
{ id: 'map', label: 'Map' },
],
},
ga: {
label: 'GA Steam (Bendix/King)', short: 'GA PANEL',
tabs: [
{ id: 'vfr', label: 'Panel' }, { id: 'ap', label: 'KAP 140' },
{ id: 'map', label: 'Map' }, { id: 'audio', label: 'Audio' },
],
},
};
export default function App() {
const xp = useXplane();
// Active cockpit profile — persisted; switches the whole avionics suite.
const [profile, setProfile] = useState(() => localStorage.getItem('cockpitProfile') || 'g1000');
const [profMenu, setProfMenu] = useState(false);
const PROF = PROFILES[profile] || PROFILES.g1000;
const TABS = PROF.tabs;
const pickProfile = (p) => {
localStorage.setItem('cockpitProfile', p); setProfile(p); setProfMenu(false);
const first = PROFILES[p].tabs[0].id; setTab(first); history.replaceState(null, '', `#${first}`);
};
const [tab, setTab] = useState(() => location.hash.replace('#', '') || 'pfd');
// Collapsible nav rail: narrow (icons) ↔ wide (icons + labels), remembered.
const [navWide, setNavWide] = useState(() => localStorage.getItem('navWide') === '1');
@@ -93,6 +128,11 @@ export default function App() {
const uiInset = xp.values.uiInset, uiPage = xp.values.uiMfdPage;
useEffect(() => { if (uiInset === 0 || uiInset === 1) setInset(!!uiInset); }, [uiInset]);
useEffect(() => { if (typeof uiPage === 'number' && MFD_PAGES[uiPage]) setMfdPage(MFD_PAGES[uiPage]); }, [uiPage]);
// Keep the active tab valid for the current profile (e.g. after a hash deep-link
// into a tab the profile doesn't have).
useEffect(() => {
if (!TABS.some((t) => t.id === tab)) { const f = TABS[0].id; setTab(f); history.replaceState(null, '', `#${f}`); }
}, [profile]); // eslint-disable-line react-hooks/exhaustive-deps
const connKind = xp.xpConnected ? 'ok' : xp.connected ? 'warn' : 'bad';
const connText = xp.xpConnected ? 'X-PLANE' : xp.connected ? 'NO SIM' : 'OFFLINE';
@@ -131,10 +171,22 @@ export default function App() {
return (
<div className={`app ${navWide ? 'nav-wide' : 'nav-narrow'}`}>
<aside className="sidebar">
<button className="sb-top" onClick={toggleNav} title="Menü ein-/ausklappen">
<span className="brand">G<span>1000</span></span>
<span className="sb-chev">{navWide ? '' : ''}</span>
<button className="sb-top" onClick={() => setProfMenu((v) => !v)} title="Cockpit-Profil wählen">
<span className="brand">{PROF.short}</span>
<span className="sb-chev">{profMenu ? '' : ''}</span>
</button>
{profMenu && (
<div className="prof-menu">
{Object.entries(PROFILES).map(([id, p]) => (
<button key={id} className={`prof-i ${id === profile ? 'on' : ''}`} onClick={() => pickProfile(id)}>
{p.label}
</button>
))}
<button className="prof-collapse" onClick={() => { setProfMenu(false); toggleNav(); }}>
{navWide ? '◂ Menü einklappen' : '▸ Menü ausklappen'}
</button>
</div>
)}
<nav className="snav">
{TABS.map((t) => (
<button key={t.id} className={tab === t.id ? 'snav-i active' : 'snav-i'}
@@ -158,7 +210,8 @@ export default function App() {
</aside>
<main className="screen">
{tab === 'pfd' && (
{/* ---- Garmin G1000 suite ---- */}
{profile === 'g1000' && tab === 'pfd' && (
<Bezel variant="pfd" xp={xp} knobMode={knobMode} svt3d={svt3d} onToggleSvt={() => setSvt3d((v) => !v)}
svtOpts={svtOpts} onSvtOpt={setSvtOpts}
inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode}
@@ -173,17 +226,27 @@ export default function App() {
{dialogs}
</Bezel>
)}
{tab === 'mfd' && (
{profile === 'g1000' && tab === 'mfd' && (
<Bezel variant="mfd" xp={xp} knobMode={knobMode} mapMode={mapMode} onMapMode={setMapMode} onDirect={() => toggleWin('dto')} onProc={() => toggleWin('proc')} onFms={cycleMfd} onFpl={() => setMfdPage('fpl')} onMenu={() => toggleWin('menu')} onClr={() => setWin(null)}>
<MFD values={xp.values} flightPlan={xp.flightPlan} fp={xp.fp} mapMode={mapMode} page={mfdPage} onCycle={cycleMfd} xp={xp} vnav={vnavCfg} onVnav={setVnavCfg} />
{dialogs}
</Bezel>
)}
{/* ---- Cessna Citation X suite (Honeywell Primus 2000) ---- */}
{profile === 'citation' && tab === 'pfd' && <CitPFD xp={xp} />}
{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} />}
{/* ---- shared tabs ---- */}
{tab === 'map' && <MapView values={xp.values} flightPlan={xp.flightPlan} fp={xp.fp} />}
{tab === 'fms' && <CDU xp={xp} />}
{tab === 'fms' && <CDU xp={xp} vnav={vnavCfg} onVnav={setVnavCfg} />}
{tab === 'vfr' && <VFR xp={xp} />}
{tab === 'ap' && <AutopilotPanel xp={xp} />}
{tab === 'audio' && <AudioPanel xp={xp} />}
{tab === 'ap' && profile === 'g1000' && <AutopilotPanel xp={xp} />}
{tab === 'ap' && profile === 'ga' && <KAP140 xp={xp} />}
</main>
{settings && (
<div className="dlg-backdrop" onClick={() => setSettings(false)}>