Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Bonjour Habr. Probablement que chacun, qui a déjà rencontré ou accompagné des proches ou des amis à l'aéroport, a utilisé le service gratuit Flightradar24. C'est un moyen très pratique de suivre la position d'un avion en temps réel.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Dans de la première partie le principe de fonctionnement de ce type de service en ligne a été décrit. Maintenant, nous allons plus loin et découvrirons quelles données sont transmises et reçues de l'avion à la station de réception, et nous les décoderons nous-mêmes avec Python.

Historique

Il est évident que les données sur les avions ne sont pas transmises pour que les utilisateurs les voient sur leurs smartphones. Le système s'appelle ADS–B (Automatic dependent surveillance—broadcast) et sert à transmettre automatiquement les informations sur l'avion au centre de contrôle — son identifiant, ses coordonnées, sa direction, sa vitesse, son altitude et d'autres données sont transmises. Auparavant, avant l'apparition de tels systèmes, le contrôleur ne pouvait voir qu'un point sur le radar. Cela est devenu insuffisant lorsque le nombre d'avions a trop augmenté.

Techniquement, l'ADS-B consiste en un émetteur sur l'avion, qui envoie périodiquement des paquets d'informations à une fréquence suffisamment élevée de 1090 MHz (il existe d'autres modes, mais ils ne nous intéressent pas tellement car les coordonnées ne sont transmises qu'ici). Bien sûr, en plus de l'émetteur, il y a un récepteur quelque part à l'aéroport, mais pour nous, en tant qu'utilisateurs, le récepteur qui nous intéresse est notre propre récepteur.

À propos, pour comparer, le premier tel système, Airnav Radarbox, destiné aux utilisateurs ordinaires, est apparu en 2007 et coûtait environ 900 $, en plus d'un abonnement d'environ 250 $ par an pour les services en ligne.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Les avis des premiers propriétaires russes peuvent être lus sur le forum radioscanner. Maintenant, avec la disponibilité massive des récepteurs RTL-SDR, un dispositif similaire peut être assemblé pour 30 $, des détails à ce sujet ont été abordés dans de la première partie. Nous allons maintenant passer au protocole — voyons comment cela fonctionne.

Réception des signaux

Pour commencer, le signal doit être enregistré. L'ensemble du signal ne dure que 120 microsecondes, donc pour examiner confortablement ses composants, un récepteur SDR avec une fréquence d'échantillonnage d'au moins 5 MHz est souhaitable.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Après l'enregistrement, nous obtenons un fichier WAV avec une fréquence d'échantillonnage de 5000000 échantillons/sec, 30 secondes de cet enregistrement « pèsent » environ 500 Mo. L'écouter avec un lecteur multimédia est bien sûr inutile — le fichier ne contient pas de son, mais directement un signal radio numérisé — c'est ainsi que fonctionne la Radio Définie par Logiciel.

Nous allons ouvrir et traiter le fichier avec Python. Ceux qui souhaitent expérimenter par eux-mêmes peuvent télécharger un exemple d'enregistrement. via le lien.

Téléchargons le fichier et voyons ce qu'il contient.

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

Résultat : nous voyons des « impulsions » évidentes sur fond de bruit.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Chaque « impulsion » représente le signal, dont la structure est bien visible si l'on augmente la résolution du graphique.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Comme on peut le voir, l'image correspond tout à fait à ce qui a été décrit ci-dessus. Nous pouvons commencer le traitement des données.

Décodage

Pour commencer, il faut obtenir le flux binaire. Le signal est codé à l'aide de l'encodage Manchester :

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

À partir des différences de niveaux dans les demi-octets, il est facile d'obtenir les réels « 0 » et « 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 structure même du signal est la suivante :

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Examinons les champs plus en détail.

DF (Downlink Format, 5 bits) — définit le type de message. Il existe plusieurs types :

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B
(source du tableau)

Nous ne nous intéressons qu'au type DF17, car c'est celui qui contient les coordonnées de l'aéronef.

ICAO (24 bits) — code unique international de l'aéronef. Vous pouvez vérifier l'avion par son code sur le site (malheureusement, l'auteur a cessé de mettre à jour la base, mais elle est encore d'actualité). Par exemple, pour le code 3c5ee2, nous avons les informations suivantes :

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Correction : dans les commentaires de l'article la description du code ICAO est présentée plus en détail, je recommande à ceux qui s'intéressent de la consulter.

DATA (56 ou 112 bits) — ce sont les données mêmes que nous allons décoder. Les 5 premiers bits de données constituent le champ Type Code, contenant le sous-type des données stockées (ne pas confondre avec DF). Il existe de nombreux types.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B
(source du tableau)

Examinons quelques exemples de paquets.

Identification de l'aéronef

Exemple en binaire :

00100 011 000101 010111 000111 110111 110001 111000

Champs de données :

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

TC = 00100b = 4, chaque caractère C1-C8 contient des codes correspondant aux indices dans la chaîne :
#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######

En décodant la chaîne, il est facile d'obtenir le code de l'avion : EWG7184

symbols = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"\ncode_str = ""\nfor p in range(8):\n     c = int(bits_str[8 + 6*p:8 + 6*(p + 1)], 2)\n     code_str += symbols[c]\nprint("Identification de l'avion :", code_str.replace('#', ''))

Position aérienne

Si le nom est simple, les coordonnées sont plus complexes. Elles sont transmises sous forme de trames pairs et impairs de 2. Le code du champ TC = 01011b = 11.

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B

Exemple de paquets pairs et impairs :

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

Le calcul des coordonnées se fait selon une formule assez complexe :

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B
(source)

Je ne suis pas un spécialiste des systèmes d'information géographique, donc je ne sais pas d'où cela provient. Ceux qui savent, veuillez laisser un commentaire.

La hauteur est plus facile à calculer — selon un bit spécifique, elle peut être représentée soit comme un multiple de 25, soit de 100 pieds.

Vitesse aérienne

Paquet avec TC=19. Ce qui est intéressant ici, c'est que la vitesse peut être à la fois précise, par rapport à la terre (Ground Speed), et aérienne, mesurée par le capteur de l'avion (Airspeed). De nombreux autres champs sont également transmis :

Flightradar24 — comment ça fonctionne ? Partie 2, protocole ADS-B
(source)

Conclusion

Comme on peut le voir, la technologie ADS-B est devenue un intéressant symbiote, lorsqu'une norme est utile non seulement aux professionnels, mais aussi aux utilisateurs ordinaires. Bien sûr, le rôle clé dans cela a été joué par la réduction des coûts de la technologie des récepteurs SDR numériques, permettant de recevoir des signaux à des fréquences supérieures au gigahertz sur un appareil pour pratiquement rien.

Dans la norme elle-même, il y a bien plus de choses. Ceux qui le souhaitent peuvent consulter le PDF sur la page ICAO ou visiter le site mentionné ci-dessus site.

Il est peu probable que tout ce qui précède soit utile à beaucoup de gens, mais au moins l'idée générale de son fonctionnement, j'espère, est restée.

D'ailleurs, un décodeur prêt à l'emploi en Python existe déjà, vous pouvez l'étudier ici. Et les propriétaires de récepteurs SDR peuvent assembler et lancer un décodeur ADS-B prêt à l'emploi depuis la page, cela a été discuté en détail dans de la première partie.

Le code source du parseur, décrit dans l'article, est présenté ci-dessous. C'est un exemple de test, ne prétendant pas à la production, mais certaines choses fonctionnent et vous pouvez l'utiliser pour parser le fichier enregistré ci-dessus.
Code source (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()

J'espère que cela a intéressé quelqu'un, merci de votre attention.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster