-- ============================================================================ -- 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: -- -- /Output/fms-sync/to_sim.txt written by the bridge (our plan) -- /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 /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)