# Build the station

A shopping list and the build steps for a station like mine. It receives aircraft, ships, the text messages that aircraft send, and most of shortwave. Every program in the main steps is free and public.

Spurious Emissions · Rev B · checked 2026-10-21 · https://spurious-emissions.net/guides/build-the-station/

Some links are affiliate links: if you buy through them, I may earn a commission at no extra cost to you. As an Amazon Associate I earn from qualifying purchases.

## 1. What this guide builds

Two computers and five radios. Each radio sits where its antenna cable enters the house. Each computer decodes the signal there and sends only data over the home network. Antenna cable loses signal with every meter. Network cable loses nothing.

| Signal | What you get | Frequency | Antenna | Radio | Decoder | Computer |
|---|---|---|---|---|---|---|
| ADS-B | Aircraft positions | 1090 MHz | 1090 MHz vertical | FlightAware Pro Stick Plus | readsb (in ultrafeeder) | `vhf` |
| AIS | Ship positions | 161.975 and 162.025 MHz | AIS-band vertical | Airspy Mini | AIS-catcher (in shipfeeder) | `vhf` |
| ACARS | Aircraft text messages | 129 to 132 MHz | Discone, splitter leg A | Airspy R2 | acarsdec | `vhf` |
| VDL Mode 2 | Aircraft text messages, newer system | 136.650 to 136.975 MHz | Discone, splitter leg B | RTL-SDR Blog V4L | dumpvdl2 | `vhf` |
| HFDL | Aircraft messages over oceans | 12 bands, 2.9 to 21.9 MHz | Active loop | RX-888 Mk II | dumphfdl | `hf` |
| FT8 and FT4 | Amateur stations, worldwide | 1.8 to 28 MHz | Active loop | RX-888 Mk II | decode_ft8 | `hf` |
| WSPR | Low-power beacons, worldwide | 0.136 to 28 MHz | Active loop | RX-888 Mk II | wsprd | `hf` |

The two computers have these names in this guide:

- **`vhf`**: a Raspberry Pi 5. It runs four radios in Docker containers.
- **`hf`**: an x86 desktop PC. It runs the RX-888 Mk II with ka9q-radio, as system services.

My own station uses four computers, for reasons of history.[^layout] Section 15 lists the signals I receive with my own programs, which are not public, and a free program for each job.

```
 roof                              cable entry                      home network
 ----                              -----------                      ------------
 1090 MHz vertical ---------------- Pro Stick Plus --+
 AIS vertical ------ FM filter ---- Airspy Mini -----+-- USB hub -- vhf (Pi 5, Docker)
 discone -- FM filter -- splitter + Airspy R2 -------+                 |  maps, message log,
                                  + RTL-SDR V4L -----+                 |  feeds to networks
                                                                       |
 loop -- injector -- 30 MHz filter -- limiter -- RX-888 Mk II -- hf (x86 PC)
                                                  (own USB 3 socket)   HFDL by UDP to vhf
```

### Values to choose first

Write these down. The steps use them as placeholders.

| Placeholder | Meaning | Example form |
|---|---|---|
| `<LAT>` `<LON>` | Antenna position, decimal degrees | `xx.xxxxx` |
| `<ALT_M>` | Antenna height above sea level, meters | `45` |
| `<TZ>` | Time zone, TZ database name | `America/Los_Angeles` |
| `<NAME>` | A station name with no personal detail | `my-station` |
| `<UUID>` | One random ID for the aircraft networks | from `cat /proc/sys/kernel/random/uuid` |
| `<SER_ADSB>` `<SER_VDL2>` | Serial numbers you write to the two RTL radios | `1090`, `136` |
| `<SER_MINI>` `<SER_R2>` | Factory serial numbers of the two Airspy radios | 16 hexadecimal characters |

> **Your position becomes public.** Most tracking networks show their feeder stations on a map. Multilateration needs your exact position, so you cannot give a false one. Choose a station name that does not name you or your street. Sections 7 and 9 show the settings that hide the station mark on your own maps.

## 2. Bill of materials

Prices are US dollars, as listed by the named seller on 2026-10-07. They change. "See listing" means the price is on the linked page. A part marked ◆ is what I use; "an example" or "a similar one" is a link to a part like mine, not the one I have. A blank box is for your pencil.

### Antennas

