About Link to heading

The whole reason I’m messing about with a little RTL-SDR box is because of a little project between me and a friend; to make a little CWIS for our desks. Usually, they’re found on navy ships to defend against incoming aircraft; but they’ve got a cute little novelty, when they’re not trying to kill you:

CWIS

There’s multiple ways of actually getting data of aircraft, sites such as FlightRadar24, FlightAware and PlaneFinder let you see flight information in realtime. However, most of these don’t offer an API, and even less (if any) offer a free API; and depending on how long I want to run this little project and little desk toy, the cost will pile up.

Sourcing our Flight Data Link to heading

Sites like the ones mentioned above, as well as air traffic control towers use a technology called ADS-B to receive the position and velocity of nearby aircraft in real-time. The important part is that ADS-B mostly operates at a radio frequency of 1090MHz.

Instead of using an online API that requires an internet connection and potentially a monthly cost; I can use the ADS-B signal and an antenna to get the data ourselves from nearby aircraft’s broadcast.

For this project, I’ve used an RTL-SDR software defined radio dongle and a cheap antenna off Amazon to listen to aircraft’s ADS-B broadcasts. More information about the RTL-SDR dongle can be found on their site. It’s a nifty little dongle, and even a cheap antenna can give some interesting results.

RTL-SDR Link to heading

The dongle itself plugs in via USB, making it easy to implement on both regular PC and something small like a Raspberry Pi (spoiler). The other end of the RTL-SDR has a SMA female connector, which is where you plug your cheap antenna into before you stick it out the window.

To make sure that it even works in the first place, I plugged the RTL-SDR into my computer and downloaded one of the support RTL-SDR software, in this case, it was SDR++. I moved to the the frequency of BBC Radio 1 at around 97-99FM, for me it’s at 98.1FM exactly and clicking on the active frequency resulted in hearing music.

SDR++ UI

Since I didn’t buy this dongle to only listen to FM Radio, I continued with attempting to find the ADS-B data. I tuned SDR++ to 1.090.000.000 or 1090MHz/1.09GHz, turning the Gain up to full will show a nice solid line, this is the ADS-B activity. Tuning directly into this frequency and clicking play gives nothing but static (what did you expect?); but it at least shows that the RTL-SDR can and does receive flight data.

SDR++ with 1090MHz

Reading the Signal Link to heading

The signal that’s returned from SDR++ is pretty useless since it’s impossible for humans to understand static. Instead there are different software available for parsing 1090MHz data directly from the raw signal.

At first I used RTL1090 by JetVision to parse the signal coming in, since I didn’t know what to expect. It’s pretty useful, but didn’t really work as intended the first time. The RTL1090 doesn’t really show much, and even after installing Virtual Radar, I had no luck.

To get a meaningful representation of the signal, I tried using a different software; I looked towards Dump1090 by antirez which is just a simple command line interface which boasts an interactive mode as well being able to output the parsed data to a TCP socket.

The version by antirez seemed to be the original, but outdated version, and there is a chain of forks leading to the most updated version by none other than “FlightAware” (one of the sites from above).

Unfortunately, this version doesn’t have any pre-built releases on their GitHub page, and so I had to build it myself from source (in 2023/4…). Thankfully I have WSL installed on Windows so I just booted an Ubuntu terminal and built it using the commands they provide.

Which…. didn’t work. Since the RTL-SDR dongle didn’t passthrough properly so the program on Ubuntu didn’t detect that it was corrected.

The Spoiler Link to heading

It’s pretty finnicky to have to plug the RTL-SDR into a PC and do all the processing on there, and then somehow give the info back to actually turn the CWIS. So as hinted before, the software for the CWIS project will be running on a Raspberry Pi. A Raspberry Pi Zero W to be exact, since it’s set up for wireless communication.

This also gives the benefit of being able to actually compile and run the previously mentioned dump1090 program. I had to recompile it, though; as the single core 1GHz processor on the Raspberry Pi Zero W is a different architecture compare to my Intel processor on my PC (ARM vs AMD64). It’s a slower process in comparison but it means the program will actually run on the Pi.

Raspberry Pi Struggles Link to heading

The Raspberry Pi Zero W came without a micro-SD card, and as such, without an installed operating system. Meaning I had to flash a microSD card with the latest Raspbian version. This took a few tries partly due to using incorrect charging cables and a few other misdiagnoses when it took longer to boot or when it didn’t boot at all.

Once the OS was set up the next and I could access the Pi via SSH. The trouble was connecting the RTL-SDR dongle, as the Raspberry Pi Zero W comes with two micro-USB slots on board; one for power and one for USB communication. However, as previously noted, the RTL-SDR dongle only contains USB-A connection. An adapter was a few quid from Amazon which sorted it easily, and the loose cable form factor of it meant that I could fold the RTL-SDR back onto the Pi to save some space (when it comes to making the shell).

Running the ./dump1090 with the --interactive argument prints off the nearby airplanes in a nice GUI:

dump1090 GUI

Including the latitude, longitude, altitude and speed. The latitude and longitude is what’s interesting; as that’s the “coordinates” of the aircraft. The altitude and speed could be useful for interpolation later on.

Parsing the Data (The Code Part) Link to heading

It’s no use having the data just in a GUI, as I want to parse this via software to turn a model CWIS. Luckily, similar to rtl1090, it’s possible to expose a TCP socket from dump1090 to whatever software I want to parse the data in.

For ease of use as well as for speed of development, the software to parse this data from dump1090 is written in Python. Using the asyncio library I can open a socket to the port exposed by dump1090 and decode the data coming from it like so:

