G1000: two-way sim sync, more PFD/MFD fidelity, authentic dialogs

Sync (FlyWithLua companions in plugins/ + server/fmssync.js):
- FMS flight-plan two-way sync (App <-> in-sim FMS) via fms-sync.lua
- G1000 UI-state publish (page/range/inset) via ui-sync.lua + CDI source,
  baro, map-range follow
- Terrain awareness: elevation grid probe (terrain-probe.lua) -> red/yellow
  MFD overlay vs aircraft altitude

PFD:
- AFCS mode annunciation bar from autopilot _status datarefs
- CDI source GPS/VLOC colouring, BRG1/BRG2 pointers + DME windows, marker beacons
- magenta speed/altitude trend vectors, selected-altitude alerting
- time-based (frame-rate-independent) smoothing for attitude/heading/tapes

MFD:
- nav data bar (DTK/ETE/active leg), airways overlay from earth_awy.dat,
  compass rose anchored to the ownship

Dialogs (NEAREST/FLIGHTPLAN/DIRECT-TO/PROCEDURES):
- flat, square, embedded G1000 look (no shadow/rounded/transparency)
- compact lower-right placement, no close X (softkey toggles), single window
- NEAREST 2-line entries (ILS/VFR, COM freq, runway length), PROC action menu

Service worker: network-first HTML so reloads pick up new builds (cache v2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 02:17:06 +02:00
parent 354ea5d44b
commit 38b048ad41
23 changed files with 1707 additions and 213 deletions
+85
View File
@@ -0,0 +1,85 @@
// Two-way flight-plan sync with X-Plane's in-sim FMS, bridged by a FlyWithLua
// companion script (see plugins/fms-sync.lua). X-Plane's Web API can't inject a
// flight plan into the FMS, so the Lua script (which has the FMS SDK) does it.
//
// Channel = two text files in <X-Plane>/Output/fms-sync/ (bridge + Lua run on
// the same PC). We write to_sim.txt (our plan); Lua applies it to the FMS and
// writes from_sim.txt (the sim's plan); we adopt sim-side changes. A position
// signature (3-decimal lat/lon) de-dupes so the two sides never loop.
import fs from 'node:fs';
import path from 'node:path';
import { xplaneRoot } from './navdata.js';
function dir() {
const r = xplaneRoot();
return r ? path.join(r, 'Output', 'fms-sync') : path.join(process.cwd(), 'fms-sync');
}
const toSimFile = () => path.join(dir(), 'to_sim.txt');
const fromSimFile = () => path.join(dir(), 'from_sim.txt');
// loop-guard signature: rounded lat/lon list (idents/alt ignored, and coords
// from our navdata == X-Plane's, so it stays stable across the round-trip)
const sig = (wps) => (wps || []).map((w) => `${(+w.lat).toFixed(3)},${(+w.lon).toFixed(3)}`).join(';');
let lastSig = null;
function serialize(wps) {
const body = (wps || [])
.map((w) => `${(+w.lat).toFixed(6)} ${(+w.lon).toFixed(6)} ${Math.round(w.alt || 0)} ${w.id || 'WPT'} ${w.type || 'WPT'}`)
.join('\n');
return `# ${sig(wps)}\n${body}\n`; // first line = sig comment, then waypoints
}
function parse(txt) {
const wps = [];
for (const ln of (txt || '').split(/\r?\n/)) {
const t = ln.trim();
if (!t || t.startsWith('#')) continue; // skip sig/comment line
const p = t.split(/\s+/);
const lat = +p[0], lon = +p[1];
if (p.length >= 2 && isFinite(lat) && isFinite(lon) && Math.abs(lat) <= 90 && Math.abs(lon) <= 180) {
const alt = +p[2] || 0;
wps.push({ id: p[3] || 'WPT', lat, lon, type: p[4] || 'WPT', alt: alt > 0 ? alt : null });
}
}
return wps;
}
// our plan changed → hand it to the Lua script
export function pushToSim(plan) {
try {
fs.mkdirSync(dir(), { recursive: true });
fs.writeFileSync(toSimFile(), serialize(plan?.waypoints || []));
lastSig = sig(plan?.waypoints || []);
} catch { /* sim not local / no write access */ }
}
// Terrain elevation grid published by the FlyWithLua terrain probe
// (terrain.json in the sync dir). Polled and broadcast so the MFD can colour
// terrain awareness (red/yellow). Only re-broadcasts when it actually changes.
const terrainFile = () => path.join(dir(), 'terrain.json');
export function startTerrainSync(onTerrain, intervalMs = 1500) {
let lastMtime = 0;
setInterval(() => {
let st;
try { st = fs.statSync(terrainFile()); } catch { return; }
if (st.mtimeMs === lastMtime) return;
lastMtime = st.mtimeMs;
try {
const t = JSON.parse(fs.readFileSync(terrainFile(), 'utf8'));
if (t && Array.isArray(t.elev) && t.elev.length) onTerrain(t);
} catch { /* mid-write / malformed */ }
}, intervalMs);
}
// poll the Lua-written sim plan; adopt genuine sim-side changes
export function startFmsSync({ getPlan, onSimPlan }) {
pushToSim(getPlan());
setInterval(() => {
let txt;
try { txt = fs.readFileSync(fromSimFile(), 'utf8'); } catch { return; }
const wps = parse(txt);
const s = sig(wps);
if (wps.length && s && s !== lastSig) { lastSig = s; onSimPlan(wps); }
}, 1200);
}