// 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 /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); }