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