Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Hola Habr. Probablemente, todos los que alguna vez han recibido o despedido a familiares o amigos en el aeropuerto han utilizado el servicio gratuito Flightradar24. Es una forma muy conveniente de rastrear la ubicación de un avión en tiempo real.

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

En parte anterior se describió el principio de funcionamiento de este tipo de servicio en línea. Ahora vamos a avanzar y averiguar qué datos se transmiten y reciben del avión a la estación receptora, y los decodificaremos nosotros mismos con Python.

Historia

Es obvio que los datos sobre los aviones no se transmiten para que los usuarios los vean en sus teléfonos inteligentes. El sistema se llama ADS–B (Broadcast de vigilancia dependiente automática) y sirve para la transmisión automática de información sobre la aeronave al centro de control de tráfico aéreo—su identificador, coordenadas, dirección, velocidad, altitud y otros datos se transmiten. Antes, sin este tipo de sistemas, el controlador solo podía ver un punto en el radar. Esto se volvió insuficiente cuando hubo demasiados aviones.

Técnicamente, el ADS-B se compone de un transmisor en la aeronave que envía periódicamente paquetes con información en una frecuencia bastante alta de 1090 MHz (hay otros modos, pero no son tan interesantes para nosotros, ya que las coordenadas solo se transmiten aquí). Por supuesto, además del transmisor, hay un receptor en algún lugar del aeropuerto, pero para nosotros, como usuarios, nos interesa nuestro propio receptor.

Por cierto, para comparar, el primer sistema de este tipo, Airnav Radarbox, diseñado para usuarios comunes, apareció en 2007 y costaba alrededor de 900$, además de que la suscripción a los servicios de red costaba otros 250$ al año.

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Las opiniones de esos primeros propietarios rusos se pueden leer en el foro radioscanner. Ahora, cuando los receptores RTL-SDR están ampliamente disponibles, se puede ensamblar un dispositivo similar por 30$, más detalles sobre esto se encontraron en parte anterior. Pasemos, entonces, al protocolo: veamos cómo funciona.

Recepción de señales

Para empezar, es necesario grabar la señal. La duración total de la señal es de solo 120 microsegundos, así que, para descomponer sus componentes cómodamente, se recomienda un receptor SDR con una frecuencia de muestreo de al menos 5 MHz.

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Después de grabar, obtenemos un archivo WAV con una frecuencia de muestreo de 5000000 muestras/seg, 30 segundos de esta grabación "pesa" alrededor de 500 MB. Escucharlo con un reproductor multimedia es, por supuesto, inútil: el archivo no contiene sonido, sino la señal de radio digitalizada directamente; así es como funciona la Radio Definida por Software.

Abriremos y procesaremos el archivo usando Python. Quienes deseen experimentar por su cuenta, pueden descargar un ejemplo de grabación. en el enlace.

Carguemos el archivo y veamos qué hay dentro.

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

Resultado: vemos claros "impulsos" en medio del ruido.

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Cada "impulso" es la propia señal, cuya estructura se puede observar bien si aumentamos la resolución en el gráfico.

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Como se puede ver, la imagen corresponde bastante a lo que se describe arriba. Se puede proceder al procesamiento de los datos.

Decodificación

Para comenzar, necesitamos obtener el flujo de bits. La señal misma está codificada utilizando el manchester encoding:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

A partir de la diferencia de niveles en los medio bytes, es fácil obtener los reales "0" y "1".

    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 += "1"

La estructura de la señal misma es la siguiente:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Veamos los campos con más detalle.

DF (Downlink Format, 5 bits) — define el tipo de mensaje. Hay varios tipos:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B
(fuente de la tabla)

Solo nos interesa el tipo DF17, ya que es el que contiene las coordenadas de la aeronave.

ICAO (24 bits) — código único internacional de la aeronave. Puedes verificar la aeronave por su código en el sitio web (desafortunadamente, el autor dejó de actualizar la base, pero todavía es actual). Por ejemplo, para el código 3c5ee2 tenemos la siguiente información:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Corrección: en los comentarios del artículo la descripción del código ICAO se proporciona con más detalle; recomiendo a los interesados que se familiaricen con ello.

DATA (56 o 112 bits) — los propios datos, que decodificaremos. Los primeros 5 bits de datos son el campo Type Code, que contiene el subtipo de datos almacenados (no confundir con DF). Hay muchos tipos:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B
(fuente de la tabla)

Analicemos algunos ejemplos de paquetes.

Identificación de aeronaves

Ejemplo en forma binaria:

00100 011 000101 010111 000111 110111 110001 111000

Campos de datos:

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

TC = 00100b = 4, cada símbolo C1-C8 contiene códigos correspondientes a los índices en la cadena:
#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######

Decodificando la cadena, es fácil obtener el código del avión: 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("Identificación del avión:", code_str.replace('#', ''))

Posición en vuelo

Si el nombre es sencillo, las coordenadas son más complicadas. Se transmiten en forma de tramas pares e impares. El código del campo TC = 01011b = 11.

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B

Ejemplo de paquetes par e impar:

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

El cálculo de las coordenadas se realiza mediante una fórmula bastante ingeniosa:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B
(fuente)

No soy un especialista en GIS, así que no sé de dónde se obtiene. Quien esté al tanto, por favor, escriba en los comentarios.

La altitud se calcula de manera más sencilla: dependiendo de un bit específico, puede representarse como múltiplo de 25 o 100 pies.

Velocidad en vuelo

Paquete con TC=19. Lo interesante aquí es que la velocidad puede ser tanto exacta, relativa al suelo (Ground Speed), como en aire, medida por un sensor del avión (Airspeed). También se transmiten muchos otros campos diferentes:

Flightradar24 — ¿Cómo funciona? Parte 2, protocolo ADS-B
(fuente)

Conclusión

Como se puede ver, la tecnología ADS-B se ha convertido en una interesante combinación, donde algún estándar es útil no solo para profesionales, sino también para usuarios comunes. Pero, por supuesto, un papel clave en esto ha sido el abaratamiento de la tecnología de receptores SDR digitales, que permiten recibir señales a frecuencias superiores a un gigaherzio de manera económica.

En el propio estándar, por supuesto, hay mucho más. Los interesados pueden consultar el PDF en la página ICAO o visitar el ya mencionado el sitio web.

Es improbable que a muchos les sirva todo lo anterior, pero al menos espero que la idea general de cómo funciona haya quedado clara.

Por cierto, ya existe un decodificador terminado en Python, se puede estudiar aquí. Y los propietarios de receptores SDR pueden ensamblar y poner en funcionamiento un decodificador ADS-B ya preparado desde la página, se habló más sobre esto en parte anterior.

El código fuente del parser, descrito en el artículo, se incluye a continuación. Es un ejemplo de prueba que no pretende ser producción, pero algo en él funciona y puede utilizarse para analizar el archivo mencionado anteriormente.
Código fuente (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()

Espero que a alguien le haya resultado interesante, gracias por su atención.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster