Manual audit fixes A/E/F: fuel totalizer, Victor/Jet airways, TIME TO BOD

A — Fuel totalizer (SYSTEM key, renamed from ENGINE): DEC/INC/RST FUEL had no
handler. Now adjust the fuel_totalizer_sum_kg dataref (±1 gal, RST→max fuel) and
the EIS shows the calculated remaining/used. cdiSrc-style writable + demo echo.

E — AIRWAYS: was a single on/off. earth_awy.dat field 8 (airway layer) is now
parsed (1=Victor/low, 2=Jet/high; name-prefix fallback), the bbox returns it, and
the AIRWAYS softkey cycles off→all→Victor→Jet with an AIRWY-LO/-HI label.

F — CURRENT VNV PROFILE now shows TIME TO BOD (bottom of descent) once past the
top of descent, instead of only TIME TO TOD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 00:59:00 +02:00
parent 502fa249a0
commit 5db22c85bc
7 changed files with 44 additions and 9 deletions
+1
View File
@@ -325,6 +325,7 @@ function startDemo() {
// engine strip (arrays, like the sim)
engRpm: [2410], fuelFlow: [0.0072], oilTemp: [88], oilPress: [52], egt: [720],
fuelQty: [60, 58], volts: [process.env.DEMO_ALERT ? 23.4 : 28.0, 27.8], amps: [-1.5], genAmps: [20.5], engHrs: 5040,
fuelTot: 118 * 2.72, fuelMax: 144 * 2.72, // fuel totalizer: 118 of 144 gal (kg) — SYSTEM keys adjust
});
// a sample plan so the map/FMS show something in demo mode
fp.setPlan({ name: 'DEMO', waypoints: [
+3
View File
@@ -96,6 +96,8 @@ export const DATAREFS = {
oilPress: 'sim/cockpit2/engine/indicators/oil_pressure_psi',
egt: 'sim/cockpit2/engine/indicators/EGT_deg_C',
fuelQty: 'sim/cockpit2/fuel/fuel_quantity',
fuelTot: 'sim/cockpit2/fuel/fuel_totalizer_sum_kg', // pilot-set fuel totalizer (remaining, kg)
fuelMax: 'sim/aircraft/weight/acf_m_fuel_tot', // max fuel capacity (kg) — for RST FUEL
volts: 'sim/cockpit2/electrical/bus_volts', // array: [0]=main bus, [1]=essential
amps: 'sim/cockpit2/electrical/battery_amps', // battery (S) amps
genAmps: 'sim/cockpit2/electrical/generator_amps', // alternator (M) amps
@@ -135,6 +137,7 @@ export const WRITABLE_DATAREFS = {
xpdrMode: 'sim/cockpit2/radios/actuators/transponder_mode', // 0 off,1 stby,2 on,3 alt
xpdrCode: 'sim/cockpit2/radios/actuators/transponder_code', // 4-digit squawk
cdiSrc: 'sim/cockpit2/radios/actuators/HSI_source_select_pilot', // 0 NAV1 · 1 NAV2 · 2 GPS (CDI softkey cycles it)
fuelTot: 'sim/cockpit2/fuel/fuel_totalizer_sum_kg', // SYSTEM → DEC/INC/RST FUEL adjusts the totalizer
};
// Commands the frontend may TRIGGER (autopilot mode buttons etc.).
+5 -1
View File
@@ -167,9 +167,13 @@ async function parseAirways(file) {
const b = index.get((p[3] || '').toUpperCase());
if (!a || !b) continue; // endpoint not in our database
const name = p[p.length - 1];
// Field 8 (index 7) = airway layer: 1 = low/Victor, 2 = high/Jet (used by the
// MFD AIRWAYS key to show all / Victor-only / Jet-only). Fall back to the name
// prefix (V… = low, J… = high) if the field is missing.
const lyr = +p[7] === 1 ? 1 : +p[7] === 2 ? 2 : /^J/i.test(name) ? 2 : /^V/i.test(name) ? 1 : 0;
const k = `${Math.floor((a.lat + b.lat) / 2)},${Math.floor((a.lon + b.lon) / 2)}`;
let arr = awyCells.get(k); if (!arr) { arr = []; awyCells.set(k, arr); }
arr.push({ la1: a.lat, lo1: a.lon, la2: b.lat, lo2: b.lon, name });
arr.push({ la1: a.lat, lo1: a.lon, la2: b.lat, lo2: b.lon, name, lyr });
state.awy++;
}
}
+11 -5
View File
@@ -28,10 +28,11 @@ const PFD_MENU = {
// page; TOPO/TERRAIN/OSM switch the base map; BACK returns. (OSM is our tuned
// extra layer in an otherwise-empty slot.)
const MFD_MENU = {
root: ['ENGINE', 'MAP', '', '', '', '', '', '', '', 'DCLTR', '', ''],
root: ['SYSTEM', 'MAP', '', '', '', '', '', '', '', 'DCLTR', '', ''],
mapopt: ['TRAFFIC', 'PROFILE', 'TOPO', 'TERRAIN', 'AIRWAYS', 'AIRSPACE', 'NEXRAD', 'OSM', '', '', 'BACK', ''],
engine: ['DEC FUEL', 'INC FUEL', 'RST FUEL', '', '', '', '', '', '', '', 'BACK', ''],
system: ['DEC FUEL', 'INC FUEL', 'RST FUEL', '', '', '', '', '', '', '', 'BACK', ''],
};
const KG_PER_GAL = 2.72; // fuel totalizer steps in US gallons (matches the EIS readout)
// autopilot_state bitfield (best-effort; tweak per aircraft)
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 };
@@ -67,13 +68,16 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
const cycleDcltr = (setter) => setter && setter((m) => ({ ...m, dcltr: (((m.dcltr || 0) + 1) % 4) }));
if (variant === 'mfd') {
if (label === 'MAP') setPage('mapopt');
else if (label === 'ENGINE') setPage('engine');
else if (label === 'SYSTEM') setPage('system');
else if (label === 'BACK') setPage('root');
else if (label === 'DEC FUEL' && xp) xp.setDataref('fuelTot', Math.max(0, num(xp.values?.fuelTot) - KG_PER_GAL));
else if (label === 'INC FUEL' && xp) xp.setDataref('fuelTot', num(xp.values?.fuelTot) + KG_PER_GAL);
else if (label === 'RST FUEL' && xp) xp.setDataref('fuelTot', num(xp.values?.fuelMax) || num(xp.values?.fuelTot));
else if (label === 'TOPO') setBase('topo'); // relief on/off
else if (label === 'TERRAIN') onMapMode && onMapMode((m) => ({ ...m, terrain: !m.terrain })); // awareness overlay (independent)
else if (label === 'OSM') setBase('osm');
else if (label === 'DCLTR') cycleDcltr(onMapMode);
else if (label === 'AIRWAYS') onMapMode && onMapMode((m) => ({ ...m, airways: !m.airways }));
else if (label === 'AIRWAYS') onMapMode && onMapMode((m) => ({ ...m, airways: (((m.airways | 0) + 1) % 4) })); // off→all→Victor→Jet
else if (label === 'AIRSPACE') onMapMode && onMapMode((m) => ({ ...m, airspace: !m.airspace }));
} else {
if (label === 'PFD') setPage('pfd');
@@ -149,7 +153,9 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
<div className="sk-labels">
{keys.map((s, i) => (
<span key={i} className={`skl ${s ? '' : 'empty'} ${s === 'CAUTION' ? 'caution' : ''} ${isOn(s) ? 'on' : ''}`}>{
(s === 'DCLTR' && (mapMode?.dcltr || insetMode?.dcltr)) ? `DCLTR-${mapMode?.dcltr || insetMode?.dcltr}` : s
(s === 'DCLTR' && (mapMode?.dcltr || insetMode?.dcltr)) ? `DCLTR-${mapMode?.dcltr || insetMode?.dcltr}`
: (s === 'AIRWAYS' && (mapMode?.airways | 0) >= 2) ? (mapMode.airways === 2 ? 'AIRWY-LO' : 'AIRWY-HI')
: s
}</span>
))}
</div>
+9 -2
View File
@@ -80,7 +80,11 @@ export default function FplPage({ xp, full = false, onClose }) {
const vsReq = c > 0 ? (t - alt) / (c / gs * 60) : 0;
const vDev = alt - (t + c * 6076.12 * tan);
const todNm = c - (alt - t) / (6076.12 * tan);
vnav = { wptId: wps[i].id, tgtAlt: t, vsTgt, vsReq, vDev, fpa: 3.0, todSec: todNm > 0 ? (todNm / gs) * 3600 : 0 };
// Before TOD: time until the descent path is intercepted. After TOD (already
// descending): time to Bottom of Descent = reaching the target waypoint.
const todSec = todNm > 0 ? (todNm / gs) * 3600 : 0;
const bodSec = todNm > 0 ? 0 : (c / gs) * 3600;
vnav = { wptId: wps[i].id, tgtAlt: t, vsTgt, vsReq, vDev, fpa: 3.0, todSec, bodSec };
break;
}
}
@@ -124,7 +128,10 @@ export default function FplPage({ xp, full = false, onClose }) {
<div className="fpl-vnav-grid">
<b>ACTIVE VNV WPT</b><span className="vwpt">{vnav.tgtAlt}<u>FT</u> at {vnav.wptId}</span>
<b>VS TGT</b><span>{Math.round(vnav.vsTgt)}<u>FPM</u></span><b>FPA</b><span>{vnav.fpa.toFixed(1)}°</span>
<b>VS REQ</b><span>{Math.round(vnav.vsReq)}<u>FPM</u></span><b>TIME TO TOD</b><span>{fmtSec(vnav.todSec)}</span>
<b>VS REQ</b><span>{Math.round(vnav.vsReq)}<u>FPM</u></span>
{vnav.todSec > 0
? <><b>TIME TO TOD</b><span>{fmtSec(vnav.todSec)}</span></>
: <><b>TIME TO BOD</b><span>{fmtSec(vnav.bodSec)}</span></>}
<b>V DEV</b><span>{vnav.vDev >= 0 ? '+' : ''}{Math.round(vnav.vDev)}<u>FT</u></span>
</div>
) : <div className="fpl-vnav-none"> no active VNAV profile </div>}
+11
View File
@@ -155,6 +155,17 @@ function EisStrip({ V }) {
<Bar y={284} label="VAC" min={0} max={10} value={5}
zones={[{ from: 4.5, to: 5.5, c: '#0c0' }]} />
<FuelBar y={330} left={fuelL} right={fuelR} />
{/* fuel totalizer (SYSTEM → DEC/INC/RST FUEL): pilot-set remaining + used */}
{(() => {
const rem = num(V.fuelTot) / KG_PER_GAL;
const used = Math.max(0, (num(V.fuelMax) - num(V.fuelTot)) / KG_PER_GAL);
return (<>
<text x="8" y="392" fill="#39d3c0" fontSize="11">CALC</text>
<text x="118" y="392" fill="#fff" fontSize="14" textAnchor="end">{rem.toFixed(1)}</text>
<text x="124" y="392" fill="#7f8c97" fontSize="10">GAL</text>
<text x="182" y="392" fill="#9aa" fontSize="10" textAnchor="end">USED {used.toFixed(0)}</text>
</>);
})()}
<text x="8" y="412" fill="#39d3c0" fontSize="12">ENG</text>
<text x="182" y="412" fill="#fff" fontSize="14" textAnchor="end">{engHrs.toFixed(1)} HRS</text>
<text x="95" y="438" fill="#39d3c0" fontSize="12" textAnchor="middle"> ELECTRICAL </text>
+4 -1
View File
@@ -100,7 +100,7 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
const track = num(values.track);
const gs = num(values.groundspeed) * 1.94384; // m/s -> kt
const base = mapMode?.base || 'topo';
const airways = !!mapMode?.airways;
const airways = (mapMode?.airways | 0); // 0 off · 1 all · 2 Victor (low) · 3 Jet (high)
const airspace = !!mapMode?.airspace;
aspOnRef.current = airspace;
const dcltrRef = useRef(dcltr);
@@ -154,8 +154,11 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
const segs = await res.json();
layer.clearLayers();
const labels = map.getZoom() >= 8;
const mode = awyOnRef.current; // 1 all · 2 Victor (low) · 3 Jet (high)
const seen = new Set();
for (const sg of segs) {
if (mode === 2 && sg.lyr !== 1) continue; // Victor-only: low-altitude airways
if (mode === 3 && sg.lyr !== 2) continue; // Jet-only: high-altitude airways
L.polyline([[sg.la1, sg.lo1], [sg.la2, sg.lo2]], { color: '#5db4e6', weight: 1.2, opacity: 0.7, interactive: false }).addTo(layer);
if (labels && sg.name && !seen.has(sg.name)) {
seen.add(sg.name);