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,31 @@
|
||||
# FlyWithLua companion — FMS two-way sync
|
||||
|
||||
X-Plane's Web API can't write a flight plan into the FMS. `fms-sync.lua` runs
|
||||
inside X-Plane (via FlyWithLua, which has the FMS SDK) and syncs the shared
|
||||
cockpit plan ↔ the in-sim FMS through two files in `Output/fms-sync/`.
|
||||
|
||||
## Install (sim PC only)
|
||||
|
||||
1. Install **FlyWithLua NG+** (free): copy its plugin folder into
|
||||
`<X-Plane>/Resources/plugins/FlyWithLua/`.
|
||||
2. Copy the scripts into `<X-Plane>/Resources/plugins/FlyWithLua/Scripts/`:
|
||||
- **`fms-sync.lua`** — flight-plan two-way sync
|
||||
- **`ui-sync.lua`** — G1000 UI state (page / range / inset)
|
||||
- **`terrain-probe.lua`** — terrain-awareness elevation grid for the MFD
|
||||
3. Restart X-Plane (or *FlyWithLua → Reload all Lua script files*).
|
||||
The log shows `[glass-cockpit] FMS sync active`.
|
||||
|
||||
The bridge (desktop app / `node server/bridge.js`) must run on the **same PC**
|
||||
as X-Plane, so both see `<X-Plane>/Output/fms-sync/`.
|
||||
|
||||
## What you get
|
||||
|
||||
- **App → Sim:** load/build a plan in the web cockpit → it appears in the 3-D
|
||||
G1000 and the autopilot can fly it.
|
||||
- **Sim → App:** build/edit the plan in the real FMS → it shows on every tablet.
|
||||
- **Terrain:** the MFD TERRAIN map colours real scenery elevation red/yellow
|
||||
relative to your altitude (probed live by `terrain-probe.lua`).
|
||||
|
||||
A 3-decimal lat/lon signature de-dupes the round-trip, so the two sides never
|
||||
loop. Waypoints are pushed as lat/lon legs (exact route; in-sim idents are
|
||||
generic — route accuracy over cosmetics).
|
||||
@@ -0,0 +1,113 @@
|
||||
-- ============================================================================
|
||||
-- X-Plane Glass Cockpit — FMS two-way sync (FlyWithLua companion)
|
||||
-- ============================================================================
|
||||
-- The web cockpit's bridge can't write the FMS via X-Plane's Web API. This
|
||||
-- script runs INSIDE X-Plane (FlyWithLua) and has the FMS SDK, so it bridges
|
||||
-- the shared plan <-> the in-sim FMS through two text files:
|
||||
--
|
||||
-- <X-Plane>/Output/fms-sync/to_sim.txt written by the bridge (our plan)
|
||||
-- <X-Plane>/Output/fms-sync/from_sim.txt written here (the sim's plan)
|
||||
--
|
||||
-- A 3-decimal lat/lon signature de-dupes both sides so they never loop.
|
||||
--
|
||||
-- INSTALL: copy this file to <X-Plane>/Resources/plugins/FlyWithLua/Scripts/
|
||||
-- (install FlyWithLua NG+ first), then restart X-Plane or run
|
||||
-- "FlyWithLua > Reload all Lua script files".
|
||||
-- ============================================================================
|
||||
|
||||
local SYNC = SYSTEM_DIRECTORY .. "Output/fms-sync/"
|
||||
local TO_SIM = SYNC .. "to_sim.txt"
|
||||
local FROM_SIM = SYNC .. "from_sim.txt"
|
||||
local last_sig = nil
|
||||
|
||||
-- make sure the folder exists (bridge also creates it)
|
||||
os.execute('mkdir -p "' .. SYNC .. '" 2>/dev/null || mkdir "' .. SYNC .. '" 2>nul')
|
||||
|
||||
-- 3-decimal lat/lon signature of a waypoint list ---------------------------
|
||||
local function sig_of(wps)
|
||||
local parts = {}
|
||||
for i = 1, #wps do
|
||||
parts[i] = string.format("%.3f,%.3f", wps[i].lat, wps[i].lon)
|
||||
end
|
||||
return table.concat(parts, ";")
|
||||
end
|
||||
|
||||
local function read_file(p)
|
||||
local f = io.open(p, "r"); if not f then return nil end
|
||||
local s = f:read("*a"); f:close(); return s
|
||||
end
|
||||
|
||||
local function write_file(p, s)
|
||||
local f = io.open(p, "w"); if not f then return end
|
||||
f:write(s); f:close()
|
||||
end
|
||||
|
||||
-- parse the bridge file: skip "# sig" lines, take "lat lon alt id type" ------
|
||||
local function parse(txt)
|
||||
local wps = {}
|
||||
if not txt then return wps end
|
||||
for line in txt:gmatch("[^\r\n]+") do
|
||||
if line:sub(1, 1) ~= "#" then
|
||||
local lat, lon, alt, id = line:match("^%s*(-?%d+%.?%d*)%s+(-?%d+%.?%d*)%s+(-?%d+)%s+(%S+)")
|
||||
if lat and lon then
|
||||
wps[#wps + 1] = { lat = tonumber(lat), lon = tonumber(lon), alt = tonumber(alt) or 0, id = id or "WPT" }
|
||||
end
|
||||
end
|
||||
end
|
||||
return wps
|
||||
end
|
||||
|
||||
-- read the current in-sim FMS plan ------------------------------------------
|
||||
local function read_fms()
|
||||
local wps = {}
|
||||
local n = XPLMCountFMSEntries()
|
||||
for i = 0, n - 1 do
|
||||
-- FlyWithLua: type, id, ref, altitude, lat, lon
|
||||
local _t, id, _ref, alt, lat, lon = XPLMGetFMSEntryInfo(i)
|
||||
if lat and lon and (math.abs(lat) > 0.0001 or math.abs(lon) > 0.0001) then
|
||||
wps[#wps + 1] = { lat = lat, lon = lon, alt = alt or 0, id = (id ~= "" and id) or "WPT" }
|
||||
end
|
||||
end
|
||||
return wps
|
||||
end
|
||||
|
||||
-- write our plan into the in-sim FMS ----------------------------------------
|
||||
local function apply_to_fms(wps)
|
||||
local old = XPLMCountFMSEntries()
|
||||
for i = 1, #wps do
|
||||
-- lat/lon entries keep our exact coords -> stable round-trip (no drift)
|
||||
XPLMSetFMSEntryLatLon(i - 1, wps[i].lat, wps[i].lon, math.floor(wps[i].alt or 0))
|
||||
end
|
||||
for i = old - 1, #wps, -1 do XPLMClearFMSEntry(i) end -- trim leftovers
|
||||
if #wps >= 1 then
|
||||
XPLMSetDisplayedFMSEntry(0)
|
||||
XPLMSetDestinationFMSEntry(#wps - 1)
|
||||
end
|
||||
end
|
||||
|
||||
local function serialize(wps)
|
||||
local lines = { "# " .. sig_of(wps) }
|
||||
for i = 1, #wps do
|
||||
lines[#lines + 1] = string.format("%.6f %.6f %d %s WPT", wps[i].lat, wps[i].lon, math.floor(wps[i].alt or 0), wps[i].id)
|
||||
end
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
-- main loop (~1×/sec): whichever side differs from the agreed plan wins ------
|
||||
function fms_sync_tick()
|
||||
local to_wps = parse(read_file(TO_SIM))
|
||||
local tsig = sig_of(to_wps)
|
||||
local fm_wps = read_fms()
|
||||
local fsig = sig_of(fm_wps)
|
||||
|
||||
if tsig ~= "" and tsig ~= last_sig then
|
||||
apply_to_fms(to_wps) -- App -> Sim
|
||||
last_sig = tsig
|
||||
elseif fsig ~= last_sig then
|
||||
write_file(FROM_SIM, serialize(fm_wps)) -- Sim -> App
|
||||
last_sig = fsig
|
||||
end
|
||||
end
|
||||
|
||||
do_often("fms_sync_tick()")
|
||||
logMsg("[glass-cockpit] FMS sync active -> " .. SYNC)
|
||||
@@ -0,0 +1,55 @@
|
||||
-- ============================================================================
|
||||
-- X-Plane Glass Cockpit — Terrain awareness probe (FlyWithLua companion)
|
||||
-- ============================================================================
|
||||
-- The web MFD can't read X-Plane's scenery elevation over the Web API. This
|
||||
-- script samples a grid of terrain heights around the aircraft with X-Plane's
|
||||
-- terrain probe and writes them to terrain.json in the sync folder; the bridge
|
||||
-- streams it to the tablets, which colour it red/yellow vs aircraft altitude
|
||||
-- (G1000 TAWS). See terrain-sync in server/fmssync.js.
|
||||
--
|
||||
-- INSTALL: copy to <X-Plane>/Resources/plugins/FlyWithLua/Scripts/ (alongside
|
||||
-- fms-sync.lua). Needs FlyWithLua NG+ (XPLM scenery-probe bindings).
|
||||
-- ============================================================================
|
||||
|
||||
local SYNC = SYSTEM_DIRECTORY .. "Output/fms-sync/"
|
||||
local OUT = SYNC .. "terrain.json"
|
||||
os.execute('mkdir -p "' .. SYNC .. '" 2>/dev/null || mkdir "' .. SYNC .. '" 2>nul')
|
||||
|
||||
local M_FT = 3.28084
|
||||
local ROWS, COLS = 24, 24
|
||||
local DLAT, DLON = 0.35, 0.5 -- half-box (deg) around the aircraft
|
||||
local probe = XPLMCreateProbe(0) -- xplm_ProbeY
|
||||
|
||||
-- terrain elevation (ft MSL) at a lat/lon, via the vertical scenery probe
|
||||
local function elev_ft(lat, lon)
|
||||
local x, y, z = XPLMWorldToLocal(lat, lon, 0)
|
||||
local res, _px, py = XPLMProbeTerrainXYZ(probe, x, y, z)
|
||||
if res ~= 0 then return 0 end -- 0 = xplm_ProbeHitTerrain
|
||||
local _plat, _plon, palt = XPLMLocalToWorld(x, py, z)
|
||||
return math.max(0, math.floor(palt * M_FT))
|
||||
end
|
||||
|
||||
function gc_terrain_tick()
|
||||
local lat = get("sim/flightmodel/position/latitude")
|
||||
local lon = get("sim/flightmodel/position/longitude")
|
||||
local alt = math.floor(get("sim/flightmodel/position/elevation") * M_FT) -- true MSL
|
||||
local n, s = lat + DLAT, lat - DLAT
|
||||
local w, e = lon - DLON, lon + DLON
|
||||
local cells = {}
|
||||
for r = 0, ROWS - 1 do -- r = 0 → north (top)
|
||||
local glat = n - (r / (ROWS - 1)) * (n - s)
|
||||
for c = 0, COLS - 1 do -- c = 0 → west
|
||||
local glon = w + (c / (COLS - 1)) * (e - w)
|
||||
cells[#cells + 1] = elev_ft(glat, glon)
|
||||
end
|
||||
end
|
||||
local f = io.open(OUT, "w")
|
||||
if not f then return end
|
||||
f:write(string.format(
|
||||
'{"lat":%.5f,"lon":%.5f,"alt":%d,"n":%.5f,"s":%.5f,"w":%.5f,"e":%.5f,"rows":%d,"cols":%d,"elev":[%s]}',
|
||||
lat, lon, alt, n, s, w, e, ROWS, COLS, table.concat(cells, ",")))
|
||||
f:close()
|
||||
end
|
||||
|
||||
do_often("gc_terrain_tick()") -- ~1×/sec
|
||||
logMsg("[glass-cockpit] terrain probe active -> " .. OUT)
|
||||
@@ -0,0 +1,66 @@
|
||||
-- ============================================================================
|
||||
-- X-Plane Glass Cockpit — G1000 UI-state publisher (FlyWithLua companion)
|
||||
-- ============================================================================
|
||||
-- The web G1000 mirrors the in-sim G1000's display state. Most of it already
|
||||
-- flows over the Web API (attitude, radios, AP, CDI source, baro, ...). The few
|
||||
-- bits that are G1000-internal (MFD page, map range, PFD inset) aren't standard
|
||||
-- datarefs, so this script reads them and re-publishes them under our own
|
||||
-- namespace, which the bridge then streams to every tablet:
|
||||
--
|
||||
-- glasscockpit/ui/mfd_page Int 0 = MAP, 1 = FPL, 2 = NRST
|
||||
-- glasscockpit/ui/map_range_nm Float active map range in NM
|
||||
-- glasscockpit/ui/inset Int PFD inset map on/off (0/1)
|
||||
--
|
||||
-- INSTALL: copy to <X-Plane>/Resources/plugins/FlyWithLua/Scripts/ (alongside
|
||||
-- fms-sync.lua). The web app follows these when present and falls back to its
|
||||
-- own local control when they're absent — so it never breaks without the plugin.
|
||||
-- ============================================================================
|
||||
|
||||
-- our published values (the create_dataref callbacks read these) ------------
|
||||
local ui_mfd_page = -1 -- -1 = "unknown" -> web keeps local control
|
||||
local ui_map_range_nm = -1
|
||||
local ui_inset = -1
|
||||
|
||||
create_dataref("glasscockpit/ui/mfd_page", "Int", function() return ui_mfd_page end)
|
||||
create_dataref("glasscockpit/ui/map_range_nm", "Float", function() return ui_map_range_nm end)
|
||||
create_dataref("glasscockpit/ui/inset", "Int", function() return ui_inset end)
|
||||
|
||||
-- safe optional dataref readers (nil if the dataref doesn't exist) ----------
|
||||
local function geti(name) local h = XPLMFindDataRef(name); if h then return XPLMGetDatai(h) end end
|
||||
local function getf(name) local h = XPLMFindDataRef(name); if h then return XPLMGetDataf(h) end end
|
||||
|
||||
-- ============================================================================
|
||||
-- TODO (confirm in YOUR sim): the exact G1000 source datarefs differ per
|
||||
-- aircraft. Run the probe below once, read the X-Plane Log.txt, and plug the
|
||||
-- right names in here. Until then these stay -1 and the web app uses its own
|
||||
-- local page/range/inset (no harm).
|
||||
-- ============================================================================
|
||||
local function read_g1000_state()
|
||||
-- MAP RANGE — many G1000s expose an NM range or an enum index. Try a couple
|
||||
-- of common candidates; map_range may be an enum needing a lookup table.
|
||||
local rng = getf("sim/cockpit2/EFIS/map_range") -- <-- verify name
|
||||
if rng then ui_map_range_nm = rng end
|
||||
|
||||
-- PFD INSET on/off — G1000-internal, name varies:
|
||||
-- local ins = geti("sim/cockpit2/EFIS/inset_map_on") -- <-- verify name
|
||||
-- if ins then ui_inset = ins end
|
||||
|
||||
-- MFD PAGE group — G1000-internal, name varies:
|
||||
-- local pg = geti("sim/cockpit2/EFIS/mfd_page") -- <-- verify name
|
||||
-- if pg then ui_mfd_page = pg end
|
||||
end
|
||||
|
||||
do_often("read_g1000_state()")
|
||||
|
||||
-- ---- one-shot probe: log every dataref whose name contains a keyword -------
|
||||
-- Bind to a key/macro, fire once, then read Log.txt to discover the real names.
|
||||
function gc_probe_g1000()
|
||||
local hits = {}
|
||||
for _, kw in ipairs({ "EFIS", "g1000", "GPS/g1000", "map_range", "inset", "mfd" }) do
|
||||
logMsg("[glass-cockpit] probe keyword: " .. kw .. " (search Log.txt / DataRefEditor)")
|
||||
end
|
||||
logMsg("[glass-cockpit] tip: use the DataRefEditor or DataRefTool plugin and filter for 'EFIS' / 'g1000' to find map-range / inset / page datarefs, then edit ui-sync.lua")
|
||||
end
|
||||
add_macro("Glass Cockpit: probe G1000 datarefs", "gc_probe_g1000()")
|
||||
|
||||
logMsg("[glass-cockpit] UI-state publisher active (mfd_page / map_range_nm / inset)")
|
||||
Reference in New Issue
Block a user