import asyncio

class DumpSocket:
    
    def __init__(self, host, port):
        self._host: str = host
        self._port: int = port
        self._reader: asyncio.StreamReader = None
        self._writer: asyncio.StreamWriter = None

    async def start_connection(self) -> None:
        self._reader, self._writer = await asyncio.open_connection(self._host,self._port)
    
    async def get_data(self) -> str:
        data: bytes = await self._reader.readline()
        data_decoded: str = data.decode()
        data_decoded.removesuffix("\r\n")
        return data_decoded
    
    def connected(self) -> bool:
        return not self._writer.is_closing()
        
    async def stop_connection(self) -> None:
        await self._writer.wait_closed()

The usage of the class above is like so:

async def create_socket_poll():
    print("Creating TCP RTL-SDR socket")
    socket = DumpSocket(HOST,PORT)
    await socket.start_connection()
    while socket.connected() is not False:
        data = await socket.get_data()

Where the returned data is simply a string. It’s not really mentioned anywhere what format the string is, but, according to the documentation of dump1090, when using the --net argument, it exposes multiple ports with different formats for the data from the TCP socket.

The format that’s the simplest to parse is the BaseStation output format. This prints a CSV string to the console for every message, like so:

MSG,5,1,1,400A7C,1,2024/01/02,21:14:08.151,2024/01/02,21:14:08.203,,19900,,,,,,,0,,0,
MSG,6,1,1,400A7C,1,2024/01/02,21:14:08.182,2024/01/02,21:14:08.206,,,,,,,,5144,0,0,0,
MSG,4,1,1,400A7C,1,2024/01/02,21:14:08.378,2024/01/02,21:14:08.423,,,362,4,,,-1792,,,,,0
MSG,8,1,1,400A7C,1,2024/01/02,21:14:08.613,2024/01/02,21:14:08.645,,,,,,,,,,,,0
MSG,8,1,1,400A7C,1,2024/01/02,21:14:08.629,2024/01/02,21:14:08.648,,,,,,,,,,,,0
MSG,8,1,1,400A7C,1,2024/01/02,21:14:08.631,2024/01/02,21:14:08.656,,,,,,,,,,,,0
MSG,3,1,1,400A7C,1,2024/01/02,21:14:08.724,2024/01/02,21:14:08.755,,19875,,,54.27269,-1.80066,,,0,,0,0
MSG,5,1,1,400A7C,1,2024/01/02,21:14:08.773,2024/01/02,21:14:08.805,SHT12T  ,19875,,,,,,,0,,0,

The meanings of these individual fields aren’t mentioned in the documentation, but there’s a documentation thread on the FlightAware forums about someone’s exploration into the BaseStation TCP port format. This gives a nice table with the offsets and columns for the CSV row string that’s received from the socket:

Ignoring data that might not be useful, we get the following parse method:

def __init__(self, csv_data: str):
        # https://discussions.flightaware.com/t/exploring-port-30003-and-30106/27752
        csv_data = csv_data.strip().splitlines()
        parsed_data = reader(csv_data)
        parsed_data = next(parsed_data, None)
        if not parsed_data:
            raise ValueError 
        try:
            self._id = parsed_data[4]
            self._callsign = parsed_data[10].strip()
            self._altitude = parsed_data[11]
            self._longitude = parsed_data[15]
            self._latitude = parsed_data[14]
            self._heading = parsed_data[13]
            self._speed = parsed_data[12]
            self._squawk = parsed_data[17]
        except IndexError:
            raise ValueError

If we get a ValueError raised, it just means that the data we have is invalid and that we shouldn’t take any notice of it. Anything other error and we should log it and probably look at it later or something, idk.

This results in a set of aircraft details that we can cache for later. However, as you can probably tell from some some of the data above, sometimes the messages are incomplete.

Updating Existing Data Link to heading

The final thing we need to do to when parsing the data is to make sure our cache keeps track of what details we have, and updates the entries when new data comes in. I defined a static method that just takes some old aircraft data and updates it with the data from a new message:

@staticmethod
def update(old, new):
    if new._callsign:
        old._callsign = new._callsign
    if new._altitude:
        old._altitude = new._altitude
    if new._longitude:
        old._longitude = new._longitude
    if new._latitude:
        old._latitude = new._latitude
    if new._heading:
        old._heading = new._heading
    if new._heading:
        old._heading = new._heading
    if new._squawk:
        old._squawk = new._squawk
    if new._speed:
        old._speed = new._speed
    return old

It’s not clean, but it does the job.

Clean-Up Link to heading

This, after some time, results in a cache that has up-to-date details on nearby aircraft. One last neat little feature that dump1090 has in comparison to our rudimentary little tracker, is that after 60 seconds of no messages, an aircraft is removed from the tracker. This prevents stale data from planes that have flew overhead and are long gone from staying in the cache.

Since we’re using asyncio for managing the socket polling, we can just put another task on the thread loop (or whatever the Python terms are for it) where we just check when we last updated an entry, and if it’s longer than 60 seconds we remove it:

async def clean_old():
    while True:
        to_remove: list[str] = []
        for entry in last_update:
            update_time = last_update[entry]
            time_now = time()
            if (time_now - update_time) > 60:
                to_remove.append(entry)
        for removal in to_remove:
            print("Cleaning up "+removal)
            del tracked_aircrafts[removal]
            del last_update[removal]
        await asyncio.sleep(1)

Finally, we have our own flight tracker that we can manipulate the data from, where our entries expire after a minute of no contact. The usage of this data will be used to face the CWIS towards the closest plane, in a fun, non-violent way.