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:
+60
-10
@@ -11,9 +11,10 @@ import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { CONFIG, DATAREFS, WRITABLE_DATAREFS, COMMANDS } from './config.js';
|
||||
import { loadNavData, search as navSearch, navStatus, nearest as navNearest, bbox as navBbox, runwaysNear as navRunways } from './navdata.js';
|
||||
import { loadNavData, search as navSearch, navStatus, nearest as navNearest, bbox as navBbox, runwaysNear as navRunways, airwaysBbox as navAirways } from './navdata.js';
|
||||
import { parseProcedures, procedureLegs as procLegs } from './procedures.js';
|
||||
import * as fp from './flightplan.js';
|
||||
import { pushToSim, startFmsSync, startTerrainSync } from './fmssync.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// WEB_DIST can be overridden (e.g. the desktop app points it at the cockpit
|
||||
@@ -46,7 +47,9 @@ function broadcast(obj) {
|
||||
}
|
||||
|
||||
function broadcastPlan() {
|
||||
broadcast({ type: 'flightplan', data: fp.getPlan() });
|
||||
const plan = fp.getPlan();
|
||||
broadcast({ type: 'flightplan', data: plan });
|
||||
pushToSim(plan); // hand the plan to the FlyWithLua FMS bridge (App → Sim)
|
||||
}
|
||||
|
||||
async function fetchAllByName(resource, names) {
|
||||
@@ -162,6 +165,11 @@ function handleClientMessage(msg) {
|
||||
}
|
||||
if (msg.type === 'fp_remove') { fp.removeWaypoint(msg.index); return broadcastPlan(); }
|
||||
if (msg.type === 'fp_active') { fp.setActiveLeg(msg.index); return broadcastPlan(); }
|
||||
if (msg.type === 'fp_load') {
|
||||
const r = fp.loadFms(msg.name);
|
||||
if (r.ok) return broadcastPlan();
|
||||
return broadcast({ type: 'fp_export_result', ...r });
|
||||
}
|
||||
if (msg.type === 'fp_clear') { fp.setPlan({ waypoints: [] }); return broadcastPlan(); }
|
||||
if (msg.type === 'fp_export') {
|
||||
const r = fp.exportFms(msg.name || 'WEBFPL');
|
||||
@@ -216,6 +224,10 @@ app.get('/api/nav/bbox', (req, res) =>
|
||||
res.json(navBbox(+req.query.s, +req.query.w, +req.query.n, +req.query.e,
|
||||
(req.query.types || 'apt,vor,ndb').split(','), +req.query.limit || 800))
|
||||
);
|
||||
// Airways (Victor/Jet routes) inside a map window — for the MFD AIRWAYS overlay.
|
||||
app.get('/api/nav/airways', (req, res) =>
|
||||
res.json(navAirways(+req.query.s, +req.query.w, +req.query.n, +req.query.e, +req.query.limit || 500))
|
||||
);
|
||||
// Runways near a point — drawn in the PFD synthetic-vision view.
|
||||
app.get('/api/nav/runways', (req, res) =>
|
||||
res.json(navRunways(+req.query.lat, +req.query.lon, +req.query.radius || 12))
|
||||
@@ -227,6 +239,8 @@ app.get('/api/nav/procs', (req, res) => {
|
||||
if (!p) return res.status(404).json({ error: 'no procedures for ' + req.query.icao });
|
||||
res.json({ icao: p.icao, runways: p.runways, sids: p.sids, stars: p.stars, approaches: p.approaches });
|
||||
});
|
||||
// Saved flight plans (Output/FMS plans) — list for the FPL "load" picker.
|
||||
app.get('/api/fms/list', (_req, res) => res.json(fp.listPlans()));
|
||||
app.get('/api/nav/proc', (req, res) =>
|
||||
res.json(procLegs(String(req.query.icao || ''), req.query.type, req.query.name, req.query.trans))
|
||||
);
|
||||
@@ -261,17 +275,23 @@ function startDemo() {
|
||||
heading: 87, slip: 0.3, gForce: 1.04, oat: 9,
|
||||
apState: (1 << 0) | (1 << 1) | (1 << 14), // FD + HDG + ALT
|
||||
apEngaged: 1, apHdgBug: 90, apAltBug: 6000, apVsBug: 500, apSpdBug: 120,
|
||||
// AFCS annunciation: AP on, HDG active + GPS armed (lateral), ALT active (vertical)
|
||||
apMode: 2, hdgStatus: 2, gpssStatus: 1, altStatus: 2,
|
||||
lat: 47.45, lon: -122.31, track: 90, groundspeed: 64, gpsDistNm: 18.4, gpsBearing: 92,
|
||||
// radios (XP freq units: nav/com in 10 kHz, e.g. 11030 = 110.30)
|
||||
nav1: 11030, nav1Sb: 11150, nav2: 11380, nav2Sb: 10890,
|
||||
com1: 12190, com1Sb: 13000, com2: 12475, com2Sb: 12180,
|
||||
// HSI / data fields
|
||||
obsCrs: 175, hsiDef: -0.6, hsiToFrom: 1, navBearing: 168, gsDef: 0.7,
|
||||
nav1Brg: 210, nav1Dme: 12.4, nav2Brg: 320, nav2Dme: 0, // BRG1 (NAV1 VOR/DME) demo
|
||||
|
||||
baro: 29.92, tas: 131, windSpd: 14, windDir: 240,
|
||||
xpdrCode: 1200, xpdrMode: 2, fdPitch: 5, fdRoll: -10,
|
||||
cdiSrc: Number(process.env.DEMO_CDI ?? 2), // 0 VLOC1, 1 VLOC2, 2 GPS
|
||||
...(process.env.DEMO_RANGE ? { uiMapRange: Number(process.env.DEMO_RANGE) } : {}),
|
||||
// engine strip (arrays, like the sim)
|
||||
engRpm: [2410], fuelFlow: [0.0072], oilTemp: [88], oilPress: [52], egt: [720],
|
||||
fuelQty: [60, 58], volts: [28.0], amps: [12],
|
||||
fuelQty: [60, 58], volts: [process.env.DEMO_ALERT ? 23.4 : 28.0], amps: [12],
|
||||
});
|
||||
// a sample plan so the map/FMS show something in demo mode
|
||||
fp.setPlan({ name: 'DEMO', waypoints: [
|
||||
@@ -279,27 +299,57 @@ function startDemo() {
|
||||
{ id: 'SEA', lat: 47.435, lon: -122.310, type: 'VOR', alt: 4000 },
|
||||
{ id: 'KPDX', lat: 45.589, lon: -122.597, type: 'APT', alt: 1200 },
|
||||
]});
|
||||
pushToSim(fp.getPlan());
|
||||
let t = 0;
|
||||
const lat0 = 47.45, lon0 = -122.31, R = 0.05, w = 0.02; // gentle orbit around KSEA
|
||||
const cosL = Math.cos(lat0 * Math.PI / 180);
|
||||
let pLat = lat0, pLon = lon0;
|
||||
setInterval(() => {
|
||||
t += 0.1;
|
||||
state.values.roll = -12 + Math.sin(t) * 4;
|
||||
state.values.pitch = 4.5 + Math.cos(t * 0.7) * 1.5;
|
||||
state.values.heading = (87 + Math.sin(t * 0.3) * 3 + 360) % 360;
|
||||
state.values.track = state.values.heading;
|
||||
state.values.altitude = 5500 + Math.sin(t * 0.5) * 40;
|
||||
state.values.airspeed = 124 + Math.sin(t * 0.4) * 3;
|
||||
// creep south-east so the aircraft visibly moves on the map
|
||||
state.values.lat -= 0.0006;
|
||||
state.values.lon -= 0.0009;
|
||||
const newAlt = 5500 + Math.sin(t * 0.5) * 120;
|
||||
state.values.vspeed = (newAlt - state.values.altitude) / (0.1 / 60); // fpm from Δalt/Δt
|
||||
state.values.altitude = newAlt;
|
||||
state.values.airspeed = 124 + Math.sin(t * 0.4) * 8;
|
||||
// orbit so the aircraft visibly moves but stays near the demo flight plan
|
||||
const lat = lat0 + Math.cos(t * w) * R;
|
||||
const lon = lon0 + Math.sin(t * w) * R / cosL;
|
||||
const trk = (Math.atan2((lon - pLon) * cosL, lat - pLat) * 180 / Math.PI + 360) % 360;
|
||||
state.values.lat = lat; state.values.lon = lon;
|
||||
state.values.track = trk; state.values.heading = trk;
|
||||
pLat = lat; pLon = lon;
|
||||
broadcast({ type: 'status', xpConnected: true });
|
||||
broadcast({ type: 'values', data: state.values });
|
||||
}, 100);
|
||||
// synthetic terrain grid (a Cascades-style ridge rising eastward) so the MFD
|
||||
// terrain-awareness colouring (yellow/red vs aircraft altitude) is visible
|
||||
const emitTerrain = () => {
|
||||
const lat = state.values.lat, lon = state.values.lon, alt = state.values.altitude;
|
||||
const rows = 28, cols = 28, n = lat + 0.35, s = lat - 0.35, w = lon - 0.5, e = lon + 0.5;
|
||||
const elev = [];
|
||||
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
||||
const fx = c / (cols - 1), fy = r / (rows - 1); // fx: 0 west → 1 east
|
||||
let h = fx * 9000 - 1200 + Math.sin(fy * 6 + fx * 4) * 800 + Math.cos(fx * 9) * 400;
|
||||
elev.push(Math.max(0, Math.round(h)));
|
||||
}
|
||||
broadcast({ type: 'terrain', data: { lat, lon, alt, n, s, w, e, rows, cols, elev } });
|
||||
};
|
||||
emitTerrain();
|
||||
setInterval(emitTerrain, 1500);
|
||||
}
|
||||
|
||||
server.listen(CONFIG.bridgePort, CONFIG.bridgeHost, () => {
|
||||
log(`Bridge UI: http://${CONFIG.bridgeHost}:${CONFIG.bridgePort}`);
|
||||
log(`On tablets: http://<this-PC-LAN-IP>:${CONFIG.bridgePort}`);
|
||||
loadNavData(); // async; FMS resolves idents once ready
|
||||
// FMS two-way sync (Sim → App): adopt plans built/edited in the real G1000
|
||||
startFmsSync({
|
||||
getPlan: () => fp.getPlan(),
|
||||
onSimPlan: (waypoints) => { fp.setPlan({ name: 'ACTIVE', waypoints, activeLeg: 1 }); broadcastPlan(); },
|
||||
});
|
||||
// Terrain awareness grid (from the FlyWithLua terrain probe) → MFD colouring
|
||||
startTerrainSync((t) => broadcast({ type: 'terrain', data: t }));
|
||||
if (process.env.DEMO) startDemo();
|
||||
else connectXPlane();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user