Fix 3rd autopilot UI (Bezel APController) + wire dead PATHWAY/APTSIGNS keys
Line-by-line control audit. APController (the MFD left-bezel autopilot mode controller) still decoded the unreliable autopilot_state bitfield — the same bug already fixed in AutopilotPanel and KAP140, missed in the third AP UI. Now reads per-mode *_status datarefs so every mode key lights correctly. PATHWAY and APTSIGNS softkeys were dead (sim-mirror only). PATHWAY now draws the flight-plan route on the synthetic-vision terrain; APTSIGNS toggles the runway labels. Threaded via App svtOpts → PFD → SVT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -74,6 +74,9 @@ export default function App() {
|
|||||||
// profile/chevrons, fpa is the descent angle (°), offsetNm levels off that far
|
// profile/chevrons, fpa is the descent angle (°), offsetNm levels off that far
|
||||||
// before the waypoint. See FplPage CURRENT VNV PROFILE + PFD chevrons.
|
// before the waypoint. See FplPage CURRENT VNV PROFILE + PFD chevrons.
|
||||||
const [vnavCfg, setVnavCfg] = useState({ enabled: true, fpa: 3, offsetNm: 0 });
|
const [vnavCfg, setVnavCfg] = useState({ enabled: true, fpa: 3, offsetNm: 0 });
|
||||||
|
// Synthetic-vision display options (PFD submenu): PATHWAY draws the flight-plan
|
||||||
|
// route on the 3D terrain; APTSIGNS shows runway/airport labels.
|
||||||
|
const [svtOpts, setSvtOpts] = useState({ pathway: true, aptSigns: true });
|
||||||
// Altimeter barometric units (false = inHg, true = hectopascal) — PFD ALT UNIT softkey.
|
// Altimeter barometric units (false = inHg, true = hectopascal) — PFD ALT UNIT softkey.
|
||||||
const [baroHpa, setBaroHpa] = useState(false);
|
const [baroHpa, setBaroHpa] = useState(false);
|
||||||
// Barometric minimums (set in TMR/REF) — shown on the PFD altimeter as BARO MIN.
|
// Barometric minimums (set in TMR/REF) — shown on the PFD altimeter as BARO MIN.
|
||||||
@@ -139,6 +142,7 @@ export default function App() {
|
|||||||
<main className="screen">
|
<main className="screen">
|
||||||
{tab === 'pfd' && (
|
{tab === 'pfd' && (
|
||||||
<Bezel variant="pfd" xp={xp} knobMode={knobMode} svt3d={svt3d} onToggleSvt={() => setSvt3d((v) => !v)}
|
<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}
|
inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode}
|
||||||
nrst={nrst} onToggleNrst={() => toggleWin('nrst')} onDirect={() => toggleWin('dto')}
|
nrst={nrst} onToggleNrst={() => toggleWin('nrst')} onDirect={() => toggleWin('dto')}
|
||||||
tmr={tmr} onToggleTmr={() => toggleWin('tmr')} dme={dme} onToggleDme={() => toggleWin('dme')}
|
tmr={tmr} onToggleTmr={() => toggleWin('tmr')} dme={dme} onToggleDme={() => toggleWin('dme')}
|
||||||
@@ -147,7 +151,7 @@ export default function App() {
|
|||||||
<PFD xp={xp} values={xp.values} command={xp.command} connected={xp.xpConnected} svt={svt3d} inset={inset} insetMode={insetMode} nrst={nrst} onCloseNrst={() => setWin(null)}
|
<PFD xp={xp} values={xp.values} command={xp.command} connected={xp.xpConnected} svt={svt3d} inset={inset} insetMode={insetMode} nrst={nrst} onCloseNrst={() => setWin(null)}
|
||||||
tmr={tmr} onCloseTmr={() => setWin(null)} dme={dme} onCloseDme={() => setWin(null)}
|
tmr={tmr} onCloseTmr={() => setWin(null)} dme={dme} onCloseDme={() => setWin(null)}
|
||||||
alerts={alerts} onCloseAlerts={() => setWin(null)} baroHpa={baroHpa} obs={obs}
|
alerts={alerts} onCloseAlerts={() => setWin(null)} baroHpa={baroHpa} obs={obs}
|
||||||
minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} vnav={vnavCfg} />
|
minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} vnav={vnavCfg} svtOpts={svtOpts} />
|
||||||
{dialogs}
|
{dialogs}
|
||||||
</Bezel>
|
</Bezel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ const MFD_MENU = {
|
|||||||
};
|
};
|
||||||
const KG_PER_GAL = 2.72; // fuel totalizer steps in US gallons (matches the EIS readout)
|
const KG_PER_GAL = 2.72; // fuel totalizer steps in US gallons (matches the EIS readout)
|
||||||
|
|
||||||
// autopilot_state bitfield (best-effort; tweak per aircraft)
|
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, svtOpts, onSvtOpt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onClr, onFms, mapMode, onMapMode, altHpa, onAltUnit, obs, onObs, knobMode = 'arrows', children }) {
|
||||||
const AP_BITS = { fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, nav: 1 << 8, apr: 1 << 9, vnav: 1 << 11, altHold: 1 << 14, bc: 1 << 18 };
|
|
||||||
|
|
||||||
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onClr, onFms, mapMode, onMapMode, altHpa, onAltUnit, obs, onObs, knobMode = 'arrows', children }) {
|
|
||||||
const u = variant === 'mfd' ? 'mfd' : 'pfd'; // command prefix
|
const u = variant === 'mfd' ? 'mfd' : 'pfd'; // command prefix
|
||||||
const fire = (suffix) => xp && xp.command(`${u}_${suffix}`);
|
const fire = (suffix) => xp && xp.command(`${u}_${suffix}`);
|
||||||
const [page, setPage] = useState('root'); // softkey menu page
|
const [page, setPage] = useState('root'); // softkey menu page
|
||||||
@@ -83,6 +80,8 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
|||||||
if (label === 'PFD') setPage('pfd');
|
if (label === 'PFD') setPage('pfd');
|
||||||
else if (label === 'BACK') setPage({ xpdrcode: 'xpdr', altunit: 'pfd' }[page] || 'root');
|
else if (label === 'BACK') setPage({ xpdrcode: 'xpdr', altunit: 'pfd' }[page] || 'root');
|
||||||
else if (label === 'SYN TERR') onToggleSvt && onToggleSvt();
|
else if (label === 'SYN TERR') onToggleSvt && onToggleSvt();
|
||||||
|
else if (label === 'PATHWAY') onSvtOpt && onSvtOpt((o) => ({ ...o, pathway: !o.pathway })); // route on the 3D terrain
|
||||||
|
else if (label === 'APTSIGNS') onSvtOpt && onSvtOpt((o) => ({ ...o, aptSigns: !o.aptSigns })); // runway/airport labels in SVT
|
||||||
else if (label === 'ALT UNIT') setPage('altunit');
|
else if (label === 'ALT UNIT') setPage('altunit');
|
||||||
else if (label === 'IN') { onAltUnit && onAltUnit(false); setPage('pfd'); }
|
else if (label === 'IN') { onAltUnit && onAltUnit(false); setPage('pfd'); }
|
||||||
else if (label === 'HPA') { onAltUnit && onAltUnit(true); setPage('pfd'); }
|
else if (label === 'HPA') { onAltUnit && onAltUnit(true); setPage('pfd'); }
|
||||||
@@ -120,7 +119,8 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
|||||||
|| (label === 'TERRAIN' && mapMode?.terrain) || (label === 'OSM' && mapMode?.base === 'osm')
|
|| (label === 'TERRAIN' && mapMode?.terrain) || (label === 'OSM' && mapMode?.base === 'osm')
|
||||||
|| (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways)
|
|| (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways)
|
||||||
|| (label === 'AIRSPACE' && mapMode?.airspace);
|
|| (label === 'AIRSPACE' && mapMode?.airspace);
|
||||||
return (label === 'SYN TERR' && svt3d) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr)
|
return (label === 'SYN TERR' && svt3d) || (label === 'PATHWAY' && svtOpts?.pathway) || (label === 'APTSIGNS' && svtOpts?.aptSigns)
|
||||||
|
|| (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr)
|
||||||
|| (label === 'DME' && dme) || (label === 'OBS' && obs) || (label === 'CAUTION' && (alerts || hasAlerts))
|
|| (label === 'DME' && dme) || (label === 'OBS' && obs) || (label === 'CAUTION' && (alerts || hasAlerts))
|
||||||
|| (label === 'STBY' && xpdrMode === 1) || (label === 'ON' && xpdrMode === 2) || (label === 'ALT' && xpdrMode === 3)
|
|| (label === 'STBY' && xpdrMode === 1) || (label === 'ON' && xpdrMode === 2) || (label === 'ALT' && xpdrMode === 3)
|
||||||
|| (label === 'IN' && !altHpa) || (label === 'HPA' && altHpa)
|
|| (label === 'IN' && !altHpa) || (label === 'HPA' && altHpa)
|
||||||
@@ -195,26 +195,30 @@ function BtnG({ fire, cmd, onClick, children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Autopilot mode controller (left bezel of the MFD). Buttons fire real X-Plane
|
// Autopilot mode controller (left bezel of the MFD). Buttons fire real X-Plane
|
||||||
// commands; active modes light up from autopilot_state / servos_on.
|
// commands; active modes light from the per-mode *_status datarefs (off/armed/
|
||||||
|
// active) — the same reliable source as the PFD bar and the AutopilotPanel, not
|
||||||
|
// the autopilot_state bitfield (whose bit positions don't match X-Plane).
|
||||||
function APController({ xp }) {
|
function APController({ xp }) {
|
||||||
const st = num(xp.values.apState);
|
const V = xp.values;
|
||||||
const on = (bit) => (st & bit) !== 0;
|
const lit = (k) => num(V[k]) > 0;
|
||||||
const eng = num(xp.values.apEngaged) > 0;
|
const apMode = num(V.apMode);
|
||||||
|
const eng = num(V.apEngaged) > 0 || apMode >= 2;
|
||||||
|
const fdOn = apMode >= 1 || eng;
|
||||||
const B = ({ label, cmd, active }) => (
|
const B = ({ label, cmd, active }) => (
|
||||||
<button className={`ap-key ${active ? 'on' : ''}`} onClick={() => xp.command(cmd)}>{label}</button>
|
<button className={`ap-key ${active ? 'on' : ''}`} onClick={() => xp.command(cmd)}>{label}</button>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="ap-controller">
|
<div className="ap-controller">
|
||||||
<B label="AP" cmd="apToggle" active={eng} />
|
<B label="AP" cmd="apToggle" active={eng} />
|
||||||
<B label="FD" cmd="fdToggle" active={on(AP_BITS.fd)} />
|
<B label="FD" cmd="fdToggle" active={fdOn} />
|
||||||
<B label="HDG" cmd="hdg" active={on(AP_BITS.hdg)} />
|
<B label="HDG" cmd="hdg" active={lit('hdgStatus')} />
|
||||||
<B label="ALT" cmd="altHold" active={on(AP_BITS.altHold)} />
|
<B label="ALT" cmd="altHold" active={lit('altStatus')} />
|
||||||
<B label="NAV" cmd="nav" active={on(AP_BITS.nav)} />
|
<B label="NAV" cmd="nav" active={lit('navStatus') || lit('gpssStatus')} />
|
||||||
<B label="VNV" cmd="vnav" active={on(AP_BITS.vnav)} />
|
<B label="VNV" cmd="vnav" active={lit('vnavStatus')} />
|
||||||
<B label="APR" cmd="apr" active={on(AP_BITS.apr)} />
|
<B label="APR" cmd="apr" active={lit('aprStatus')} />
|
||||||
<B label="BC" cmd="backCourse" active={on(AP_BITS.bc)} />
|
<B label="BC" cmd="backCourse" active={lit('bcStatus')} />
|
||||||
<B label="VS" cmd="vs" active={on(AP_BITS.vs)} />
|
<B label="VS" cmd="vs" active={lit('vsStatus')} />
|
||||||
<B label="FLC" cmd="flc" active={on(AP_BITS.flc)} />
|
<B label="FLC" cmd="flc" active={lit('flcStatus')} />
|
||||||
<B label="NOSE UP" cmd="noseUp" />
|
<B label="NOSE UP" cmd="noseUp" />
|
||||||
<B label="NOSE DN" cmd="noseDown" />
|
<B label="NOSE DN" cmd="noseDown" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const SVT_BOX = { x: 0, y: 74, w: W, h: H - 74 };
|
|||||||
// The INSET moving map sits in the bottom-left corner (toggled by INSET softkey).
|
// The INSET moving map sits in the bottom-left corner (toggled by INSET softkey).
|
||||||
const INSET_BOX = { x: 6, y: 556, w: 300, h: 172 };
|
const INSET_BOX = { x: 6, y: 556, w: 300, h: 172 };
|
||||||
|
|
||||||
export default function PFD({ xp, values: V, command, connected = true, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, dme = false, onCloseDme, alerts = false, onCloseAlerts, baroHpa = false, obs = false, minimums, onMinimums, flightPlan, fp, vnav: vnavCfg }) {
|
export default function PFD({ xp, values: V, command, connected = true, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, dme = false, onCloseDme, alerts = false, onCloseAlerts, baroHpa = false, obs = false, minimums, onMinimums, flightPlan, fp, vnav: vnavCfg, svtOpts }) {
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const [box, setBox] = useState(null);
|
const [box, setBox] = useState(null);
|
||||||
@@ -164,7 +164,7 @@ export default function PFD({ xp, values: V, command, connected = true, svt = tr
|
|||||||
<div className="pfd-wrap" ref={wrapRef}>
|
<div className="pfd-wrap" ref={wrapRef}>
|
||||||
{svt && box && (
|
{svt && box && (
|
||||||
<div className="svt-pos" style={box}>
|
<div className="svt-pos" style={box}>
|
||||||
<Suspense fallback={<div className="svt-fallback" />}><SVT values={V} /></Suspense>
|
<Suspense fallback={<div className="svt-fallback" />}><SVT values={V} flightPlan={flightPlan} opts={svtOpts} /></Suspense>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{inset && insetBox && (
|
{inset && insetBox && (
|
||||||
|
|||||||
@@ -83,11 +83,27 @@ function cameraPitchForAircraft(aircraftPitchDeg) {
|
|||||||
return Math.max(60, Math.min(85, pitch));
|
return Math.max(60, Math.min(85, pitch));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SVT({ values }) {
|
// Flight-plan route as a GeoJSON LineString (drawn on the terrain for PATHWAY).
|
||||||
|
function routeGeo(plan) {
|
||||||
|
const wps = plan?.waypoints || [];
|
||||||
|
return { type: 'Feature', geometry: { type: 'LineString', coordinates: wps.map((w) => [w.lon, w.lat]) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the PATHWAY route line and APTSIGNS runway labels (default on).
|
||||||
|
function setSvtVisibility(map, opts) {
|
||||||
|
if (!map) return;
|
||||||
|
const set = (id, vis) => { try { if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', vis ? 'visible' : 'none'); } catch { /* not ready */ } };
|
||||||
|
set('fpl-line', !opts || opts.pathway !== false);
|
||||||
|
['rwy-fill', 'rwy-line', 'rwy-num'].forEach((id) => set(id, !opts || opts.aptSigns !== false));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SVT({ values, flightPlan, opts }) {
|
||||||
const elRef = useRef(null);
|
const elRef = useRef(null);
|
||||||
const mapRef = useRef(null);
|
const mapRef = useRef(null);
|
||||||
const dataRef = useRef(values);
|
const dataRef = useRef(values);
|
||||||
dataRef.current = values;
|
dataRef.current = values;
|
||||||
|
const planRef = useRef(flightPlan); planRef.current = flightPlan;
|
||||||
|
const optsRef = useRef(opts); optsRef.current = opts;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let map;
|
let map;
|
||||||
@@ -131,6 +147,12 @@ export default function SVT({ values }) {
|
|||||||
},
|
},
|
||||||
paint: { 'text-color': '#fff', 'text-halo-color': '#000', 'text-halo-width': 1.4 },
|
paint: { 'text-color': '#fff', 'text-halo-color': '#000', 'text-halo-width': 1.4 },
|
||||||
});
|
});
|
||||||
|
// PATHWAY: the active flight-plan route, draped magenta on the terrain.
|
||||||
|
map.addSource('fplroute', { type: 'geojson', data: routeGeo(planRef.current) });
|
||||||
|
map.addLayer({ id: 'fpl-line', type: 'line', source: 'fplroute',
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#ff20ff', 'line-width': 3, 'line-opacity': 0.9 } });
|
||||||
|
setSvtVisibility(map, optsRef.current); // honour PATHWAY / APTSIGNS from the start
|
||||||
let last = null;
|
let last = null;
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const v = dataRef.current, lat = num(v.lat), lon = num(v.lon);
|
const v = dataRef.current, lat = num(v.lat), lon = num(v.lon);
|
||||||
@@ -186,6 +208,14 @@ export default function SVT({ values }) {
|
|||||||
return () => { cancelAnimationFrame(raf); clearInterval(rwyTimer); map.remove(); mapRef.current = null; };
|
return () => { cancelAnimationFrame(raf); clearInterval(rwyTimer); map.remove(); mapRef.current = null; };
|
||||||
}, []); // eslint-disable-line
|
}, []); // eslint-disable-line
|
||||||
|
|
||||||
|
// Keep the PATHWAY route in sync with the flight plan, and apply PATHWAY /
|
||||||
|
// APTSIGNS visibility when the softkeys toggle them.
|
||||||
|
useEffect(() => {
|
||||||
|
const m = mapRef.current;
|
||||||
|
if (m && m.getSource && m.getSource('fplroute')) { try { m.getSource('fplroute').setData(routeGeo(flightPlan)); } catch { /* not ready */ } }
|
||||||
|
}, [flightPlan]);
|
||||||
|
useEffect(() => { setSvtVisibility(mapRef.current, opts); }, [opts]); // eslint-disable-line
|
||||||
|
|
||||||
// Bank: rotate the whole terrain canvas opposite to aircraft roll; scale up so
|
// Bank: rotate the whole terrain canvas opposite to aircraft roll; scale up so
|
||||||
// the corners stay covered while rotated.
|
// the corners stay covered while rotated.
|
||||||
const roll = num(values.roll);
|
const roll = num(values.roll);
|
||||||
|
|||||||
Reference in New Issue
Block a user