This Python script converts AIS NMEA lines received over UDP to BaseStation format and sends it to a specified server e.g. Virtual Radar Server (VRS). Main purpose is to plot data on SAR aircraft picked up by AIS receivers in ADS-B plotting software. The following is an example of a SAR helicopter broadcasting AIS messages plotted in VRS using AIS messages converted by ais2adsb (courtesy of jonboy1081):
And plotted using the ADS-B data:
This is a nice example where sometimes AIS has better reception than ADS-B. The idea for this tool came from flygfantast and jonboy1081.
usage: ais2adsb [-h] [--sar | --no-sar] [--ships | --no-ships]
[--callsign | --no-callsign] [--print-dict]
[--map-file MAP_FILE] [--save-file SAVE_FILE]
[--metrics-port METRICS_PORT] [--no-default-map] [--version]
udp_ip udp_port sbs_ip sbs_port
Minimal invocation:
python3 ./ais2adsb.py 192.168.1.235 4002 192.168.1.239 30003
reads AIS messages on 192.168.1.235:4002/udp and forwards converted SBS to VRS at 192.168.1.239:30003/tcp. Defaults: --sar on, --ships off, --callsign on.
If your receiver is AIS-catcher, send to ais2adsb via its -u option:
AIS-catcher -u 192.168.1.235 4002 .....
The --map-file option reads a Python-dict-literal mapping of MMSI → 24-bit ICAO from disk; --save-file writes the live map back periodically and on exit. A bundled default mapping ships in data/icao_map.dict and loads automatically (disable with --no-default-map). For unknown MMSIs ais2adsb auto-generates ICAO codes of the form 0xFxxxxx derived from the MMSI's lower 20 bits.
A full example, also exposing Prometheus metrics on port 8080:
python3 ./ais2adsb.py 192.168.1.235 4002 192.168.1.239 30003 \
--ships --map-file mapping.dict --print-dict --no-callsign --metrics-port 8080
The included docker-compose.yml configures the container via environment variables:
| Variable | Default | Notes |
|---|---|---|
SBS_TARGET_HOST |
required | TCP host for SBS output |
SBS_TARGET_PORT |
required | TCP port for SBS output |
UPD_IN_PORT |
9000 |
UDP port the container listens on for NMEA |
INCLUDE_SAR |
on | true/false — include SAR aircraft |
INCLUDE_SHIPS |
off | true/false — include vessels |
CALLSIGN |
on | true/false — emit generated callsigns |
METRICS_PORT |
disabled | exposes /metrics (Prometheus) and /health |
State (the live MMSI→ICAO map) persists to /data/ais2adsb.map.
When --metrics-port/METRICS_PORT is set, ais2adsb starts an HTTP server exposing two endpoints:
GET /metrics — Prometheus text format (version 0.0.4):
| Metric | Type | Description |
|---|---|---|
ais2adsb_messages_received_total |
counter | UDP datagrams received |
ais2adsb_messages_decoded_total |
counter | Messages decoded by aiscat (multipart reassembled) |
ais2adsb_messages_sent_total |
counter | SBS position records sent upstream |
ais2adsb_tcp_reconnects_total |
counter | Successful TCP connections to SBS server (incl. initial) |
ais2adsb_last_message_timestamp_seconds |
gauge | Unix timestamp of the most recent UDP receive |
ais2adsb_last_send_timestamp_seconds |
gauge | Unix timestamp of the most recent SBS send |
ais2adsb_unique_icaos |
gauge | Size of the active MMSI→ICAO map |
ais2adsb_connected |
gauge | 1 if currently connected to the SBS server, else 0 |
GET /health — 200 ok if connected to the SBS server and a UDP message was received in the last 300 s. 503 degraded otherwise. Use this for container healthchecks or load-balancer probes.
For Windows users who do not have Python installed there is a package available in the Releases section, built via pyinstaller.
For other platforms, the simplest path is Python 3.9+ with aiscat:
sudo apt install python3 pip
pip3 install aiscat
git clone https://github.com/jvde-github/ais2adsb.git
cd ais2adsb
Set up for example VRS so that it can receive BaseStation messages as a TCP server:

The NMEA input should be sent over UDP. Most AIS software including AIS-catcher can easily be set up to achieve this. Assuming a stream is arriving on 192.168.1.235:4002:
python3 ./ais2adsb.py 192.168.1.235 4002 192.168.1.239 30003
where 192.168.1.239 is the PC running VRS. This forwards SAR aircraft only by default. To include ship positions:
python3 ./ais2adsb.py 192.168.1.235 4002 192.168.1.239 30003 --ships
You will see in the VRS main window that the client has connected and hopefully some messages have been sent as well:

