Flightradar24 - how does it work? Part 2, ADS-B Protocol

Hello Habr. Probably everyone who at least once met or saw off relatives or friends on a plane used the free Flightradar24 service. This is a very convenient way to track the position of the aircraft in real time.

Flightradar24 - how does it work? Part 2, ADS-B Protocol

В the first part the principle of operation of such an online service was described. Now we will go further and find out what data is transmitted and received from the aircraft to the receiving station, and decode it ourselves using Python.

History

Clearly, aircraft data is not being shared for users to see on their smartphones. The system is called ADS-B (Automatic dependent surveillance—broadcast), and is used to automatically transmit information about the aircraft to the control center - its identifier, coordinates, direction, speed, altitude and other data are transmitted. Previously, before the advent of such systems, the dispatcher could only see a dot on the radar. This was not enough when there were too many planes.

Technically, ADS-B consists of an aircraft transmitter that periodically sends packets with information at a fairly high frequency of 1090 MHz (there are other modes, but they are not so interesting to us, because the coordinates are transmitted only here). Of course, in addition to the transmitter, there is also a receiver somewhere at the airport, but for us, as users, our own receiver is of interest.

By the way, for comparison, the first such system, Airnav Radarbox, designed for ordinary users, appeared in 2007 and cost about $ 900, and a subscription to network services cost about $ 250 per year.

Flightradar24 - how does it work? Part 2, ADS-B Protocol

Reviews of those first Russian owners can be read on the forum radioscanner. Now that RTL-SDR receivers have become massively available, a similar device can be assembled for $30, more about this was in the first part. We will move on to the protocol itself - let's see how it works.

Signal reception

First, the signal must be recorded. The entire signal has a duration of only 120 microseconds, so in order to comfortably disassemble its components, an SDR receiver with a sampling rate of at least 5 MHz is desirable.

Flightradar24 - how does it work? Part 2, ADS-B Protocol

After recording, we get a WAV file with a sampling rate of 5000000 samples / sec, 30 seconds of such a recording "weigh" about 500MB. Of course, it is useless to listen to it with a media player - the file does not contain sound, but a directly digitized radio signal - this is how Software Defined Radio works.

We will open and process the file using Python. Those who wish to experiment on their own can download an example recording here to register:.

Let's download the file and see what's inside.

from scipy.io import wavfile
import matplotlib.pyplot as plt
import numpy as np

fs, data = wavfile.read("adsb_20190311_191728Z_1090000kHz_RF.wav")
data = data.astype(float)
I, Q = data[:, 0], data[:, 1]
A = np.sqrt(I*I + Q*Q)

plt.plot(A)
plt.show()

Result: we see clear "impulses" against the background of noise.

Flightradar24 - how does it work? Part 2, ADS-B Protocol

Each "impulse" is a signal, the structure of which is clearly visible if you increase the resolution on the graph.

Flightradar24 - how does it work? Part 2, ADS-B Protocol

As you can see, the picture is quite consistent with what is given in the description above. You can start processing the data.

Decoding

First, you need to get a bitstream. The signal itself is encoded using manchester encoding:

Flightradar24 - how does it work? Part 2, ADS-B Protocol

It is easy to get real "0" and "1" from the level difference in nibbles.

    bits_str = ""
    for p in range(8):
        pos = start_data + bit_len*p
        p1, p2 = A[pos: pos + bit_len/2], A[pos + bit_len/2: pos + bit_len]
        avg1, avg2 = np.average(p1), np.average(p2)
        if avg1 < avg2:
            bits_str += "0"
        elif avg1 > avg2:
            bits_str += "1"

The structure of the signal itself is as follows:

Flightradar24 - how does it work? Part 2, ADS-B Protocol

Let's look at the fields in more detail.

DF (Downlink Format, 5 bits) — defines the message type. There are several types:

Flightradar24 - how does it work? Part 2, ADS-B Protocol
(table source)

We are only interested in the DF17 type, because it contains the coordinates of the aircraft.

ICAO (24 bits) — the international unique code of the aircraft. You can check the aircraft by its code https://www.izakayasushilounge.com (Unfortunately, the author stopped updating the database, but it is still relevant). For example, for the code 3c5ee2 we have the following information:

Flightradar24 - how does it work? Part 2, ADS-B Protocol

Edit: in comments on the article the description of the ICAO code is given in more detail, I recommend that those who are interested read it.

