A multiplayer and radio plugin for X-Plane 12 and X-Plane 11. Pilots see each other in the sky as real aircraft (CSL models) and talk to each other by voice on the COM frequencies they have tuned, push-to-talk, like the real thing.
Supported: Windows, macOS (Intel + Apple Silicon), Linux, on X-Plane 12 and X-Plane 11.50 or later. On X-Plane 11 everything works the same way except shared weather, which needs the region weather datarefs that only X-Plane 12 has; pilots on 11 fly their own sky and are told so in Log.txt. A sim older than 11.30 has 25 kHz radios rather than 8.33 kHz ones, and the plugin reads those instead.
Nobody has to set a server up.
One of you opens Plugins → XRadio → Settings → Hosting, ticks Host a flight here and saves. The plugin starts the relay server itself, connects to it, asks the router to open the port, and then shows the address to pass on:
Running on port 49100 · 1 connected
Friends type: 81.90.144.12:49100
Router opened the port (Livebox)
Here now: ESNA12
Everyone else puts that address into Settings → Connection and saves. That is the whole procedure — no VPS, no Python, no terminal, and nothing to configure on anyone's router.
If the router will not open the port, the window says so instead of leaving you guessing — and says what to do: friends on the same network use the local address it shows, and for friends over the internet it spells out which UDP port to forward to which machine, and looks up your public address so you have something to send them once that is done. A router that is itself behind a carrier's NAT (mobile internet, some fibre providers) is recognised and the window says hosting over the internet cannot work from there, rather than handing out an address that goes nowhere.
If UPnP is switched off, XRadio asks again over NAT-PMP before giving up.
It is a different, much simpler protocol that plenty of routers answer while
UPnP is disabled -- Apple's, a lot of OpenWrt and Fritz!Box firmware, and
anything running miniupnpd. When it is NAT-PMP that opens the port, the
window says so. python3 tools/upnp_probe.py asks the router the same way
XRadio does, from outside the sim, and says whether the silence is the
router's or a bug here.
The UPnP search goes out of the network interface that carries the internet route and is also sent straight to the gateway, because on a PC with VirtualBox, VMware, Hyper-V or a VPN adapter installed the operating system's default multicast interface is usually the wrong one and the router never hears the question. A dedicated server is still the better answer for a group that wants to fly without waiting for one particular person to be online — see Running a dedicated server.
If the port will not open at all — a router you cannot get into, or mobile internet where forwarding cannot work however you configure it — see Flying together when the router says no. It walks through putting everyone on Tailscale (five minutes each, free for a group of six), the ZeroTier alternative, forwarding by hand, and renting a small VPS, with a table at the top for picking between them.
the host's X-Plane everyone else
┌──────────────────────────┐ ┌──────────────────────────┐
│ XRadio plugin (C++) │ │ XRadio plugin │
│ · reads datarefs │ │ │
│ · sends its position │ │ │
│ · XPMP2 → CSL models │ │ │
│ · PTT → Opus voice │ │ │
│ ───────────────────── │ UDP :49100 │ │
│ relay server (thread) │ ◄───────────── │ │
│ · sessions │ ─────────────► │ │
│ · traffic at 10 Hz │ │ │
│ · frequency routing │ │ │
└──────────────────────────┘ └──────────────────────────┘
The relay server exists twice, on purpose: plugin/src/server.cpp is the one
built into the plugin, and server/server.py is the same thing for a machine
that should stay up without X-Plane running. They speak one protocol, so a
difference between them is a bug — tools/test_server_parity.py drives the
same scenarios at both over real UDP and fails if the transcripts differ.
Voice is Opus at 24 kbit/s — the codec behind Discord and WhatsApp calls — captured and played through miniaudio, which talks to WASAPI, CoreAudio, ALSA or PulseAudio at run time so the plugin has no audio library dependencies.
Rendering other aircraft is handled by XPMP2 — the same library LiveTraffic and xPilot use. It takes care of loading CSL models, the X-Plane instancing API, the TCAS override and the map layer, and it already runs on all three platforms.
Other aircraft are interpolated in the sender's own timeline rather than
drawn as reports arrive — see plugin/src/smoothing.h. Each position report
carries the sender's timestamp, and receivers render a fixed 350 ms behind the
newest one. Network and relay timing then cannot affect the motion at all.
The wire protocol is packed binary UDP, little-endian. It is defined in two places and the two must stay in sync:
plugin/src/protocol.h(C++)server/protocol.py(Python)
tools/check_sizes.cpp verifies that the struct sizes match, and the same
check covers the packet budget: every datagram stays under 1200 bytes, not
the 1400 an ethernet MTU allows, because a group whose router will not forward
a port puts everyone on Tailscale or another WireGuard-based VPN and those
hand you a 1280-byte link. More aircraft in range than fit in one traffic
packet are sent in several, so the packet size never decides how many
aeroplanes a pilot can see.
Only worth it if you want a server that is up whether or not any particular person is flying. For an evening with friends, hosting from inside the sim is simpler and does the same job.
cd server
python3 server.py --host 0.0.0.0 --port 49100 -vAdd --dashboard for a live picture of the flight in the terminal -- who is
in, where, what they have tuned, who is talking -- instead of a log; --log FILE keeps the log as well. No dependencies beyond Python 3.10+. Open
UDP port 49100 — in the
machine's firewall and in the provider's, which is where most of the wasted
evenings happen. A systemd unit is provided in server/xradio.service.
The release package also carries the same server as a compiled binary, for a machine without Python. It takes its arguments positionally:
./server/lin_x64/xradio_server 49100 # or win_x64\ , mac_x64/
./server/lin_x64/xradio_server 49100 yourword # port, then flight passwordRunning a dedicated XRadio server is the full version:
picking and sizing a VPS (the cheapest one is enough — measured, eight pilots
is 21 MB of RAM and half a megabit), choosing where to put it so the latency
lands in the middle of the group, the systemd unit with the password kept out
of it, proving it works with tools/fake_client.py before anyone is invited,
how weather and time work with no sim to take them from, and the update
routine — the server has to be updated whenever the protocol changes, or
every pilot is refused at login.
Everyone in a flight has to be on the same build, so a pilot on an old one does not merely miss features -- they cannot join at all. Once per sim session, twenty seconds after startup, the plugin asks GitHub's releases endpoint what the newest version is; if it is newer than the one running, the main window says so and prints the address:
XRadio v0.6.0 is out -- you are on v0.5.2
github.com/Anviktor2411/xradio/releases
Nothing is downloaded and nothing is installed -- the pilot decides. The check is one request and can be switched off entirely in Settings > Connection > Tell me about new versions, after which nothing is sent anywhere. It uses the HTTP stack the operating system already has (WinHTTP on Windows, curl elsewhere) rather than carrying a TLS library into the sim, and a site that is down, slow or talking nonsense is simply ignored.
bash release.sh # or double-click release.bat on Windows
It asks for the version number, suggesting the next patch:
current version : v0.5.2
new version [v0.5.3]: v0.6.0
Then it runs every check this machine is equipped for -- the portability
scan, the Windows cross-compile, the C++ tests, the differential server
test, the DLL imports of whatever is built for Windows in build*/ -- and
only if they all pass does it set the version in
plugin/src/brand.h (the one place it lives; the window and the tests both
read it from there), write dist/xradio-v0.6.0-source.zip, and leave a
release-notes skeleton beside it with the compare link filled in. A failed
check leaves the tree untouched, so a broken build cannot become a release.
Machines missing a tool skip that check and say so rather than failing, so
it is usable from Windows without mingw-w64 installed. What it deliberately
does not do is upload anything or tag anything: the binaries pilots install
come from the CI run's XRadio-all-platforms artifact, not from this zip.
bash tools/check_portability.sh # instant: MSVC-only pitfalls
bash tools/check_windows_build.sh # ~20s: compile everything for Windows
python3 tools/test_server.py # server: protocol and routing, ATC (v6) included
bash tools/check_simbrief.sh # SimBrief: parser, .fms writer, a live fetch against tools/fake_simbrief.py
python3 tools/check_win_imports.py build/XRadio/win_x64/XRadio.xpl # Windows: no DLL a pilot's PC lackscheck_portability.sh catches the mistakes that pass on Linux and macOS and
fail on Windows ten minutes later — M_PI (POSIX, not standard C++, and MSVC
only defines it when _USE_MATH_DEFINES precedes <cmath>), unguarded POSIX
headers, variable-length arrays, std::min without <algorithm>, and a
Win32 symbol used without the header that declares it. Use xr::kPi from
plugin/src/mathconst.h rather than M_PI.
check_windows_build.sh goes further and actually compiles every source file
for Windows with MinGW, from Linux, in about twenty seconds. Code inside
#ifdef _WIN32 is invisible to the Linux and macOS compilers, so a missing
header there passes two of the three CI jobs and fails the third ten minutes
in — SIO_UDP_CONNRESET is declared in <mstcpip.h>, not <winsock2.h>,
and that is exactly how it was found. MinGW is not MSVC and will not catch
everything, but it compiles the Windows branches against real Windows
headers, which is where that whole class of mistake lives.
Starts a real server on a spare port and drives it over real UDP, covering the wire format, login, traffic filtering, frequency and range routing, hostile input, the length cap and session timeouts.
The plugin is tested too, without needing X-Plane. tools/harness/ implements
just enough of the XPLM API for the real plugin code to run as an ordinary
executable, so CI can point it at a real server and check what its window
would display:
cmake -B build-tests -DXRADIO_BUILD_TESTS=ON -DXRADIO_USE_XPMP2=OFF \
-DXPLANE_SDK=$PWD/tools/fake_sdk
cmake --build build-tests -j
./build-tests/voice_test
./build-tests/placement_test
./build-tests/settings_test
./build-tests/hosting_test
python3 tools/test_server_parity.py
./build-tests/xradio_server 49700 &
python3 tools/harness/fuzz_client.py --port 49700
python3 server/server.py --port 49400 &
python3 tools/fake_client.py --port 49400 --callsign PEER01 --lat 57.86 --lon 27.03 --talk --parrot &
./build-tests/plugin_harness 127.0.0.1 49400 CIRUNThe same build runs on Windows with Visual Studio's compiler: from an x64
Native Tools prompt add -G Ninja to the configure line and run the
executables with .exe. Two things differ there. The stub SDK has no XPLM
import library, so a test build on Windows leaves the .xpl itself out and
says so when configuring. And Windows runs its own SSDP service on UDP 1900,
so the UPnP tests need the fake router and the client to meet elsewhere:
python tools/harness/fake_igd.py --mode ok --ssdp-port 11900, with
XRADIO_SSDP_PORT=11900 set alongside XRADIO_UPNP_GATEWAY=127.0.0.1 when
running upnp_test.
The harness keys the PTT for two seconds; --parrot makes the peer send every
voice frame it hears straight back, so the run proves the whole loop —
microphone, encoder, server, decoder, mixer — end to end. It exits non-zero
if fewer than the expected frames make it round.
tools/harness/smoothing_test.cpp flies a simulated aircraft through a turn,
pushes its reports through a relay that samples on its own clock (duplicating
and skipping some) and a jittery network, renders at 60 fps, and asserts the
motion never runs backwards or jumps. It runs the previous dead-reckoning
approach through the same scenario first, to prove the test catches the bug.
tools/harness/voice_test.cpp pushes a tone through the microphone path,
Opus, a simulated network and the playback mixer with no audio hardware, and
checks half-duplex muting, packet-loss concealment, sequence wrap-around and
garbage frames.
tools/harness/settings_test.cpp covers the settings window the way a pilot
uses it: it reads back the strings the window draws, clicks the rows those
strings were actually drawn on, switches tabs, drags sliders, flips toggles,
types into fields, and checks what lands in xradio.cfg. It also asserts the
things that are easy to get wrong and impossible to see — that a burst of
keystrokes arriving in one frame all land in the field that was focused when
each was typed, and that Enter saves the character typed just before it.
tools/harness/hosting_test.cpp is the hosting feature end to end: it ticks
Host a flight here in the settings window, waits for the plugin's own client
to connect to its own server, then opens a real UDP socket and joins as a
second pilot. It checks that each sees the other's aircraft, that text
reaches both ways through the hosted server, that the Hosting tab reports the
truth, and that unticking the box really does stop the server and refuse new
joins.
tools/harness/ui_apply_test.cpp is the whole plugin with its Dear ImGui
windows, headless (the draw lists are read, nothing is rendered), against a
fake server: it presses Apply fifty times -- callsign, port, UI scale, theme,
language, audio devices, hosting -- on the Settings tab, on Host / Connect and
through the wizard, with the cockpit overlay and (XR_TOWER_PORT pointing at
tools/fake_towertrainer.py) Tower mode's windows open. After every frame the
current ImGui context is what it was before, no window is left mid-frame, no
IM_ASSERT failed, the settings reached the file, the fonts were rebuilt
between frames rather than inside the click, and each change reconnected
exactly once. v0.6.0 crashed X-Plane on the first press; its code fails this
test with a segmentation fault.
tools/harness/crashlog_test.cpp is the crash handler: an address named as
<module>+<offset>, and on Windows a child process that really crashes and
leaves XRadio crash: access violation ... at crashlog_test.exe+0x... with a
stack, the way Log.txt would show it.
tools/test_server_parity.py is a differential test between the two server
implementations. It starts a fresh Python server and a fresh C++ server for
each of eight scenarios, drives identical traffic at them over real UDP, and
compares what the clients receive. Every position-validation rule is probed
with its own timestamped report, so dropping one rule from one server shows
up as a state the watching client should never have seen.
tools/harness/fake_igd.py is a fake home router — SSDP, the device
description, the SOAP control endpoint — that deliberately does the awkward
things real firmware does: a chunked description, a relative control URL
resolved through <URLBase>, and in lease mode a refusal of permanent
mappings with error 725 until asked for a timed one. upnp_test.cpp drives
the real client against it in each mood (ok, lease, refuse,
private-wan, deaf) and checks the mapping, its removal, the public
address, the double-NAT detection and the plain-words errors.
tools/harness/placement_test.cpp drives XPluginStart under several monitor
layouts — including a second monitor to the left of the main one, which makes
X-Plane's global desktop start at a negative x — and asserts both windows land
fully on screen.
tools/harness/fuzz_client.py is the mirror image, and it matters more now
that hosting puts the server on a pilot's home connection: it throws ~11,000
malformed, truncated, length-lying and outright random datagrams at the
server while a real session runs alongside, then checks the server is still
answering and still accepting new pilots. Run against the ASan build, one
out-of-bounds read stops it with a stack trace.
tools/harness/fuzz_server.py is the same harness pointed at a deliberately
hostile server that answers with truncated, inconsistent and random packets.
Run under AddressSanitizer it proves the packet parser cannot be pushed out of
bounds. CI runs all of this on every push, plus a check that the C++ and
Python struct layouts still agree byte for byte.
In two terminals:
python3 tools/fake_client.py --callsign ESNA12 --lat 57.85 --lon 27.02 --talk
python3 tools/fake_client.py --callsign ESNB34 --lat 57.88 --lon 27.05 --heading 250Both should list each other on the traffic: line, and ESNB34 should receive
ESNA12's text message.
fake_client.py --script FILE (or --script - for stdin) plays a pilot from a script: say TEXT on
COM1 (with {squawk}, {climb}, {fl}, {rwy}, {sid} filled from the last text received),
waitfor REGEX [seconds], com1 KHZ, pos LAT LON ALT_FT GS_KT HDG ground|air, squawk NNNN,
plan DEP DEST ROUTE, wait S, end. TowerTrainer's tests/aiatc.test.mjs flies one from a stand
at EETN to a stand at EVRA under its AI controllers this way.
The window needs lib/imgui (fetched by tools/fetch_deps.sh); -DXRADIO_UI_IMGUI=OFF builds the text-only
window instead. tools/ui_preview renders the window to PNG files without X-Plane (docs/ui/).
Requirements: git, CMake 3.16+, and a C++17 compiler (Windows: Visual Studio 2022 C++ tools; macOS: Xcode command line tools; Linux: gcc or clang).
1. Fetch dependencies. XPMP2 (which bundles a complete X-Plane SDK), Opus
and miniaudio, all into lib/:
./tools/fetch_deps.sh # macOS / Linux
powershell -ExecutionPolicy Bypass -File tools\fetch_deps.ps1 # Windows2. Build:
# macOS / Linux
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
# Windows
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config ReleaseThe result follows X-Plane's fat-plugin layout:
build/XRadio/
win_x64/XRadio.xpl # produced on Windows
mac_x64/XRadio.xpl # produced on macOS
lin_x64/XRadio.xpl # produced on Linux
Resources/ # XPMP2 data files (related.txt, Doc8643.txt, ...)
3. Install: copy the build/XRadio folder to
X-Plane 12/Resources/plugins/XRadio/.
For a single package that works on all three platforms, build all three and put
win_x64, mac_x64 and lin_x64 in the same XRadio/ folder. The GitHub
Actions workflow (.github/workflows/build.yml) does this for you: every push
builds all three and uploads the results as artifacts.
The macOS build is a universal binary (arm64 + x86_64).
The .xpl must be named after the plugin folder — XRadio/win_x64/XRadio.xpl,
not win_x64/win.xpl. That is the SDK 3.0 rule; with the wrong name X-Plane
skips the folder without writing anything to Log.txt, so it looks as though
the plugin does not exist.
Windows also refuses a plugin when a DLL it imports cannot be found, and all
Log.txt then says is Error Code = 126 : The specified module could not be found. That is why a MinGW-w64 build (-G "MinGW Makefiles") links the
compiler's runtime -- libstdc++, libgcc, winpthreads, libssp -- into every
binary: linked dynamically, the plugin needs libstdc++-6.dll and friends
and loads only on the PC that has the compiler. After every Windows build
tools/check_win_imports.py reads the plugin's import table and fails the
build if it names a DLL that neither Windows nor X-Plane provides.
The test executables (-DXRADIO_BUILD_TESTS=ON) run under AddressSanitizer
and UBSan on GCC and Clang. A toolchain that has no sanitizer runtimes --
MinGW-w64 on Windows, for one -- builds them with -DXRADIO_SANITIZE=OFF.
cmake -B build -DXRADIO_USE_XPMP2=OFF -DXPLANE_SDK=<path to SDK> # no 3D models
cmake -B build -DXRADIO_USE_VOICE=OFF # no voiceWithout XPMP2 other pilots only appear in the window's list; without voice the PTT still marks you as transmitting but nothing is sent.
XPMP2 needs CSL models in OBJ8 format. Without them everything still works, but
other aircraft are invisible (a warning appears in Log.txt).
Put model packages here:
X-Plane 12/Resources/plugins/XRadio/Resources/CSL/<package>/
Each package root needs an xsb_aircraft.txt. A free, open set is
Bluebell (https://github.com/oktalist/BluebellCSL), which covers most
common types. When no model matches, XPMP2 falls back to the actype from
xradio.cfg.
COM1 and COM2 only work when the avionics bus is on, the radio's own power
switch is on, and there are volts behind them -- the same three things a
pilot checks when a radio is dead. An aircraft sitting cold and dark is off
the air: push-to-talk does nothing, nobody hears you, and you hear nobody.
The window says COM1 122.800 (no power) rather than looking broken, and
switching the avionics off mid-transmission unkeys you.
An aircraft that models none of this -- no avionics or bus datarefs at all -- is treated as powered, so an unusual add-on does not go silent.
Two pilots flying together in different weather is worse than it sounds: one breaks out of cloud at 600 ft while the other is in sunshine, the winds disagree so the aircraft holding station drifts away, and the same approach happens in daylight for one and at night for the other.
So the host's sim decides the sky. Their weather and clock are sent to everyone else every ten seconds -- X-Plane 12's region weather whole, not a summary of it: sea-level pressure and temperature, visibility, rain, the three cloud layers, and all thirteen altitude levels of wind, temperature, dewpoint and turbulence, plus zulu time and the date.
Both halves are opt-out. The host can untick Settings > Hosting > Share my weather and time, and anyone who has set up their own conditions can untick Settings > Traffic > Follow the host's weather and time and keep them.
The clock is only moved when it has really drifted (more than 30 s); nudging it on every packet fights the sim's own clock and shows up as a twitching sun. The server relays weather only from the pilot who claimed it when they logged in, so a joining pilot cannot quietly move everyone else's weather, and the whole feature is off on X-Plane 11, whose region datarefs do not exist.
Two pilots who join the same server and do not already know what frequency the other is tuned to cannot reach each other: text and voice go only to people on that frequency, which is the point of modelling a radio at all. XRadio does not solve that by showing you everyone's frequency — you cannot tell what another aircraft is monitoring in real life either — but by giving you the two things aviation already uses for it.
121.500 is guard. Anything transmitted on it is heard by every pilot within
VHF range whatever they have tuned, and the window labels it GUARD 121.500 so
nobody mistakes it for an ordinary call. Tune a radio to 121.500 and ask.
[GUARD 121.500] ESNA12: anyone out there, come to 118.100
You still have to be tuned to 121.500 to transmit on it — the rule that you cannot talk on a frequency your radio is not on is not relaxed, only the listening half is. And it is still a radio: the VHF horizon applies exactly as it does anywhere else, so guard does not reach across the country.
@CALLSIGN is a message for one pilot. Typed in the Say row, it reaches
that pilot wherever they are and whatever they have tuned, and nobody else sees
it. It is not a transmission and it is not on a frequency, so the window labels
it as what it is:
Say: @ESNB34 come to 118.100
[direct to ESNB34] ESNA12: come to 118.100
[direct] ESNB34: on my way
You do not have to know the callsign. Type @ and the window lists everyone
in the flight except you — nearest first, with how far away they are when they
are in your traffic, and the rest after them, however far away. Keep typing to
narrow it, then Tab, Enter or a click to fill the name in:
> @ESNB34 A20N 4.2 nm
@DLH441 B738 46.1 nm
@RYR77K B738
Tab or Enter picks, arrows move, or click a name
Say: > @_
The list comes from the server, so a pilot on the other side of the country is in it even though they are nowhere near your traffic. Against a server older than v0.5.6 it lists the aircraft in your traffic instead.
A callsign nobody in the flight is using comes back with an answer rather than
silence. An @ anywhere but the start of the line is just an @.
Most pilots fly with the XRadio window shut — it is a large grey rectangle over the scenery. Shut, a call on the radio used to leave no trace at all: the radio log is inside the very window nobody is looking at.
So while the window is closed, anything that arrives appears for ten seconds in the top-left corner, where X-Plane puts its own messages:
ESNB34 is calling on 122.800
[GUARD 121.500] ESNA12: anyone out there, come to 118.100
[direct] ESNB34: on my way
Radio calls, guard calls and @CALLSIGN messages, plus a line when somebody
keys up on a frequency you monitor — so a closed window still tells you who is
talking and where, not only who typed. Guard and direct messages are amber,
voice is green, ordinary chatter white. The notice takes no clicks, so it can
never swallow one meant for the panel underneath, and it disappears the moment
the window is opened, because the log is then right there. Switch it off in
Settings → Audio → Show messages when the window is closed.
The header carries a version and the server rejects anything that does not
match, so all pilots and the server must run the same build. v2 added the
position timestamp, ground track and vertical speed that the smoothing needs;
a v1 client gets no error beyond being ignored. Shared weather arrived
without a version bump: it is a new packet type, which older builds ignore,
so a flight with mixed builds simply does not share a sky. v4 put the
transponder on the wire, v5 the addressee that makes @CALLSIGN possible --
both are changes to existing packets, so both refuse an older build outright
rather than letting it fly with half the behaviour. v6 (XRadio 0.6.0) is air
traffic control: stations, injected traffic, flight plans and the ATIS, four
new packets and two login flags, described in docs/atc.md. A
v0.5.x build meeting a v6 server is told so in its window.
A controller can join a flight: a program such as
TowerTrainer logs in as a
station (EETN_TWR), sees everybody, takes flight plans, publishes an ATIS and
fills the airport with computer-flown traffic that is drawn, on TCAS and on
the radio like any pilot. In the XRadio window you get an ATC online list
(click a station to tune COM1, an ATIS to tune COM2), the ATIS text when you
tune its frequency, and your flight plan's status; Settings → ATC files
the plan and holds the cap on computer-flown aircraft. Calls from a
controller and private messages are read out by the sim so they are not
missed with the window shut. The whole of it, for pilots and for programs
that want to be ATC: docs/atc.md.
One window with a sidebar -- Radio (COM1/COM2 stack with active and standby, station names, RX/TX lights,
signal strength, volumes, guard, the push-to-talk panel), ATC (controllers online, click to tune, ATIS),
Pilots (roster with distance, frequency, talking indicator, AI badges, filter), Messages (chat-style log,
private and ATC highlighted, unread badge, quick replies, @-completion), Flight plan (SimBrief import,
file, plan vs. clearance), Host / Connect (hosting with the address to share and router status, join by
address or code, connection quality and server version), Settings (grouped; mic test, key capture, scale,
theme, language English / Eesti) and Tower -- plus a compact cockpit overlay with toasts and a four-step
first-run wizard. Drawn with Dear ImGui (MIT) in X-Plane's own windows: pop-out, VR and HiDPI like any
other. Screenshots of every tab and the details are in docs/ui.md; the earlier text-only window remains
as the fallback build (-DXRADIO_UI_IMGUI=OFF).
Settings → SimBrief: your SimBrief username or Pilot ID (kept only in XRadio's settings file). Then, on
the Flight plan tab, Import from SimBrief fetches your latest OFP through SimBrief's documented
fetch API (xml.fetcher.php?username=…&json=1, or userid= for a Pilot ID) -- only when you press the
button, never on its own -- and fills departure, destination, alternate, route (SID, airways, STAR),
cruise level, TAS, EOBT and equipment. You can edit everything before File sends it to the
controller as PT_FLIGHTPLAN; the OFP numbers stay on the tab (runways, fuel, weights, times, a link
to the PDF), Write X-Plane .fms puts the route into Output/FMS plans/<DEP><DEST>.fms for the
FMS (an existing file is only replaced on a second press), and once ATC answers, the clearance is
shown next to the plan with squawk, SID, level and destination -- a difference is marked. HTTP 400
means SimBrief does not know the user or no flight has been planned yet; Plan on SimBrief opens
SimBrief's own new-flight page in the browser. Nothing is scraped and no login is touched; the plugin
holds no SimBrief API key.
In TowerTrainer the filed plan lands on the pilot's strip with the route, RFL, alternate, EOBT and
equipment, and goes into EuroScope's $FP with the SIMBRIEF remark.
Plugins → XRadio → Tower mode (command xradio/tower/toggle) puts you in the tower cab of the airport
TowerTrainer is running, with its traffic in the sky and on
the taxiways: the camera goes to the viewpoint the scenery's apt.dat gives (row 14; the trainer's
OpenStreetMap tower or the apron centre otherwise), your own aircraft is frozen where it stands and put
back exactly when you leave, and five windows open -- a character radar, the strips with ACCEPT for
incoming handoffs, the Delivery list, your frequency's log with a command line, and ATIS / operations
(LVP, runway, stop bars, light gun). What you type is what you would type in the trainer; the trainer
stays the authority. The plugin talks to the trainer's tower link (server.py --tower-port, JSON lines
over TCP, docs/tower-mode.md) as a remote controller of its session, so it works with XRadio connected
to a flight (the traffic then also arrives through the injector) and standalone against server.py
alone, when it draws the trainer's traffic as CSL models itself. Settings → Tower: address, port, session
code (TOWER), position, nickname, freeze, weather and time sync, cab height, 3D traffic, windows.
Easiest way: the Settings button at the top of the XRadio window, or
Plugins → XRadio → Settings... in the sim. The same row of buttons has
Reconnect and Hide, and while hosting a Copy address button that puts
the address friends type on the clipboard. The settings window has four
tabs; click a field and type, click a toggle to flip it, click anywhere
on a slider's bar to set it, click the < > arrows to step through the audio
devices. Tab moves to the next field, Enter saves, Escape or Cancel backs out
without changing anything. Saving writes the config file and applies
everything immediately — no restart, no text editor; only a change to the
server, port, callsign or aircraft type causes a reconnect.
Connection
| setting | what it does |
|---|---|
| Server host / Port | where to connect |
| Callsign | how you appear to everyone else; upper-cased on save |
| Aircraft type | ICAO type code that picks the CSL model others see you as |
| Connect on startup | off means you connect by hand from the menu |
| Report rate | position reports per second, 1–10. Lower it on a weak uplink |
| Smoothing delay | how far behind the newest report other aircraft are drawn, 100–1000 ms. Higher rides out worse jitter at the cost of lag |
Audio
| setting | what it does |
|---|---|
| Microphone / Output | pick a device, or leave empty for the system default |
| Volume | incoming radio volume |
| Hear own voice | sidetone: hear yourself while keyed, like a real headset |
| Radio noise | scales every bit of noise the radio makes: the faint floor under a strong signal, the hiss that grows towards the horizon, the squelch bursts. 0 for none |
| Squelch | how strong a signal has to be before the receiver opens for it, like the knob on the radio. Turned up, a distant station is kept out; all the way down the squelch is open — constant static, and the faintest station gets through |
| Cockpit noise | how much of the other pilot's cockpit comes through their microphone. It goes through their transmitter's limiter like the voice does, so it sits under the words and swells up between them |
| Radio sound | the whole VHF radio character — limiter, overdrive, squelch, 300–2700 Hz filter, distance fading, the blocked squeal. Off = clean audio |
The audio tab also shows a live mic level meter and the voice status line, so you can hold the PTT and confirm the right microphone is being heard before you go looking for someone to talk to.
Hosting
| setting | what it does |
|---|---|
| Host a flight here | run the relay server inside the plugin. Your own aircraft connects to it, so the Connection tab is ignored while this is on |
| Port to host on | the UDP port friends connect to. 49100 unless something else is using it |
| Ask the router to open it | UPnP: opens the port automatically so nobody touches a router config page. Off if you have forwarded the port yourself, or if your router's UPnP is switched off |
| Rare tropospheric ducting (realism) | off by default. On a warm, still evening the flight's shared weather may let VHF reach far past the horizon over the sea, as it really does now and then; the radio log says when it begins and ends. Changing it does not restart the server (docs/radio.md) |
Traffic
| setting | what it does |
|---|---|
| Draw other aircraft | off removes the CSL models; the radio keeps working |
| Callsign labels | the floating name tags |
| Label range | how far away labels stay readable, 1–100 nm |
| Traffic range | how far away aircraft are drawn, 5–200 nm |
The same values live in X-Plane 12/Output/preferences/xradio.cfg, written on
first run:
host = your.server.address
port = 49100
callsign = ESNA12
actype = C172
autoconnect = yes
reporthz = 5
smoothms = 350
mic =
speakers =
volume = 0.8
sidetone = no
hiss = 0.35
squelch = 0.1
cabinnoise = 0.3
radiofilter = yes
showtraffic = yes
showlabels = yes
labeldist = 20
range = 80
hosting = no
hostport = 49100
hostupnp = yes
hostducting = noOut-of-range numbers are clamped to the limits above rather than rejected, and unknown keys are ignored, so a hand-edited file cannot stop the plugin loading. If you edit it while X-Plane is running, pick up the changes with Plugins → XRadio → Reconnect.
If a window ever ends up somewhere you cannot reach it — dragged off-screen, or the monitor layout changed while X-Plane was running — use Plugins → XRadio → Reset window position.
PTT key: in X-Plane's Keyboard or Joystick settings, search for the
command xradio/ptt and bind it to a key or joystick button. Hold it to talk
on whichever COM the audio panel has selected for transmit; release it to
listen. Like a real radio it is half-duplex — you do not hear others while you
are keyed. And like a real transceiver it has a stuck-mic timer: after 35
seconds of continuous key-down it stops transmitting and says so in the radio
log, until the key is released and pressed again, so a jammed switch cannot
block the frequency for everybody.
The window's Voice: line tells you what is going on:
| shows | meaning |
|---|---|
OK mic: <device> (opens when you talk) |
speakers open, microphone found and left alone until your first PTT |
OK mic: <device> |
microphone open; you are set |
no microphone (receive only) |
no input device found — you can hear but not talk |
microphone would not open (receive only) |
one was found but refused; the next PTT tries again |
no speakers / no audio backend |
voice is off; see Log.txt |
MIC [######....] |
live level while the PTT is held — if this stays at dots, X-Plane is not getting your microphone |
RX: SU-CBB |
who you are hearing right now |
XRadio does not open your microphone when X-Plane loads. It opens it the first time you press the PTT — the first fraction of a second of that one call is spent opening it — and gives it back after ten minutes without a transmission, or when the plugin is disabled. A microphone in use is not free: a Bluetooth headset whose microphone is open drops into its hands-free mode, and everything else you hear through it — the sim, other plugins' sounds, music — goes mono, narrow and hollow. If you fly with one and want to talk, pick a different microphone (a USB one, or the PC's own) on the Audio tab and the headset keeps its full sound.
Pick the microphone and output device on the settings window's Audio tab, or leave them empty for the system default. Windows: check Settings → System → Sound → Input is the headset you mean. macOS: the first key press may trigger the microphone permission prompt; grant it and press again.
Each transmission is Opus at 24 kbit/s — about 3 KB/s per person talking.
A band-pass on its own sounds like a telephone. What makes a COM radio sound
like one is the rest of the chain, so all of it is modelled on the receiving
side, in the order the real signal goes through it (plugin/src/voice.cpp,
struct Radio):
| stage | what it does |
|---|---|
| transmitter | the other pilot's cockpit comes in through their microphone — engine and wind, a roar rather than a hiss; a modulation limiter squashes every syllable to the same level and holds that roar down under the words, letting it swell back up between them; then a soft overdrive adds the crunch that consonants get when a voice over-modulates |
| channel | noise that rises as the signal weakens -- towards the edge of the coverage, or behind a ridge; the signal flutters, a slow wobble in its level that deepens as it weakens; near the edge the audio breaks up, whole 20 ms frames going missing; two people keying at once fight it out by level: more than 6 dB apart the stronger captures the receiver and the weaker is only a whine underneath, within 6 dB both garble under the beating heterodyne squeal |
| receiver | a squelch knob: a station weaker than its setting does not open the receiver at all, and with the knob all the way down the squelch is open and there is constant static; the squelch opens with a click and a burst of noise, closes with a longer burst and a click once the carrier drops; a 4th-order 300–2700 Hz audio filter with a headset's lift around 1.8 kHz; an output stage that cannot be driven past two thirds of full scale, whatever comes in |
How strong a signal is comes from the radio model the server routes by
(docs/radio.md): the VHF horizon worked out from both aerials' heights
above the ground (1.23 × (√h₁ + √h₂) nm, never under 5 nm), an ATC station's
service volume, and a link budget -- transmitter power less free space path
loss against the receiver's noise. A signal is clean well inside the coverage
and fades to nothing at its edge, where the server stops relaying it anyway;
the plugin also samples the terrain on the path from the scenery, so a station
behind a mountain comes in weak and noisy, or not at all. Someone the traffic
list does not know about counts as strong.
To hear it without X-Plane, tools/harness/radio_demo.cpp renders a WAV
through the real pipeline — microphone path, Opus, the radio — and writes
four versions: clean, a strong signal, one from the horizon, and two people
transmitting at once:
./build-tests/radio_demo call.wav out # out_clean.wav, out_strong.wav, ...tools/harness/voice_test.cpp pins each of these down: the filter's cut-off,
the limiter bringing 24 dB of input spread to under 8 dB, the squelch
bursts and the silence after them, the noise rising with distance, the
dropouts, the squeal, and the output ceiling.
- ATC stations, computer-flown traffic from an ATC program, flight plans with squawk and clearance back, the ATIS as text, the ATC online list with click-to-tune (v0.6.0, see docs/atc.md)
- Login, automatic reconnect, session timeouts
- Own position sent at 5 Hz (location, attitude, speed, gear, flaps, lights)
- Other aircraft actually visible — CSL models, TCAS, map layer, animated gear/flaps/lights, callsign label
- Whoever is transmitting on a frequency you monitor gets a green
[TX]label - COM1/COM2 frequencies and audio panel read from the sim
- Text messages routed by frequency and VHF line-of-sight range, without needing the PTT held; the server rejects transmissions on a frequency you are not actually tuned to
- Smooth motion — positions are timestamped at the sender and interpolated 350 ms behind, so other aircraft move continuously regardless of network jitter or relay timing; extrapolation covers a short dropout, then it holds
- Voice on the COM frequencies — push-to-talk, Opus at 24 kbit/s, 20 ms frames, packet-loss concealment, half-duplex, radio band-pass; runs on its own network thread so audio never waits for a frame
- Hosting from inside the sim — one pilot ticks a box and the plugin runs the relay server itself, opens the router port over UPnP and shows the address to share; no VPS, Python or terminal for anyone
- Tabbed in-sim settings window — connection, audio (device pickers, volume, sidetone, hiss, radio filter, live mic meter), traffic (models, labels, ranges) and hosting — with validation and changes applied without a restart
- Windows placed against the monitor X-Plane is actually using, so they do not spawn off-screen on multi-monitor setups
- Builds on Windows, macOS and Linux; CI checks all three
Traffic is sent within 80 nm, but the radio reaches as far as the VHF horizon
(1.23 × (√h₁ + √h₂) in feet and nautical miles, from each aerial's height
above the ground — about 135 nm for two aircraft 3000 ft up, 7.7 nm for two on
the ground). So you can hear someone you cannot see, exactly as in real life;
docs/radio.md has the whole model.
Both ends treat the wire as hostile. The server drops position reports with NaN, infinite or out-of-range values instead of relaying them onward, and strips control characters from callsigns. The plugin does the same on the way in: implausible traffic entries are skipped rather than handed to the renderer, text is clamped and scrubbed before it reaches the window, and strings that arrive without a null terminator are handled as fixed-width.
Hosting opens a UDP port on your machine to the internet, and the relay server behind it is the same code either way -- it validates every field of every packet before relaying it, and the fuzzing below is run against it. It is still a port, though: host when you want to fly with people, and untick the box when you are done, which also asks the router to close it again.
The server has no authentication: anyone who knows the address can join under any callsign. Packets after login must carry the session id the server issued, which stops blind off-path spoofing, but this is a flying-with-friends server, not a hardened public service. Run it on a port you are willing to expose, and do not reuse it for anything sensitive.
- A list of public servers, so pilots who do not know each other can find a flight without swapping addresses. (Join codes already cover the swapping part.)
- Bandwidth — traffic is relayed at 10 Hz to every client in range; scaling past a couple of dozen pilots wants per-client rate limiting by distance.
- Slats, spoilers and reversers on the wire — the models follow the flaps for now.
Pull requests are welcome. docs/hacking.md is the map: what lives where, which thread touches what, how the wire protocol is kept in step between the plugin and the server, how a setting or a packet type is added, and how the tests work. The one rule that matters more than any other: a change to one relay server is a change to both, and the parity test will hold you to it.
MIT — see LICENSE. XPMP2 is MIT, Opus is BSD-3 and miniaudio is public
domain / MIT-0, so all are compatible.