The final output in the webinterface of VRS will look something like:

A bundled default mapping (~280 entries, kindly provided by jonboy1081 and flygfantast) ships in data/icao_map.dict and is loaded automatically. Pass --no-default-map to skip it. Custom mappings via --map-file use the same format — a Python-dict literal of {mmsi: icao_int}:
{
111232512: 0x406C79,
111232511: 0x406C82,
250002898: 0x4CA98D,
# ...
}Unknown MMSIs are auto-assigned ICAO codes of the form 0xFxxxxx derived from the MMSI's lower 20 bits, with collision-avoidance against existing entries. Use --save-file PATH to persist the live (default + auto-assigned) map to disk; the same path can be reloaded next run via --map-file PATH.
python3 -m pip install aiscat
python3 -m unittest discover tests
CI runs the unit tests on every push/PR (.github/workflows/python-ci.yml). Docker images are built for linux/amd64, linux/arm64, and linux/arm/v7 (.github/workflows/docker-image.yml) and published to GHCR — armv7 covers Pi 1/2/3 in 32-bit mode and similar boards.
Recent substantive changes (vs. earlier 0.14-era versions):
- Decoder — switched from
pyaistoaiscat. Multipart AIVDM groups are now reassembled (previously raised and were dropped). AIS sentinel values for "speed not available" are correctly mapped toNoneinstead of being passed through as102.3knots. - Altitude bug fix (SAR) — AIS type 9 altitude is in metres; the SBS BaseStation field is feet. Earlier versions emitted the raw metric value, plotting helicopters at ~30 % of their true altitude in VRS. Now converted with the
4095 = N/Asentinel handled. - Position sanity — lat/lon are range-checked (
-90..90,-180..180) so AIS sentinels (lat=91,lon=181) can no longer bleed through to ADS-B viewers as ghost positions at the North Pole. - Graceful shutdown —
SIGTERM(used bydocker stop) now triggers the same dictionary-save path asSIGINT. - CLI — replaced the positional
SHIPS onstyle withargparse. New flags:--ships/--no-ships,--sar/--no-sar,--callsign/--no-callsign,--map-file,--save-file,--print-dict,--metrics-port,--no-default-map,--version. - ICAO mapping — extracted from the script body into
data/icao_map.dict. - Metrics / health — see Metrics and health above.
- Docker — base image swapped to
python:3.13-slim(~215 MB). Multi-arch build covers amd64 / arm64 / armv7. - Tests — unit tests under
tests/cover the alt conversion (incl. sentinel), ICAO generation, filter logic, CLI parsing, andsendBaseStationrejection of bogus positions.
If you run the script directly (Python / pyinstaller / launch scripts):
-
Replace
pyaiswithaiscat:pip3 uninstall pyais # optional, can coexist pip3 install aiscat -
Update the command line — the old positional
KEY valuestyle was removed in favour ofargparse. Translation table:Old New SHIPS on/SHIPS off--ships/--no-shipsSAR on/SAR off--sar/--no-sarCALLSIGN on/CALLSIGN off--callsign/--no-callsignPRINT on--print-dictFILE path.dict--map-file path.dictSAVE path.dict--save-file path.dictSo:
# Before python3 ais2adsb.py 192.168.1.235 4002 192.168.1.239 30003 SHIPS on FILE map.dict CALLSIGN off # After python3 ais2adsb.py 192.168.1.235 4002 192.168.1.239 30003 --ships --map-file map.dict --no-callsign -
The saved-dict file format is unchanged — your existing
*.dictfiles keep working with--map-file. -
SBS output bytes are identical to the previous version for any message both libraries handled. You should not need to reconfigure VRS.
-
One-off cleanup if you previously saw weird positions: if you noticed AIS targets plotted near the North Pole or SAR helicopters at impossibly low altitudes, those were the bugs fixed in this release — no action needed, just expect cleaner output going forward.
If you run the Docker image (ghcr.io/jvde-github/ais2adsb:edge):
The image tag is unchanged. The docker-compose.yml env-var names (SBS_TARGET_HOST, SBS_TARGET_PORT, INCLUDE_SHIPS, CALLSIGN, UPD_IN_PORT) are unchanged and continue to work — just docker pull the new image. Two optional additions: INCLUDE_SAR (defaults to on) and METRICS_PORT (off unless set). The /data volume layout is unchanged, so your persistent ais2adsb.map carries over.
If you build from the Dockerfile yourself: the base image swapped from ghcr.io/sdr-enthusiasts/docker-baseimage:python to python:3.13-slim. The rootfs/ directory was removed (s6 service files no longer needed); behaviour is now provided by docker-entrypoint.sh.

