Skip to content
Anviktor2411Public

About

Multiplayer and VHF radio for X-Plane 12 and 11. See friends as CSL models, talk push-to-talk on the COM frequencies with the sound of a real radio, fly one shared sky, and host a flight from inside the sim with no server to set up. Windows, macOS, Linux.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

97 Commits

Folders and files

Repository files navigation

XRadio

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.

Flying together

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.

Architecture

  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.

Running a dedicated server

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 -v

Add --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 password

Running 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.

Telling pilots about a new version

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.

Cutting a release

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.

Tests

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 lacks

check_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 CIRUN

The 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.

Testing without X-Plane

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 250

Both 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.

Building the plugin

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   # Windows

2. 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 Release

The 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.

Building with parts switched off

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 voice

Without XPMP2 other pilots only appear in the window's list; without voice the PTT still marks you as transmitting but nothing is sent.

CSL models

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.

A radio needs electricity

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.

One sky for the whole flight

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.

Finding each other on the radio

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 @.

With the window closed

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.

Protocol versions

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.

Air traffic control

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.

The window

the Radio tab

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).

SimBrief: your flight plan from the OFP

the Flight plan tab

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.

Tower mode: control from the tower in X-Plane

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.

Configuration

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 = no

Out-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.

Voice

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.

The radio sound

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.

What works today

  • 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

Two ranges, on purpose

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.

Untrusted input

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.

Security

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.

Roadmap

  1. 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.)
  2. 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.
  3. Slats, spoilers and reversers on the wire — the models follow the flaps for now.

Contributing

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.

License

MIT — see LICENSE. XPMP2 is MIT, Opus is BSD-3 and miniaudio is public domain / MIT-0, so all are compatible.

About

Multiplayer and VHF radio for X-Plane 12 and 11. See friends as CSL models, talk push-to-talk on the COM frequencies with the sound of a real radio, fly one shared sky, and host a flight from inside the sim with no server to set up. Windows, macOS, Linux.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages