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:
+80
-17
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user