| ☐ | Part | Job | Connector | Price | Note |
|---|---|---|---|---|---|
| ☐ | ◆ [FlightAware 1090 MHz antenna](https://flightaware.store/products/antenna-1090mhz), 66 cm, 5.5 dBi | Aircraft | N type | $49.99, FlightAware store | Mast bracket included |
| ☐ | ◆ [AIS-band vertical](https://www.amazon.com/dp/B09374SM2V?tag=spuriousemiss-20), tuned near 162 MHz | Ships | See listing | See listing | A low-cost one is enough to start[^ais-ant] |
| ☐ | ◆ [Diamond D-130J discone](https://www.amazon.com/dp/B00AR09A6I?tag=spuriousemiss-20), 25 to 1300 MHz receive, 1.7 m | ACARS and VDL2 | SO-239 (the D-130NJ has N type) | See listing, Amazon | One wide antenna, two radios |
| ☐ | ◆ [MLA-30+ active loop](https://www.amazon.com/dp/B095K89WND?tag=spuriousemiss-20) with its power injector | Shortwave | SMA | See listing, Amazon | The amplifier is in the loop head[^loop] |

### Filters and protection

| ☐ | Part | Job | Price | Note |
|---|---|---|---|---|
| ☐ | ◆ [RTL-SDR Blog FM band-stop filter](https://www.amazon.com/dp/B01LE9LRPM?tag=spuriousemiss-20), 88 to 108 MHz, over 50 dB | First part after the discone | See listing, Amazon | Loss under 0.5 dB below 1 GHz |
| ☐ | ◆ [Nooelec Flamingo FM](https://www.amazon.com/dp/B079CNCR8V?tag=spuriousemiss-20), over 40 dB typical | On the AIS antenna | See listing, Amazon | Optional[^fm-ais] |
| ☐ | ◆ [Mini-Circuits ZX10-2-12-S+](https://www.minicircuits.com/WebStore/dashboard.html?model=ZX10-2-12-S%2B) 2-way splitter, 2 to 1200 MHz, SMA | Feeds two radios from the discone | $42.20, Mini-Circuits | Each leg gets a little less than half[^splitter] |
| ☐ | ◆ [Turn Island Systems F3060](https://turnislandsystems.com/product/f3060-filter-shelf-30-60-mhz-lpf/) low-pass filter | In front of the RX-888 | $80, Turn Island Systems | Required at 64.8 MS/s (section 4) |
| ☐ | ◆ [TAPR RF Limiter](https://tapr.org/product/rf-limiter/) | Last part before the RX-888 | $20, TAPR | Needed if you also transmit[^limiter] |

Do not buy an amplifier at the start. Section 17 says why.

### Radios

| ☐ | Part | Job | Price | Note |
|---|---|---|---|---|
| ☐ | ◆ [FlightAware Pro Stick Plus](https://flightaware.com/adsb/prostick/) | ADS-B | $45.99, FlightAware store | Amplifier (19 dB) and 1090 MHz filter are built in |
| ☐ | ◆ [Airspy Mini](https://airspy.com/airspy-mini/), 24 to 1700 MHz, 12-bit | AIS | $99, airspy.us (out of stock that day) | An RTL-SDR also works[^rtl-only] |
| ☐ | ◆ [Airspy R2](https://airspy.com/airspy-r2/), 24 to 1700 MHz, 12-bit | ACARS | $169, airspy.us | An RTL-SDR also works[^rtl-only] |
| ☐ | ◆ [RTL-SDR Blog V4L](https://www.rtl-sdr.com/buy-rtl-sdr-dvb-t-dongles/) | VDL2 | $37.95, rtl-sdr.com | Needs a 2026 driver (section 17). The older V4 is no longer made |
| ☐ | ◆ Stick-on aluminum heat sinks, 50 x 25 x 10 mm ([mine](https://www.amazon.com/dp/B08QM84KPZ?tag=spuriousemiss-20), a pack of two) | On the dongles. Optional | See seller | My own addition, not part of the radios as sold. They run warm |
| ☐ | ◆ [RX-888 Mk II](https://opensourcesdrlab.com/products/rx888-mkii-16bit-sdr-receiver-radio-ltc2208-adc-upgrade-rx888?DIST=QU5PHQ%3D%3D), 16-bit, 10 kHz to 64 MHz direct | All of shortwave at once | $152.79, OpenSourceSDRLab, 2026-10-11 | Copies exist. Mine is from OpenSourceSDRLab. Without it, see the note[^no-rx888] |

### Computers and USB

| ☐ | Part | Job | Price | Note |
|---|---|---|---|---|
| ☐ | ◆ [Raspberry Pi 5](https://www.amazon.com/dp/B0CK2FCG1K?tag=spuriousemiss-20), 8 GB | `vhf` | $175 list (4 GB: $110) | Requirements in section 3 |
| ☐ | ◆ [Raspberry Pi 27 W USB-C power supply](https://www.amazon.com/dp/B0D3MFLNC1?tag=spuriousemiss-20) | Power for `vhf` | See seller | With a weaker supply the Pi limits its USB sockets to 600 mA in total |
| ☐ | microSD card, 32 GB or more ([an example](https://www.amazon.com/dp/B07FCMBLV6?tag=spuriousemiss-20)), or an NVMe drive with a Pi 5 adapter | Storage for `vhf` | See seller | |
| ☐ | ◆ Powered USB hub with its own power supply (mine: [Rosonway RSH-A10](https://www.amazon.com/dp/B081DYD25S?tag=spuriousemiss-20)) | The four `vhf` radios | See seller | A Pi alone can power about three radios |
| ☐ | ◆ x86-64 desktop PC with a USB 3.0 socket on the main board (mine: HP EliteDesk 800 G1 SFF, Core i5-4570, 12 GB, bought used; [a similar one](https://www.amazon.com/dp/B094LJZ48K?tag=spuriousemiss-20)) | `hf` | Used market | Requirements in section 3 |
| ☐ | ◆ SATA solid-state drive, 120 GB or more (mine: [Crucial MX500 500 GB](https://www.amazon.com/dp/B0786QNS9B?tag=spuriousemiss-20), bought used) | Storage for `hf` | See seller | The FT8 recorder writes files all day |
| ☐ | ◆ Battery backup (UPS), 1500 VA (mine: [CyberPower CP1500PFCLCD](https://www.amazon.com/dp/B00429N19W?tag=spuriousemiss-20)) | Keeps almost the whole station running through a short power cut | See seller | Optional. A long cut still stops the station: section 11, step 2 covers that |

### Cable and small parts

| ☐ | Part | Job |
|---|---|---|
| ☐ | Low-loss 50 ohm coax for 1090 MHz, with an N plug at the antenna end and an SMA plug at the radio end ([an example](https://www.amazon.com/dp/B0CH6L5WPL?tag=spuriousemiss-20)) | Aircraft antenna. Keep the loss under 6 dB and the length under 15 m (FlightAware's advice) |
| ☐ | 50 ohm coax with a PL-259 plug ([an example](https://www.amazon.com/dp/B08F762YN1?tag=spuriousemiss-20)), and an SMA plug or adapter at the radio end ([an example](https://www.amazon.com/dp/B00CVQOOAI?tag=spuriousemiss-20)) | Discone |
| ☐ | Coax to suit the AIS antenna, SMA at the radio end | AIS antenna |
| ☐ | ◆ RG-6 quad-shield coax ([an example](https://www.amazon.com/dp/B002HESF4K?tag=spuriousemiss-20)), and two F to SMA adapters ([an example](https://www.amazon.com/dp/B00CVTRTUC?tag=spuriousemiss-20)) | Loop to `hf`. On shortwave, 10 to 20 m of it loses under 1 dB |
| ☐ | Short SMA plug-to-plug jumpers, 5 or more ([an example](https://www.amazon.com/dp/B0BFPWJ3J9?tag=spuriousemiss-20)) | Filter to splitter to radios |
| ☐ | ◆ Snap-on ferrites ([an example](https://www.amazon.com/dp/B0CSXZWGD6?tag=spuriousemiss-20)) | Both ends of the loop cable, and the USB cables |
| ☐ | ◆ Grounding block for each cable where it enters the house ([an example](https://www.amazon.com/dp/B06XDX5PNN?tag=spuriousemiss-20)), and bonding wire | Section 4, "Ground the cables" |
| ☐ | 50 ohm SMA terminator ([an example](https://www.amazon.com/dp/B0H79NB6QV?tag=spuriousemiss-20)) | The noise test in section 11 |
| ☐ | Short USB extension cables ([an example](https://www.amazon.com/dp/B077MFLH7W?tag=spuriousemiss-20)) | So wide radios do not block the next hub socket |

## 3. Computer requirements

I can only state what I measured. The table says where a number is measured and where it is an estimate.

| Computer | I recommend | What I measured on my station (2026-10-07) |
|---|---|---|
| `vhf` | Raspberry Pi 5 with 8 GB, 64-bit Raspberry Pi OS, wired Ethernet, a powered USB hub. Any 64-bit ARM or x86 computer that runs Docker also works. | A Pi 5 with the AIS, ACARS and VDL2 containers used 1.6 GB of memory and a load average near 1 on 4 cores. The aircraft containers run on another computer here. There they used about 1.5 GB more. The screenshot container alone used 0.66 GB. So 4 GB is tight, and I did not test it. |
| `hf` | x86-64, 4 cores of Intel Haswell class (2013) or newer, 8 GB, a USB 3.0 socket on the main board, a solid-state drive, Ubuntu Server 24.04 LTS, wired Ethernet. | A Core i5-4570 (4 cores, 2013) ran the receiver at 64.8 MS/s with about 150 channels. The receiver program used 69 % of one core. The whole machine was 22 % busy with HFDL and 29 % busy with every decoder. It dropped no data in a 19-hour test. Memory in use was under 2.5 GB. |

Rules for both:

1. Use wired Ethernet. Multilateration needs steady timing.
2. Give each computer a fixed address on your network, set on the computer itself.
3. Keep the clocks synchronized. The ultrafeeder project recommends `chrony`.
4. Do not run the aircraft receiver in a virtual machine. Multilateration often fails there (ultrafeeder README).
5. Plug the RX-888 Mk II into a USB 3.0 socket on the PC itself. Never use a hub for it. At 64.8 MS/s it sends about 1 gigabit each second.

## 4. The radio-frequency chains

Build each chain in this order, antenna first.

```
A. Aircraft     1090 MHz vertical -- coax -- Pro Stick Plus
                (the radio has its own amplifier and filter: add nothing)

B. Ships        AIS vertical -- coax -- [Flamingo FM, optional] -- Airspy Mini

C. Messages     discone -- coax -- FM band-stop filter -- 2-way splitter
                                                            |-- leg A -- Airspy R2      (ACARS)
                                                            `-- leg B -- RTL-SDR V4L    (VDL2)

D. Shortwave    loop -- RG-6 -- power injector -- F3060 (30 MHz low-pass in,
                shelf filter bypassed) -- RF limiter -- RX-888 Mk II, HF input
```

The reasons for the order:

- **Filter before splitter.** One filter then cleans both legs.
- **No amplifier on a short, direct run.** Add one only to pay for a loss you have measured, and count messages before and after.
- **Bias tee off on every radio behind the splitter and on the RX-888.** A radio that puts DC on the cable can feed it into the other leg. The loop takes its power from its own injector.
- **Injector first on the loop cable.** Every part after it then sees no DC. The limiter blocks DC, so it must come after the injector.
- **Low-pass filter before the limiter.** A limiter makes harmonics when it clamps. TAPR's manual says to filter ahead of it.
- **Why the 30 MHz filter is not optional.** At 64.8 million samples each second, the receiver cannot tell a signal above 32.4 MHz from one below it. A signal from 32 to 65 MHz folds back onto shortwave. The F3060 removes it: 36 dB down at 32.5 MHz and 64 dB down at 51 MHz, by its manual. It costs 3.7 dB at 28 MHz.
- **Leave the F3060 shelf filter bypassed.** It cuts 28 dB below 1 MHz and 20 dB at 3.5 MHz. Turn it on only if strong AM broadcast stations overload the receiver at night.

### Put the antennas outside

This matters more than any part on the list. My ship antenna heard about 5 messages a minute indoors. The same radio and program heard about 30 a minute with a low-cost antenna on the roof. For the loop, height matters little. Distance from the electronics in the house matters a lot.

### Ground the cables

An outdoor antenna cable needs a path to ground where it enters the building. In the US the rule is National Electrical Code Article 810. Bond the grounding block to the building's own grounding electrode system. If you add a ground rod, bond it to that system with 6 AWG copper or heavier. Never use a rod that stands alone: a surge then goes through your equipment. I read a published summary of the rule, not the code text.[^nec] Use a licensed electrician if you are not sure.

## 5. Accounts

Create these as each phase asks for them. All are free. Most give a paid-level account in return for your data.

| Network | Phase | What identifies your station | Where to sign up |
|---|---|---|---|
| adsb.fi, airplanes.live, ADS-B Exchange | 7 | `<UUID>`, no account | Nothing to do |
| FlightAware | 7 | A feeder ID that the container prints | <https://flightaware.com/adsb/piaware/claim> |
| Flightradar24 | 7 | A sharing key from the sign-up command | Section 7 |
| Plane Finder, AirNav Radar, OpenSky Network, plane.watch | 7 | A share code, key, user name or API key | Each container's README (section 19) |
| aiscatcher.org | 9 | Nothing: sharing is one setting | Nothing to do |
| MarineTraffic, AISHub, VesselFinder, ShipXplorer | 9 | A host and number that the network sends you | Each network's "add a station" page |
| airframes.io | 10 | The station ID you choose | <https://app.airframes.io> |
| PSK Reporter, WSPRnet | 13, 14 | A callsign and a grid locator | Sections 13 and 14 |
| Discord | 8 | A channel webhook address | Channel, Edit Channel, Integrations, Webhooks |

Keep every key in a password manager. The compose file on `vhf` will hold them in plain text: do not publish that file.

## 6. Phase 1: prepare `vhf`

1. Write Raspberry Pi OS Lite (64-bit) to the card with Raspberry Pi Imager. In the Imager settings, set the host name to `vhf`, turn SSH on, and add your public key.
2. Start the Pi on wired Ethernet. Log in with SSH.
3. Update the system.

   ```bash
   sudo apt update && sudo apt full-upgrade -y && sudo reboot
   ```

4. Install Docker with the sdr-enthusiasts script. It also stops the kernel's TV drivers from taking the RTL radios.

   ```bash
   bash <(curl -s https://raw.githubusercontent.com/sdr-enthusiasts/docker-install/main/docker-install.sh)
   sudo reboot
   ```

5. Check Docker and the driver block.

   ```bash
   docker compose version
   cat /etc/modprobe.d/exclusions-rtl2832.conf      # expect 9 "blacklist" lines
   lsmod | grep -c dvb_usb_rtl28xxu                 # expect 0
   ```

6. Install the Airspy tools and the clock service.

   ```bash
   sudo apt install -y airspy chrony
   ```

7. Connect the powered hub. Plug in **one RTL radio at a time** and write its serial number. Do the Pro Stick Plus first, then the V4L with `136`.

   ```bash
   docker run --rm -it --device /dev/bus/usb --entrypoint rtl_eeprom \
     ghcr.io/sdr-enthusiasts/docker-adsb-ultrafeeder -s 1090
   ```

   Change only the serial number. Do not change the maker or product text on an RTL-SDR Blog radio: its driver looks for that text. Then unplug every radio and plug it in again. A restart of the computer does not load the new serial numbers.

8. Plug in the two Airspy radios, one at a time, and read each factory serial number.

   ```bash
   airspy_info | grep -i serial
   ```

   Label each radio. Both Airspy models report the same USB identity (`1d50:60a1`). Only the serial number tells them apart.

9. Make the project folder. Every container in this guide goes into **one** compose file, so the containers can find each other by name.

   ```bash
   sudo mkdir -p -m 777 /opt/station && cd /opt/station
   cat /proc/sys/kernel/random/uuid                 # this is <UUID>
   ```

10. Create `/opt/station/.env` with your values.

    ```ini
    FEEDER_TZ=<TZ>
    FEEDER_LAT=<LAT>
    FEEDER_LONG=<LON>
    FEEDER_ALT_M=<ALT_M>
    FEEDER_NAME=<NAME>
    FEEDER_UUID=<UUID>
    ADSB_SDR_SERIAL=1090
    VDL2_SDR_SERIAL=136
    AIS_SDR_SERIAL=<SER_MINI>
    ACARS_SDR_SERIAL=<SER_R2>
    ACARS_STATION_ID=<STATION_ID>
    ```

## 7. Phase 2: aircraft (ADS-B)

Program: [docker-adsb-ultrafeeder](https://github.com/sdr-enthusiasts/docker-adsb-ultrafeeder). One container owns the radio, decodes, draws the map and feeds the networks.

1. Create `/opt/station/docker-compose.yml`.

   ```yaml
   services:
     ultrafeeder:
       image: ghcr.io/sdr-enthusiasts/docker-adsb-ultrafeeder
       container_name: ultrafeeder
       hostname: ultrafeeder
       restart: unless-stopped
       device_cgroup_rules:
         - "c 189:* rwm"
       ports:
         - 8080:80
       environment:
         - TZ=${FEEDER_TZ}
         - READSB_DEVICE_TYPE=rtlsdr
         - READSB_RTLSDR_DEVICE=${ADSB_SDR_SERIAL}
         - READSB_GAIN=auto
         - READSB_LAT=${FEEDER_LAT}
         - READSB_LON=${FEEDER_LONG}
         - READSB_ALT=${FEEDER_ALT_M}m
         - READSB_RX_LOCATION_ACCURACY=2
         - ULTRAFEEDER_CONFIG=
           adsb,feed.adsb.fi,30004,beast_reduce_plus_out;
           adsb,feed.airplanes.live,30004,beast_reduce_plus_out;
           adsb,feed1.adsbexchange.com,30004,beast_reduce_plus_out;
           mlat,feed.adsb.fi,31090;
           mlat,feed.airplanes.live,31090;
           mlat,feed.adsbexchange.com,31090
         - UUID=${FEEDER_UUID}
         - MLAT_USER=${FEEDER_NAME}
         - UPDATE_TAR1090=true
         - TAR1090_ENABLE_AC_DB=true
       volumes:
         - /opt/station/ultrafeeder/globe_history:/var/globe_history
         - /opt/station/ultrafeeder/graphs1090:/var/lib/collectd
         - /proc/diskstats:/proc/diskstats:ro
         - /dev/bus/usb:/dev/bus/usb
       tmpfs:
         - /run:exec,size=256M
   ```

   Each `ULTRAFEEDER_CONFIG` line ends with a semicolon, except the last. The README lists more networks in the same form.

2. Start it and check.

   ```bash
   cd /opt/station && docker compose up -d
   docker ps --filter name=ultrafeeder        # expect "(healthy)" after a few minutes
   curl -s http://localhost:8080/data/aircraft.json | head -c 300
   ```

   The map is at `http://vhf:8080/`. The performance graphs are at `http://vhf:8080/graphs1090/`.

3. To hide the station mark on your own map, set `READSB_RX_LOCATION_ACCURACY=1` (two decimal places) or `0`. The networks still receive the exact position.

### More networks, one container each

Each of these reads the decoded stream from `ultrafeeder` at `30005/tcp` and sends it to one network. Add each as a service in the same file. FlightAware and Flightradar24 show the pattern.

```yaml
  piaware:
    image: ghcr.io/sdr-enthusiasts/docker-piaware:latest
    container_name: piaware
    restart: unless-stopped
    environment:
      - TZ=${FEEDER_TZ}
      - RECEIVER_TYPE=relay
      - BEASTHOST=ultrafeeder
      - BEASTPORT=30005
      - FEEDER_ID=${PIAWARE_FEEDER_ID}
      - MLAT_RESULTS_BEASTHOST=ultrafeeder
      - MLAT_RESULTS_BEASTPORT=31004
    tmpfs:
      - /run:exec,size=64M
      - /var/log

  fr24:
    image: ghcr.io/sdr-enthusiasts/docker-flightradar24:latest
    container_name: fr24
    restart: unless-stopped
    dns_search: .
    environment:
      - BEASTHOST=ultrafeeder
      - FR24KEY=${FR24_KEY}
      - MLAT=no
```

| Network | Image | Required settings | How to get the key |
|---|---|---|---|
| FlightAware | `ghcr.io/sdr-enthusiasts/docker-piaware` | `RECEIVER_TYPE=relay`, `BEASTHOST`, `FEEDER_ID` | `timeout 30 docker run --rm ghcr.io/sdr-enthusiasts/docker-piaware:latest \| grep "my feeder ID"`, then claim it on the FlightAware page |
| Flightradar24 | `ghcr.io/sdr-enthusiasts/docker-flightradar24` | `BEASTHOST`, `FR24KEY` | `docker run -it --rm --entrypoint /usr/bin/fr24feed ghcr.io/sdr-enthusiasts/docker-flightradar24:latest --signup --configfile=/tmp/config.txt` |
| Plane Finder | `ghcr.io/sdr-enthusiasts/docker-planefinder:5.0.161_arm64` on a Pi 5 | `BEASTHOST`, `SHARECODE`, `LAT`, `LONG` | See its README |
| AirNav Radar | `ghcr.io/sdr-enthusiasts/docker-airnavradar` | `BEASTHOST`, `LAT`, `LONG`, `ALT` (meters, no unit), `SHARING_KEY` | The first run prints a key in the log. On a Pi 5, read section 17 first |
| OpenSky Network | `ghcr.io/sdr-enthusiasts/docker-opensky-network` | `BEASTHOST`, `OPENSKY_USERNAME`, `OPENSKY_SERIAL`, `LAT`, `LONG`, `ALT` | The first run creates the serial. Save it, or each start makes a new station |
| plane.watch | `ghcr.io/plane-watch/docker-plane-watch` | `BEASTHOST`, `API_KEY`, `LAT`, `LONG`, `ALT` | <https://atc.plane.watch> |

Three rules from the ultrafeeder README:

1. Never send multilateration results on to any network.
2. If you feed several networks, leave multilateration off for Flightradar24 (`MLAT=no`). They ask for this.
3. Set `FEEDER_ID` for FlightAware. Without it, each start creates a new site.

## 8. Phase 3: alerts for unusual aircraft

Program: [docker-planefence](https://github.com/sdr-enthusiasts/docker-planefence). It keeps a log of aircraft that pass low and close. Its plane-alert part compares every aircraft with a public watch list, [plane-alert-db](https://github.com/sdr-enthusiasts/plane-alert-db).

1. Add two services to the compose file.

   ```yaml
     planefence:
       image: ghcr.io/sdr-enthusiasts/docker-planefence
       container_name: planefence
       hostname: planefence
       restart: unless-stopped
       ports:
         - 8088:80
         - 9999:9999
       environment:
         - TZ=${FEEDER_TZ}
       volumes:
         - /opt/station/planefence/config:/usr/share/planefence/persist
         - /opt/station/planefence/html:/usr/share/planefence/html
       tmpfs:
         - /run:rw,nosuid,nodev,exec,relatime,size=512M
         - /tmp:rw,nosuid,nodev,noexec,relatime,size=128M

     screenshot:
       image: ghcr.io/kx1t/screenshot
       container_name: screenshot
       hostname: screenshot
       restart: unless-stopped
       shm_size: 1gb
       environment:
         - MAP_ARGS=zoom=7&hideSidebar&hideButtons&mapDim=0.2&monochromeMarkers=ff0000&outlineColor=505050&iconScale=1.5&enableLabels&extendedLabels=2&trackLabels&screenshot&rangerings=0
         - LOAD_SLEEP_TIME=25
         - BASE_URL=http://ultrafeeder
         - MAXTIME=60
         - CANVAS_SIZE=800,1200
   ```

   The host name of the second container must be `screenshot`. Planefence looks for that name.

2. Run `docker compose up -d`. Then copy `planefence.config.RENAME-and-EDIT-me` to `planefence.config` in `/opt/station/planefence/config/`, or use the settings page at `http://vhf:9999/`.
3. Set these lines in `planefence.config`.

   ```ini
   FEEDER_LAT=<LAT>
   FEEDER_LON=<LON>
   PF_SOCK30003HOST=ultrafeeder
   PF_MAXDIST=2.0
   PF_MAXALT=5000
   PF_ALERTLIST=https://raw.githubusercontent.com/sdr-enthusiasts/plane-alert-db/main/plane-alert-pia.csv,https://raw.githubusercontent.com/sdr-enthusiasts/plane-alert-db/main/plane-alert-db-images.csv
   PF_PA_SQUAWKS=7400,7500,7600,7700
   PA_DISCORD=ON
   PA_DISCORD_WEBHOOKS=<your webhook address>
   PF_DISCORD=OFF
   DISCORD_FEEDER_NAME=<NAME>
   DISCORD_MEDIA=screenshot+photo
   PF_SCREENSHOTURL=http://screenshot:5042
   ```

   This file spells the longitude `FEEDER_LON`. The `.env` file spells it `FEEDER_LONG`.

4. Apply and check.

   ```bash
   docker compose up -d planefence --force-recreate
   docker exec planefence curl -s -o /tmp/t.png http://screenshot:5042/snap/<hex>   # <hex> from aircraft.json
   ```

   The log page is at `http://vhf:8088/`. The alert page is at `http://vhf:8088/plane-alert`.

Notes from my station:

- Lists match first to last. I put the list of aircraft that hide their identity (`plane-alert-pia.csv`) first.
- I keep `PF_DISCORD=OFF`. With it on, every low aircraft near the house sends a message.
- `zoom=7` and `LOAD_SLEEP_TIME=25` are my values. At the default 15 seconds the map tiles had not loaded, and the picture was grey.
- `DISCORD_FEEDER_NAME` appears in every alert. Do not put your town in it if the channel is shared.

## 9. Phase 4: ships (AIS)

Program: [docker-shipfeeder](https://github.com/sdr-enthusiasts/docker-shipfeeder), which runs [AIS-catcher](https://github.com/jvde-github/AIS-catcher).

1. Add the service.

   ```yaml
     shipfeeder:
       image: ghcr.io/sdr-enthusiasts/docker-shipfeeder
       container_name: shipfeeder
       hostname: shipfeeder
       restart: unless-stopped
       environment:
         - SDR_TYPE=AIRSPY
         - RTLSDR_DEVICE_SERIAL=${AIS_SDR_SERIAL}
         - STATION_NAME=${FEEDER_NAME}
         - FEEDER_LAT=${FEEDER_LAT}
         - FEEDER_LONG=${FEEDER_LONG}
         - AISCATCHER_SHAREDATA=true
         # one line for each network, as each sends you its host and number:
         # - MARINETRAFFIC_UDP_PORT=<host>:<number>
         # - AISHUB_UDP_PORT=<host>:<number>
         # - VESSELFINDER_UDP_PORT=<host>:<number>
         # - SHIPXPLORER_UDP_PORT=<host>:<number>
       ports:
         - 90:80
       device_cgroup_rules:
         - "c 189:* rwm"
       tmpfs:
         - /tmp
       volumes:
         - /opt/station/shipfeeder/data:/data
         - /dev:/dev:rw
   ```

   The setting names say `RTLSDR`, but they apply to an Airspy too when `SDR_TYPE=AIRSPY`. Leave `SITESHOW` unset: when set, the map shows your station as a dot.

2. Start and check.

   ```bash
   docker compose up -d
   docker logs shipfeeder 2>&1 | grep "Searching for device"     # must name <SER_MINI>, not <SER_R2>
   ```

   The ship map is at `http://vhf:90/`. Near salt water, the first messages arrive within minutes.

3. Sign up with each network. Each one sends a host name and a number. Put them in the matching line, remove the `#`, and run `docker compose up -d` again.
4. For ShipXplorer on a Pi 5, choose "NMEA over UDP" when you add the station, and use `SHIPXPLORER_UDP_PORT`. Its sharing-key program does not run on the standard Pi 5 kernel (section 17). Never set both methods.

## 10. Phase 5: aircraft messages (ACARS and VDL Mode 2)

Four containers: [acarsdec](https://github.com/sdr-enthusiasts/docker-acarsdec) and [dumpvdl2](https://github.com/sdr-enthusiasts/docker-dumpvdl2) decode, [acars_router](https://github.com/sdr-enthusiasts/acars_router) removes duplicates and sends the messages on, and [ACARS Hub](https://github.com/sdr-enthusiasts/docker-acarshub) stores and shows them. The HFDL lines are ready for section 12.

1. Choose `<STATION_ID>`. The airframes.io form is `XX-YYYY`: your initials, then the code of the nearest large airport. This ID is public.
2. Add the services.

   ```yaml
     acarsdec:
       image: ghcr.io/sdr-enthusiasts/docker-acarsdec:latest
       container_name: acarsdec
       restart: unless-stopped
       environment:
         - TZ=${FEEDER_TZ}
         - AIRSPY_SERIAL=${ACARS_SDR_SERIAL}
         - FEED_ID=${ACARS_STATION_ID}-ACARS
         - FREQUENCIES=130.025;130.425;130.450;131.125;131.550
         - OUTPUT_SERVER=acars_router
         - OUTPUT_SERVER_PORT=5550
         - OUTPUT_SERVER_MODE=udp
       tmpfs:
         - /run:exec,size=64M
         - /var/log
       device_cgroup_rules:
         - "c 189:* rwm"
       volumes:
         - /dev/bus/usb:/dev/bus/usb:ro

     dumpvdl2:
       image: ghcr.io/sdr-enthusiasts/docker-dumpvdl2:latest
       container_name: dumpvdl2
       restart: unless-stopped
       environment:
         - TZ=${FEEDER_TZ}
         - RTL_SERIAL=${VDL2_SDR_SERIAL}
         - FEED_ID=${ACARS_STATION_ID}-VDL2
         - GAIN=40
         - FREQUENCIES=136.650;136.700;136.800;136.975
         - ZMQ_MODE=server
         - ZMQ_ENDPOINT=tcp://0.0.0.0:45555
       tmpfs:
         - /run:exec,size=64M
         - /var/log
       device_cgroup_rules:
         - "c 189:* rwm"
       volumes:
         - /dev/bus/usb:/dev/bus/usb:ro

     acars_router:
       image: ghcr.io/sdr-enthusiasts/acars_router:latest
       container_name: acars_router
       restart: unless-stopped
       ports:
         - 5556:5556/udp          # HFDL arrives here from the hf computer
       environment:
         - TZ=${FEEDER_TZ}
         - AR_OVERRIDE_STATION_NAME=${ACARS_STATION_ID}
         - AR_ENABLE_DEDUPE=true
         - AR_RECV_ZMQ_VDLM2=dumpvdl2:45555
         - AR_SKEW_WINDOW=30
         - AR_SEND_UDP_ACARS=feed.airframes.io:5550
         - AR_SEND_TCP_VDLM2=feed.airframes.io:5553
         - AR_SEND_TCP_HFDL=feed.airframes.io:5556
       tmpfs:
         - /run:exec,size=64M
         - /var/log:size=64M

     acarshub:
       image: ghcr.io/sdr-enthusiasts/docker-acarshub:latest
       container_name: acarshub
       restart: unless-stopped
       ports:
         - 8081:80
       volumes:
         - /opt/station/acars_data:/run/acars
       tmpfs:
         - /database:exec,size=64M
         - /run:exec,size=64M
         - /var/log:size=64M
       environment:
         - TZ=${FEEDER_TZ}
         - ENABLE_ACARS=true
         - ACARS_CONNECTIONS=zmq://acars_router:45550
         - ENABLE_VDLM=true
         - VDLM_CONNECTIONS=zmq://acars_router:45555
         - ENABLE_HFDL=true
         - HFDL_CONNECTIONS=zmq://acars_router:45556
         - ENABLE_ADSB=true
         - ADSB_URL=http://ultrafeeder/data/aircraft.json
         - ADSB_LAT=${FEEDER_LAT}
         - ADSB_LON=${FEEDER_LONG}
   ```

3. Start and check.

   ```bash
   docker compose up -d
   docker logs acarsdec 2>&1 | grep "Starting:"        # must show --airspy <SER_R2>
   docker logs dumpvdl2 2>&1 | grep -i "detected"     # a V4L must be named here
   ```

   The message log is at `http://vhf:8081/`. Near a large airport, expect the first VDL2 messages in minutes. ACARS is slower and comes in bursts.

4. Find your station on <https://app.airframes.io> by its ID.

Facts to know:

- **Frequencies.** These are the airframes.io lists for North America. An RTL radio covers about 2 MHz, so every channel of one decoder must fit in 2 MHz. The Airspy R2 covers more: I add 129.125 MHz to the ACARS list on mine.[^acars-freq] Always include 136.975 MHz, the worldwide calling channel for VDL2. Other regions use other lists.
- **`AR_SKEW_WINDOW=30`.** The default is 5 seconds. HFDL messages arrive later than that and would be dropped.
- **Gain on an Airspy.** I run acarsdec with the container's default gain and it works. The decoder's own README gives the Airspy a different gain scale (0 to 21). I did not compare the two.
- **After you unplug the hub,** run `docker restart acarsdec`. On my station the two Airspy radios come back in a race, and acarsdec can hold the wrong one. Then both feeds stop.

## 11. Phase 6: prepare `hf` and the receiver

Receiver program: [ka9q-radio](https://github.com/ka9q/ka9q-radio). Its `radiod` service reads the whole of shortwave from the RX-888 Mk II and gives each decoder its own channel over the computer's internal network. Nothing here uses Docker.

The project changes every week, and some of its documents are behind the code. These steps build the same version that my station runs (commit `b5bbb9aa`, 2026-09-25). The project now also offers ready-made packages for Ubuntu 24.04. I have not used them.

1. Install Ubuntu Server 24.04 LTS. Set the host name to `hf`. Turn SSH on. Use wired Ethernet.
2. In the PC's firmware settings, set it to start again after a power cut. A UPS rides through a short cut; this setting covers a long one.
3. Add the author's package source. Two of the libraries are only there.

   ```bash
   sudo wget -P /usr/share/keyrings http://www.ka9q.net/debian/ka9q-radio.asc
   echo "deb [signed-by=/usr/share/keyrings/ka9q-radio.asc arch=amd64] http://www.ka9q.net/debian noble main" \
     | sudo tee /etc/apt/sources.list.d/ka9q.list
   sudo apt update
   ```

4. Install the build tools and libraries (the list is from the project's `docs/INSTALL.md`).

   ```bash
   sudo apt install -y git avahi-utils build-essential make gcc libairspy-dev libairspyhf-dev \
     libbsd-dev libfftw3-dev libfftw3-bin libhackrf-dev libiniparser-dev libncurses-dev libopus-dev \
     librtlsdr-dev libusb-1.0-0-dev libusb-dev portaudio19-dev libasound2-dev rsync libogg-dev \
     libsamplerate0-dev libliquid-dev libncursesw5-dev libbladerf-dev libhydrasdr-dev libfobos-dev \
     libcsv-dev libsoapysdr-dev chrony
   ```

5. Build and install.

   ```bash
   cd ~ && git clone https://github.com/ka9q/ka9q-radio.git && cd ka9q-radio
   git checkout b5bbb9aa
   make -j && sudo make install
   sudo adduser $(whoami) radio
   sudo systemctl enable --now set_lo_multicast
   ```

   The install creates the user and group `radio`, the service files, and the rules that load the receiver's firmware when you plug it in. Log out and in again so the new group applies.

6. Connect the chain from section 4. Plug the RX-888 Mk II into a USB 3.0 socket on the PC. Check it.

   ```bash
   lsusb | grep 04b4          # expect 04b4:00f1 "RX888mk2". 00f3 means the firmware did not load
   lsusb -t | grep -B2 usbfs  # the receiver's line must end in 5000M
   ```

7. Write the receiver settings. Start from the project's example.

   ```bash
   sudo cp ~/ka9q-radio/config/examples/radiod@rx888-generic.conf /etc/radio/radiod@hf.conf
   sudoedit /etc/radio/radiod@hf.conf
   ```

   Set these lines in the first two sections. Leave the rest as it is for now.

   ```ini
   [global]
   hardware = rx888
   status = hf.local

   [rx888]
   device = "rx888"
   description = "<STATION_ID>-HFDL"
   samprate = 64m8
   gain = 10
   ```

   Always write `samprate = 64m8`. One of the project's default files uses the full rate, which makes more heat and needs a faster PC. The `description` text is sent to airframes.io as your HFDL station name.

8. Make the speed tables for this PC. This took 36 minutes on my Core i5-4570. `radiod` must not run during it.

   ```bash
   cd /etc/fftw && sudo touch nwisdom && sudo chown $(whoami):radio nwisdom wisdomf 2>/dev/null
   time fftwf-wisdom -v -T 1 -o nwisdom rof1620000 cob1920 cob1200 cob960 cob800 cob600 \
     cob480 cob320 cob300 cob200 cob160 cob150
   ls -l nwisdom && cp nwisdom wisdomf
   ```

   Do not leave out `-T 1`. Do not copy the file from another computer.

9. Start the receiver and look at it.

   ```bash
   sudo systemctl enable --now radiod@hf
   systemctl is-active radiod@hf        # expect "active"
   control hf.local                     # the live panel. Press q to leave
   ```

   The service name is `radiod@hf`, not `radiod@hf.conf`.

10. Set the gain. In the panel, read the level of the analog-to-digital converter. The project's rule is an average of -20 to -25 dBFS. I run 10 dB of gain, which gives about -32 dBFS here and leaves room for strong stations at night. A count of "overranges" that keeps rising means too much gain.
11. Test that the antenna, not the receiver, sets the noise. Note the noise level on a quiet frequency. Replace the antenna cable with the 50 ohm terminator. The noise must fall by 6 dB or more. On my station it fell by 30 to 40 dB. If it does not fall on some band, an amplifier may help on that band, and a filter will not.
12. Keep the receiver in its case, and keep air around it. It runs hot.

## 12. Phase 7: aircraft over the oceans (HFDL)

Decoder: [dumphfdl](https://github.com/szpajder/dumphfdl). The `hfdl` service of ka9q-radio starts one copy for each of 12 bands.

1. Build dumphfdl. Ubuntu 24.04 has no package for it.

   ```bash
   sudo apt install -y cmake pkg-config libglib2.0-dev libconfig++-dev libacars2-dev
   cd ~ && git clone https://github.com/szpajder/dumphfdl.git && cd dumphfdl
   git checkout v1.7.0
   mkdir build && cd build && cmake ../ && make && sudo make install
   dumphfdl --version                   # expect 1.7.0
   ```

2. Give `radiod` the 12 HFDL channels. In `/etc/radio/radiod@hf.conf`, add the line `disable = yes` to the section `[HFDL]`. Then add the project's HFDL file at the end.

   ```bash
   cat ~/ka9q-radio/config/examples/fragments/hfdl.conf | sudo tee -a /etc/radio/radiod@hf.conf >/dev/null
   sudo systemctl restart radiod@hf
   ```

3. Make the helper send each message to the router on `vhf`, not straight to airframes.io. Work on a copy, so a later install does not undo it.

   ```bash
   sudo cp /usr/local/sbin/start-hfdl /usr/local/sbin/start-hfdl-local
   sudo sed -i 's/decoded:json:tcp:address=feed.airframes.io,port=5556/decoded:json:udp:address=vhf.local,port=5556/' \
     /usr/local/sbin/start-hfdl-local
   grep -c "udp:address=vhf.local" /usr/local/sbin/start-hfdl-local     # expect 1
   ```

   If `hf` cannot find the name `vhf.local`, use the fixed address of `vhf`.

4. Point the service at the copy. Run `sudo systemctl edit hfdl` and enter these three lines.

   ```ini
   [Service]
   ExecStart=
   ExecStart=/usr/local/bin/pcmrecord --raw -v --exec "/usr/local/sbin/start-hfdl-local \"$k\" \"$d\" \"$r\"" ${MCAST}
   ```

5. Set the decoder's location and give it the ground-station table. In `/etc/radio/hfdl.conf`, change the `DUMPHFDL` line to `DUMPHFDL=/usr/local/bin/dumphfdl`.

   ```bash
   sudo install -D -o radio -g radio -m 664 ~/dumphfdl/etc/systable.conf /var/lib/hfdl/systable.conf
   sudo systemctl enable --now hfdl
   tail -f /var/log/hfdl.log
   ```

6. Check on `vhf`: the HFDL list of ACARS Hub fills. My first message came 20 minutes after the first start, with the loop indoors. Low bands carry traffic at night and high bands by day.

To skip the router, leave out steps 3 and 4. The messages then go to airframes.io only.

## 13. Phase 8: amateur stations (FT8 and FT4)

Decoder: `decode_ft8` from [ka9q/ft8_lib](https://github.com/ka9q/ft8_lib). Uploader: [ftlib-pskreporter](https://github.com/pjsg/ftlib-pskreporter). The method is in the project's `docs/ft8.md`.

1. Build the decoder.

   ```bash
   cd ~ && git clone https://github.com/ka9q/ft8_lib.git && cd ft8_lib
   make && sudo make install
   ls /usr/local/bin/decode_ft8
   ```

2. Give `radiod` the channels. In `/etc/radio/radiod@hf.conf`, add `disable = yes` to the section `[FT8]`. Then add the two files.

   ```bash
   cd ~/ka9q-radio/config/examples/fragments
   cat ft8.conf ft4.conf | sudo tee -a /etc/radio/radiod@hf.conf >/dev/null
   ```

3. In the lines you added, and in the section `[WSPR]`, delete every frequency above 30 MHz (50.310, 50.318, 50.293 and 144.489 MHz). The receiver cannot hear them at this rate. It would report false stations.
4. Start the recorders and decoders.

   ```bash
   sudo systemctl restart radiod@hf
   sudo systemctl enable --now ft8-record ft4-record ft8-decode@1 ft4-decode@1
   tail -f /var/log/ft8.log             # new lines every 15 seconds on an open band
   ```

5. Optional: report what you hear to [PSK Reporter](https://pskreporter.info).

   ```bash
   cd ~ && git clone https://github.com/pjsg/ftlib-pskreporter.git && cd ftlib-pskreporter
   sudo apt install -y python3-docopt
   sudo install -m 755 pskreporter-sender /usr/local/bin/
   sudo install -m 644 pskreporter.py /usr/local/lib/python3.12/dist-packages/
   sudo cp pskreporter@.service /etc/systemd/system/
   sudo cp ft8-pskreporter.conf ft4-pskreporter.conf /etc/radio/
   sudoedit /etc/radio/ft8-pskreporter.conf /etc/radio/ft4-pskreporter.conf     # CALLSIGN, LOCATOR, ANTENNA
   sudo systemctl daemon-reload && sudo systemctl enable --now pskreporter@ft8 pskreporter@ft4
   sudo ss -tnp | grep 4739             # an open connection means it uploads
   ```

Facts to know:

- **No licence is needed to report.** PSK Reporter says a listener can pick a name with the country prefix, such as `W/SWL/BOSTON`. I hold a licence and report with my callsign and a number after a dash. That keeps the receiver apart from my transmitting station.
- **The locator is public** and needs 6 characters. That places you within a few kilometers. The antenna text is public too.
- The reports carry no signal strength. The decoder's number is a score, not a measurement.
- The recorders write short sound files all day. That is why `hf` has a solid-state drive.

## 14. Phase 9: low-power beacons (WSPR)

Decoder: `wsprd`, from the WSJT-X package. ka9q-radio's `wspr-decoded` service records two minutes on each band and runs it.

1. Install the decoder.

   ```bash
   sudo apt install -y wsjtx
   ls /usr/bin/wsprd
   ```

2. Make the two names agree. Find the `data =` line in the section `[WSPR]` of `/etc/radio/radiod@hf.conf`. Put the same name after `MCAST=` in `/etc/radio/wspr-decode.conf`. On my station they did not match after the install, and nothing was decoded.
3. Start it.

   ```bash
   sudo systemctl enable --now wspr-decoded
   grep -v DecodeFinished /var/log/wspr.log | tail      # lines with a time, a level in dB, a frequency, a callsign
   ```

   Expect about 10 stations an hour at night on an indoor loop, and far more outdoors.

4. To report: ftlib-pskreporter has a `wspr-pskreporter.conf` that sends these to PSK Reporter in the same way as step 5 of section 13. I have not tested it. For reports to WSPRnet, the usual public program is [wsprdaemon](https://github.com/rrobinett/wsprdaemon). It installs and manages its own copy of ka9q-radio, so do not add it to this build without a plan. I upload with a small program of my own.

## 15. Other signals: what I receive with my own programs

On the same receiver I also decode the signals below. I wrote those programs for my own station and they are not public. Each one asks ka9q-radio for one more channel and reads the sound from it. This table names a free public program for each job, and says so where I found none that runs without a screen.

| Signal | Frequencies | Free public program | Limits |
|---|---|---|---|
| Weather fax, US Coast Guard station NMC | 4346, 8682, 12786, 17151.2, 22527 kHz. Tune 1.9 kHz lower, upper sideband | [fldigi](http://www.w1hkj.com) (WEFAX mode), or [Isobar](https://github.com/skgsara/isobar), which decodes a recording from the command line | fldigi needs a screen and a sound input. Isobar is a new project, and I have not tested it |
| NAVTEX warnings for ships | 518 kHz | [fventuri/navtex](https://github.com/fventuri/navtex), which reads a sound stream with no screen, or fldigi | Mostly a night signal here. Keep the F3060 shelf filter bypassed |
| SSTV pictures from amateurs | 14.230, 21.340, 28.680 MHz | [QSSTV](https://github.com/ON4QZ/QSSTV) | Needs a screen. [colaclanth/sstv](https://github.com/colaclanth/sstv) decodes one recording at a time, in fewer modes |
| DSC distress and calling | 2187.5, 4207.5, 6312, 8414.5, 12577, 16804.5 kHz | [SDRangel](https://github.com/f4exb/sdrangel), or YaDD on Windows | I found no free public decoder for Linux that runs without a screen |
| NCDXF/IARU beacons | 14.100, 18.110, 21.150, 24.930, 28.200 MHz | Faros, on Windows, $25 after a 30-day trial | ka9q-radio includes a channel file for these (`ibp.conf`), but no program that measures them |
| US Air Force voice messages (HFGCS) | 4724, 8992, 11175, 15016 kHz, upper sideband. Hobby lists add 6739 and 13200 | `pcmrecord` from ka9q-radio to record, then [whisper.cpp](https://github.com/ggml-org/whisper.cpp) or [faster-whisper](https://github.com/SYSTRAN/faster-whisper) to make text | I found no finished public program that does both |
| The sun's effect on very low frequencies | 24.8 kHz (NLK) and other navy transmitters | [SuperSID](https://github.com/sberl/supersid), with [NOAA's X-ray data](https://services.swpc.noaa.gov/json/goes/primary/) to compare | SuperSID reads a sound card. I did not confirm that it can read an SDR |
| One screen for the whole station | | [Homepage](https://gethomepage.dev) with [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) (read-only) shows which containers are healthy | Never open the proxy to the internet |

## 16. Final checks

Run these after the last phase. Each line is one feed.

| Feed | Check on | Command or page | Pass |
|---|---|---|---|
| Aircraft | `vhf` | `docker ps --filter name=ultrafeeder` | `(healthy)` |
| Aircraft | browser | `http://vhf:8080/` | Aircraft on the map |
| Alerts | Discord | The screenshot test in section 8 | A picture with a map |
| Ships | browser | `http://vhf:90/` | Ships on the map |
| Ships | `vhf` | `docker logs shipfeeder 2>&1 \| grep "Searching for device"` | Names `<SER_MINI>` |
| ACARS, VDL2 | browser | `http://vhf:8081/` | New messages in both lists |
| HFDL | browser | `http://vhf:8081/` | Messages in the HFDL list. Night and day use different bands |
| Receiver | `hf` | `systemctl is-active radiod@hf` | `active` |
| FT8, WSPR | `hf` | `tail /var/log/ft8.log /var/log/wspr.log` | New lines every 15 seconds and every 2 minutes |
| USB | both | `lsusb -t` | The RX-888 at `5000M`. No radio missing |

Then write down the message rate of every feed for one normal day. That number is your baseline. Change one thing at a time, and compare with it.

## 17. Known traps

Each of these cost me time.

| Symptom | Cause | What to do |
|---|---|---|
| `usb_claim_interface error -6`, "Device or resource busy" | The kernel's TV driver took an RTL radio | Check step 5 of section 6. Restart the computer |
| The ship feed stops when the ACARS decoder starts, or the reverse | Two Airspy radios with one USB identity, and a decoder that opened "the first one" | Give every container its radio's serial number. Never leave one unset |
| After the hub was unplugged: no ACARS, and the ship log repeats "cannot open device" | acarsdec holds the wrong Airspy | `docker restart acarsdec` |
| An RTL-SDR Blog V4L opens, the container is "healthy", and it decodes nothing | A driver older than August 2026 does not know its tuner | `docker compose pull && docker compose up -d`. The log must name the V4L |
| Fewer messages after you add an amplifier | The strong signals now overload the radio, and the weak ones are lost | Remove it. On my station a 20 dB amplifier cut ACARS from 95 to 42 messages an hour |
| AirNav Radar or the ShipXplorer key program stops at once on a Pi 5 | Those programs need 4 kB memory pages. `getconf PAGE_SIZE` shows 16384 | Add `kernel=kernel8.img` to `/boot/firmware/config.txt` and restart. For ShipXplorer, the UDP method avoids this |
| Plane Finder fails on a Pi 5 | Same kind of problem | Use the image tag `5.0.161_arm64` (sdr-enthusiasts guide) |
| A new FlightAware or OpenSky station appears at every start | The feeder ID or serial is not in the compose file | Save it in the file after the first run |
| The alert picture is a grey map | The map tiles had not loaded | Raise `LOAD_SLEEP_TIME` |
| Shortwave is full of signals that should not be there | No 30 MHz low-pass filter, so signals above 32.4 MHz fold back | Fit the filter (section 4) |
| `radiod` reports dropped data | The RX-888 is on a hub or a USB 2 socket | Use a USB 3.0 socket on the main board |
| The loop has no signal, or a radio behind the splitter runs hot | DC is wrong: the limiter blocks it, or a bias tee is on | Injector first on the loop cable. Bias tee off everywhere else |
| NAVTEX and the low bands are weak | The F3060 shelf filter is on | Bypass it |
| A decoder for 22.5 kHz hears nothing | In ka9q-radio, `22500` means 22.5 MHz | Write `22k5` |

## 18. Law and manners

- I am not a lawyer. This is what I read, for the United States. Other countries differ.
- US law (47 U.S.C. section 605) says that a person who receives a radio message not meant for the public must not tell others its contents, or use it for gain. It makes exceptions for broadcasts to the public, for amateur and CB radio, and for messages about ships, aircraft or people in distress.
- So I show that a message arrived, and what kind it was. I do not publish the text of real aircraft messages.
- Each network has terms for the data it gives back to you. Read them before you republish a map or a list.
- A feeder station is a small public service. Keep it running, keep its clock right, and keep its position honest.

## 19. Sources

Every repository and page that this guide depends on. All were read on 2026-10-07.

**Host preparation.** sdr-enthusiasts guide: <https://sdr-enthusiasts.gitbook.io/ads-b>. Install script: <https://github.com/sdr-enthusiasts/docker-install>.

**Aircraft.** <https://github.com/sdr-enthusiasts/docker-adsb-ultrafeeder> · <https://github.com/sdr-enthusiasts/docker-piaware> · <https://github.com/sdr-enthusiasts/docker-flightradar24> · <https://github.com/sdr-enthusiasts/docker-planefinder> · <https://github.com/sdr-enthusiasts/docker-airnavradar> · <https://github.com/sdr-enthusiasts/docker-opensky-network> · <https://github.com/plane-watch/docker-plane-watch>

**Alerts.** <https://github.com/sdr-enthusiasts/docker-planefence> · <https://github.com/sdr-enthusiasts/plane-alert-db> · <https://github.com/kx1t/browser-screenshot-service>

**Ships.** <https://github.com/sdr-enthusiasts/docker-shipfeeder> · <https://github.com/jvde-github/AIS-catcher>

**Aircraft messages.** <https://github.com/sdr-enthusiasts/docker-acarsdec> · <https://github.com/f00b4r0/acarsdec> · <https://github.com/sdr-enthusiasts/docker-dumpvdl2> · <https://github.com/szpajder/dumpvdl2> · <https://github.com/sdr-enthusiasts/acars_router> · <https://github.com/sdr-enthusiasts/docker-acarshub> · <https://docs.airframes.io>

**Shortwave.** <https://github.com/ka9q/ka9q-radio> (read `docs/INSTALL.md`, `docs/SDR/rx888.md`, `docs/FFTW3.md`, `docs/hfdl.md`, `docs/ft8.md`) · <https://github.com/szpajder/dumphfdl> · <https://github.com/ka9q/ft8_lib> · <https://github.com/pjsg/ftlib-pskreporter> · <https://github.com/rrobinett/wsprdaemon> · Heat in the RX-888: <https://ka7oei.blogspot.com/2023/08/improving-thermal-management-of-rx-888.html>

**Parts.** The maker or seller page linked from each row of section 2. Filter figures: the Turn Island F3060 manual and the TAPR RF Limiter manual.

**Rules.** 47 U.S.C. 605: <https://www.law.cornell.edu/uscode/text/47/605>. Weather fax schedule: <https://www.weather.gov/media/marine/rfax.pdf>. NAVTEX: <https://www.navcen.uscg.gov/navtex-maritime-safety-broadcasts>. DSC frequencies: <https://www.law.cornell.edu/cfr/text/47/80.359>. Beacons: <https://www.ncdxf.org/beacon/>.

### What I could not confirm

- That 4 GB is enough for `vhf`. I measured an 8 GB Pi 5.
- The connector of the low-cost AIS antenna, and its price on the day. Read the listing.
- That the Mini-Circuits splitter passes or blocks DC. Its data sheet does not say. So keep every bias tee off.
- The best gain setting for acarsdec on an Airspy R2.
- The Airspy prices at the maker. Its site did not answer that day, so the prices are the US distributor's.
- The price of the RX-888 Mk II. I found no seller page I could read. The maker warns of copies and names the sellers it trusts: read <https://www.rx-888.com> before you buy.
- That the newest ka9q-radio builds with these exact steps. I built the commit named in section 11.
- The ready-made ka9q-radio packages, the WSPR path to PSK Reporter, Isobar, and every program in section 15. I have not run them.
- The rule of WSPRnet for a listener with no callsign. Its site did not answer.
- No step in this guide was run on a new, empty computer as written. The settings are the ones on my station, checked against each project's documents.

[^layout]: My aircraft radio is on a Raspberry Pi 4 with FlightAware's own PiAware system. A small server (an Intel Atom E3950 with 16 GB) runs ultrafeeder, the other feeder containers and planefence, and reads the Pi 4 over the network. A Raspberry Pi 5 with 8 GB runs the ship and message containers. An HP EliteDesk 800 G1 runs the shortwave receiver. I built it in that order, one part at a time. One Pi 5 for all of the first three is simpler, and section 3 gives the numbers I have for it.
[^ais-ant]: I started with a small whip indoors, then put a low-cost AIS-band vertical on the roof. The rate went up about six times. A marine-grade antenna is a later step, if at all.
[^loop]: My loop is still indoors, near the computer, and that is the worst place for it. I own a second one as a spare. The amplifier in the loop head is the part most open to static.
[^fm-ais]: I added it and the message rate did not change. I left it in as cheap protection. On the wide discone, the FM filter is not optional.
[^splitter]: I bought mine from a surplus dealer. I have not measured what the split costs me in messages.
[^limiter]: I transmit on shortwave from the same property, at 5 to 20 watts. The limiter is there for that, and for static. If you never transmit, it is still cheap insurance for an expensive receiver.
[^rtl-only]: For ships, ACARS and VDL2, three RTL-SDR radios do the job in place of the two Airspy radios: set `RTL_SERIAL` in place of `AIRSPY_SERIAL` for acarsdec, and leave `SDR_TYPE` unset for shipfeeder. I ran VDL2 on a Nooelec NESDR SMArt v5 for six weeks. I use the Airspy radios because I owned them.
[^no-rx888]: I use and recommend the RX-888 Mk II because it hears all of shortwave at once, so every decoder runs all day. A narrow radio hears one slice. An Airspy HF+ Discovery (768 kHz wide) can run dumphfdl on one HFDL band at a time. The dumphfdl README also calls the RTL-SDR Blog V4 a decent low-budget choice; I have not tried its replacement, the V4L, on shortwave. That is a real station, at a fraction of the cost. It is not the build in sections 11 to 14.
[^acars-freq]: My six ACARS channels run from 129.125 to 131.550 MHz. That is wider than 2 MHz, and it works on the Airspy R2. My VDL2 list has eight channels from 136.650 to 136.975 MHz, which includes channels that airframes.io lists for Europe. I have not checked which of the eight carry traffic here.
[^nec]: Mike Holt's summary of NEC 810.21, 2020 edition: <https://www.mikeholt.com/files/PDF/20BG_810.21.pdf>.