DATE (56 or 112 bits) - the actual data that we will decode. The first 5 bits of data are the field Type codeA containing the subtype of the stored data (not to be confused with DF). There are quite a few of these types:

Flightradar24 - how does it work? Part 2, ADS-B Protocol
(table source)

Let's look at some examples of packages.

aircraft identification

Binary example:

00100 011 000101 010111 000111 110111 110001 111000

Data fields:

+------+------+------+------+------+------+------+------+------+------+
| TC,5 | EC,3 | C1,6 | C2,6 | C3,6 | C4,6 | C5,6 | C6,6 | C7,6 | C8,6 |
+------+------+------+------+------+------+------+------+------+------+

TC = 00100b = 4, each character C1-C8 contains codes corresponding to indices in the string:
#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######

Having decoded the string, it is easy to get the aircraft code: EWG7184

symbols = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
code_str = ""
for p in range(8):
     c = int(bits_str[8 + 6*p:8 + 6*(p + 1)], 2)
     code_str += symbols[c]
print("Aircraft Identification:", code_str.replace('#', ''))

airborne position

If everything is simple with the name, then with the coordinates it is more complicated. They are transmitted as 2x, odd and even frames. Field code TC = 01011b = 11.

Flightradar24 - how does it work? Part 2, ADS-B Protocol

An example of even and odd packets:

01011 000 000101110110 00 10111000111001000 10000110101111001
01011 000 000110010000 01 10010011110000110 10000011110001000

The calculation of the coordinates itself takes place according to a rather tricky formula:

Flightradar24 - how does it work? Part 2, ADS-B Protocol
(source)

I'm not a GIS specialist, so I don't know where it comes from. Who knows, write in the comments.

Altitude is considered simpler - depending on the specific bit, it can be represented either as a multiple of 25 or 100 feet.

Airborne Velocity

Packet with TC=19. The interesting thing here is that the speed can be both accurate, relative to the ground (Ground Speed), and air, measured by the aircraft sensor (Airspeed). A lot of different fields are also passed:

Flightradar24 - how does it work? Part 2, ADS-B Protocol
(source)

Conclusion

As you can see, ADS-B technology has become an interesting symbiosis, when a standard is useful not only for professionals, but also for ordinary users. But of course, the key role in this was played by the cheaper technology of digital SDR receivers, which allows the device to literally “for a penny” receive signals with a frequency higher than a gigahertz.

In the standard itself, of course, much more. Those interested can view the PDF on the page ICAO or visit already mentioned above broker.

It is unlikely that all of the above will be useful to many, but at least the general idea of ​​\uXNUMXb\uXNUMXbhow it works, I hope, remains.

By the way, a ready-made Python decoder already exists, you can study it here. And owners of SDR receivers can assemble and run a ready-made ADS-B decoder from the page, this is discussed in more detail in the first part.

The source code of the parser described in the article is given under the cut. This is a test example that does not pretend to be production, but something works in it, and they can parse the file recorded above.
Source Code (Python)

