Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

你好哈布尔。 可能每个在飞机上见过或送别亲友的人都使用过免费的 Flightradar24 服务。 这是一种非常方便的实时跟踪飞机位置的方法。

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

В 第一部分 描述了这种在线服务的工作原理。 现在,我们将继续弄清楚从飞机到接收站发送和接收的数据,并使用 Python 自行解码。

故事

显然,飞机数据不会传输给用户在智能手机上查看。 该系统被称为ADS-B(自动相关监视-广播),用于自动将飞机的信息传输到控制中心——传输其标识符、坐标、方向、速度、高度和其他数据。 此前,在此类系统出现之前,调度员只能看到雷达上的一个点。 当飞机太多时,这已经不够了。

从技术上讲,ADS-B 由飞机上的发射器组成,该发射器以 1090 MHz 的相当高的频率定期发送信息包(还有其他模式,但我们对它们不太感兴趣,因为只在这里传输坐标)。 当然,除了发射器之外,在机场的某个地方还有一个接收器,但对于我们作为用户来说,我们自己的接收器很有趣。

顺便说一句,作为比较,第一个此类系统 Airnav Radarbox 是为普通用户设计的,于 2007 年出现,价格约为 900 美元;订阅网络服务每年还要花费 250 美元。

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

可以在论坛上阅读第一批俄罗斯所有者的评论 无线电扫描仪。 现在 RTL-SDR 接收器已广泛使用,只需 30 美元即可组装一个类似的设备;有关此内容的更多信息,请参阅 第一部分。 让我们继续讨论协议本身 - 让我们看看它是如何工作的。

接收信号

首先,需要记录信号。 整个信号的持续时间仅为 120 微秒,因此为了轻松拆卸其组件,需要采样频率至少为 5 MHz 的 SDR 接收器。

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

录音后,我们收到一个采样率为 5000000 样本/秒的 WAV 文件;这样的录音 30 秒“重”约 500MB。 当然,用媒体播放器收听它是没有用的 - 该文件不包含声音,而是直接数字化的无线电信号 - 这正是软件定义无线电的工作原理。

我们将使用 Python 打开并处理该文件。 想要自己尝试的人可以下载示例录音 链接.

让我们下载该文件并看看里面有什么。

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

结果:我们在背景噪音中看到了明显的“脉冲”。

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

每个“脉冲”都是一个信号,如果提高图表上的分辨率,其结构就清晰可见。

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

正如您所看到的,该图片与上面的描述非常一致。 您可以开始处理数据。

解码

首先,您需要获取比特流。 信号本身使用曼彻斯特编码进行编码:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

从半字节的电平差很容易得到真正的“0”和“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 += "0"
        elif avg1 > avg2:
            bits_str += "1"

信号本身的结构如下:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

让我们更详细地看看这些字段。

DF (下行链路格式,5 位)- 确定消息的类型。 有以下几种类型:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议
(表源)

我们只对 DF17 型感兴趣,因为…… 它包含飞机的坐标。

ICAO (24 位)- 飞机的国际唯一代码。 您可以通过代码查看飞机 在线 (不幸的是,作者已经停止更新数据库,但仍然相关)。 例如,对于代码 3c5ee2,我们有以下信息:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

编辑:在 对文章的评论 ICAO代码的描述比较详细;我建议有兴趣的人阅读它。

数据 (56 或 112 位)- 我们将解码的实际数据。 数据的前5位是字段 类型代码,包含正在存储的数据的子类型(不要与 DF 混淆)。 其中有很多类型:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议
(表源)

让我们看一些包的示例。

飞机识别

二进制形式的示例:

00100 011 000101 010111 000111 110111 110001 111000

数据字段:

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

TC = 00100b = 4,每个字符 C1-C8 包含与该行中的索引相对应的代码:
#ABCDEFGHIJKLMNOPQRSTUVWXYZ####_################0123456789######

通过解码该字符串,很容易得到飞机代码: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('#', ''))

空中位置

如果名称简单,那么坐标就比较复杂。 它们以 2 个偶数帧和奇数帧的形式传输。 字段代码 TC = 01011b = 11。

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议

偶数和奇数数据包示例:

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

坐标的计算本身是根据一个相当棘手的公式进行的:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议
()

我不是GIS专家,所以我不知道它来自哪里。 谁知道,写在评论里。

高度被认为更简单 - 根据具体位,它可以表示为 25 或 100 英尺的倍数。

空中速度

TC=19 的封装。 这里有趣的是,速度可以是相对于地面的精确速度(地速),也可以是由飞机传感器测量的空中速度(空速)。 还传输许多不同的字段:

Flightradar24 - 它是如何工作的? 第 2 部分,ADS-B 协议
()

结论

正如您所看到的,ADS-B 技术已成为一种有趣的共生关系,标准不仅对专业人士有用,而且对普通用户也有用。 当然,其中发挥关键作用的是更便宜的数字 SDR 接收器技术,该技术允许设备“花几分钱”接收频率高于千兆赫的信号。

当然,标准本身还有更多内容。 有兴趣的可以查看页面上的PDF ICAO 或访问上面已经提到的 现场.

上述所有内容不太可能对许多人有用,但我希望至少其工作原理的总体思路仍然存在。

顺便说一句,Python中已经有现成的解码器,你可以研究一下 这里。 SDR 接收器的所有者可以组装并启动现成的 ADS-B 解码器 从页面,这在中进行了更详细的讨论 第一部分.

下面给出了文章中描述的解析器的源代码。 这是一个测试示例,并不冒充生产环境,但其中有些功能可以正常工作,并且可以用来解析上面记录的文件。
源代码(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()

我希望有人感兴趣,感谢您的关注。

来源: habr.com

添加评论