from __future__ import print_function
from scipy.io import wavfile
from scipy import signal
import matplotlib.pyplot as plt
import numpy as np
import math
import sys
def parse_message(data, start, bit_len):
max_len = bit_len*128
A = data[start:start + max_len]
A = signal.resample(A, 10*max_len)
bits = np.zeros(10*max_len)
bit_len *= 10
start_data = bit_len*8
# Parse first 8 bits
bits_str = ""
for p in range(8):
pos = start_data + bit_len*p
p1, p2 = A[pos: pos + bit_len/2], A[pos + bit_len/2: pos + bit_len]
avg1, avg2 = np.average(p1), np.average(p2)
if avg1 < avg2:
bits_str += "0"
elif avg1 > avg2:
bits_str += "1"
df = int(bits_str[0:5], 2)
# Aircraft address (db - https://junzis.com/adb/?q=3b1c5c )
bits_str = ""
for p in range(8, 32):
pos = start_data + bit_len * p
p1, p2 = A[pos: pos + bit_len / 2], A[pos + bit_len / 2: pos + bit_len]
avg1, avg2 = np.average(p1), np.average(p2)
if avg1 < avg2:
bits_str += "0"
elif avg1 > avg2:
bits_str += "1"
# print "Aircraft address:", bits_str, hex(int(bits_str, 2))
address = hex(int(bits_str, 2))
# Filter specific aircraft (optional)
# if address != "0x3c5ee2":
#    return
if df == 16 or df == 17 or df == 18 or df == 19 or df == 20 or df == 21:
# print "Pos:", start, "DF:", msg_type
# Data (56bit)
bits_str = ""
for p in range(32, 88):
pos = start_data + bit_len*p
p1, p2 = A[pos: pos + bit_len/2], A[pos + bit_len/2: pos + bit_len]
avg1, avg2 = np.average(p1), np.average(p2)
if avg1 < avg2:
bits_str += "0"
# bits[pos + bit_len / 2] = 50
elif avg1 > avg2:
bits_str += "1"
# http://www.lll.lu/~edward/edward/adsb/DecodingADSBposition.html
# print "Data:"
# print bits_str[:8], bits_str[8:20],  bits_str[20:22], bits_str[22:22+17], bits_str[39:39+17]
# Type Code:
tc, ec = int(bits_str[:5], 2), int(bits_str[5:8], 2)
# print("DF:", df, "TC:", tc)
# 1 - 4  Aircraft identification
# 5 - 8  Surface position
# 9 - 18  Airborne position (w/ Baro Altitude)
# 19  Airborne velocities
if tc >= 1 and tc <= 4: # and (df == 17 or df == 18):
print("Aircraft address:", address)
print("Data:")
print(bits_str[:8], bits_str[8:14],  bits_str[14:20], bits_str[20:26], bits_str[26:32], bits_str[32:38], bits_str[38:44])
symbols = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
code_str = ""
for p in range(8):
c = int(bits_str[8 + 6*p:8 + 6*(p + 1)], 2)
code_str += symbols[c]
print("Aircraft Identification:", code_str.replace('#', ''))
print()
if tc == 11:
print("Aircraft address:", address)
print("Data: (11)")
print(bits_str[:8], bits_str[8:20],  bits_str[20:22], bits_str[22:22+17], bits_str[39:39+17])
# Bit 22 contains the F flag which indicates which CPR format is used (odd or even)
# First frame has F flag = 0 so is even and the second frame has F flag = 1 so odd
# f = bits_str[21:22]
# print("F:", int(f, 2))
# Altitude
alt1b = bits_str[8:20]
if alt1b[-5] == '1':
bits = alt1b[:-5] + alt1b[-4:]
n = int(bits, 2)
alt_ft = n*25 - 1000
print("Alt (ft)", alt_ft)
# lat_dec = int(bits_str[22:22+17], 2)
# lon_dec = int(bits_str[39:39+17], 2)
# print("Lat/Lon:", lat_dec, lon_dec)
# http://airmetar.main.jp/radio/ADS-B%20Decoding%20Guide.pdf
print()
if tc == 19:
print("Aircraft address:", address)
print("Data:")
# print(bits_str)
print(bits_str[:5], bits_str[5:8], bits_str[8:10], bits_str[10:13], bits_str[13] ,bits_str[14:24], bits_str[24], bits_str[25:35], bits_str[35:36], bits_str[36:65])
subtype = int(bits_str[5:8], 2)
# https://mode-s.org/decode/adsb/airborne-velocity.html
spd, hdg, rocd = -1, -1, -1
if subtype == 1 or subtype == 2:
print("Velocity Subtype 1: Ground speed")
v_ew_sign = int(bits_str[13], 2)
v_ew = int(bits_str[14:24], 2) - 1       # east-west velocity
v_ns_sign = int(bits_str[24], 2)
v_ns = int(bits_str[25:35], 2) - 1       # north-south velocity
v_we = -1*v_ew if v_ew_sign else v_ew
v_sn = -1*v_ns if v_ns_sign else v_ns
spd = math.sqrt(v_sn*v_sn + v_we*v_we)  # unit in kts
hdg = math.atan2(v_we, v_sn)
hdg = math.degrees(hdg)                 # convert to degrees
hdg = hdg if hdg >= 0 else hdg + 360    # no negative val
if subtype == 3:
print("Subtype Subtype 3: Airspeed")
hdg = int(bits_str[14:24], 2)/1024.0*360.0
spd = int(bits_str[25:35], 2)
vr_sign = int(bits_str[36], 2)
vr = int(bits_str[36:45], 2)
rocd = -1*vr if vr_sign else vr         # rate of climb/descend
print("Speed (kts):", spd, "Rate:", rocd, "Heading:", hdg)
print()
# print()
def calc_coordinates():
def _cprN(lat, is_odd):
nl = _cprNL(lat) - is_odd
return nl if nl > 1 else 1
def _cprNL(lat):
try:
nz = 15
a = 1 - math.cos(math.pi / (2 * nz))
b = math.cos(math.pi / 180.0 * abs(lat)) ** 2
nl = 2 * math.pi / (math.acos(1 - a/b))
return int(math.floor(nl))
except:
# happens when latitude is +/-90 degree
return 1
def floor_(x):
return int(math.floor(x))
lat1b, lon1b, alt1b = "10111000111010011", "10000110111111000", "000101111001"
lat2b, lon2b, alt2b = "10010011101011100", "10000011000011011", "000101110111"
lat1, lon1, alt1 = int(lat1b, 2), int(lon1b, 2), int(alt1b, 2)
lat2, lon2, alt2 = int(lat2b, 2), int(lon2b, 2), int(alt2b, 2)
# 131072 is 2^17, since CPR lat and lon are 17 bits each
cprlat_even, cprlon_even = lat1/131072.0, lon1/131072.0
cprlat_odd, cprlon_odd = lat2/131072.0, lon2/131072.0
print(cprlat_even, cprlon_even)
j = floor_(59*cprlat_even - 60*cprlat_odd)
print(j)
air_d_lat_even = 360.0 / 60
air_d_lat_odd = 360.0 / 59
# Lat
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
if lat_even >= 270:
lat_even = lat_even - 360
if lat_odd >= 270:
lat_odd = lat_odd - 360
# Lon
ni = _cprN(lat_even, 0)
m = floor_(cprlon_even * (_cprNL(lat_even)-1) - cprlon_odd * _cprNL(lat_even) + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even)
print("Lat", lat_even, "Lon", lon)
# Altitude
# Q-bit (bit 48) indicates whether the altitude is encoded in multiples of 25 or 100 ft (0: 100 ft, 1: 25 ft)
# The value can represent altitudes from -1000 to +50175 ft.
if alt1b[-5] == '1':
bits = alt1b[:-5] + alt1b[-4:]
n = int(bits, 2)
alt_ft = n*25 - 1000
print("Alt (ft)", alt_ft)
fs, data = wavfile.read("adsb_20190311_191728Z_1090000kHz_RF.wav")
T = 1/fs
print("Sample rate %f MS/s" % (fs / 1e6))
print("Cnt samples %d" % len(data))
print("Duration: %f s" % (T * len(data)))
data = data.astype(float)
cnt = data.shape[0]
# Processing only part on file (faster):
# cnt = 10000000
# data = data[:cnt]
print("Processing I/Q...")
I, Q = data[:, 0], data[:, 1]
A = np.sqrt(I*I + Q*Q)
bits = np.zeros(cnt)
# To see scope without any processing, uncomment
# plt.plot(A)
# plt.show()
# sys.exit(0)
print("Extracting signals...")
pos = 0
avg = 200
msg_start = 0
# Find beginning of each signal
while pos < cnt - 16*1024:
# P1 - message start
while pos < cnt - 16*1024:
if A[pos] < avg and A[pos+1] > avg and pos - msg_start > 1000:
msg_start = pos
bits[pos] = 100
pos += 4
break
pos += 1
start1, start2, start3, start4 = msg_start, 0, 0, 0
# P2
while pos < cnt - 16*1024:
if A[pos] < avg and A[pos+1] > avg:
start2 = pos
bits[pos] = 90
pos += 1
break
pos += 1
# P3
while pos < cnt - 16*1024:
if A[pos] < avg and A[pos+1] > avg:
start3 = pos
bits[pos] = 80
pos += 1
break
pos += 1
# P4
while pos < cnt - 16*1024:
if A[pos] < avg and A[pos+1] > avg:
start4 = pos
bits[pos] = 70
pos += 1
break
pos += 1
sig_diff = start4 - start1
if 20 < sig_diff < 25:
bits[msg_start] = 500
bit_len = int((start4 - start1) / 4.5)
# print(pos, start1, start4, ' - ', bit_len)
# start = start1 + 8*bit_len
parse_message(A, msg_start, bit_len)
pos += 450
# For debugging: check signal start
# plt.plot(A)
# plt.plot(bits)
# plt.show()

I hope someone was interested, thanks for your attention.

Source: habr.com

Add a comment