Flightradar24 - كيف يعمل؟ الجزء 2 ، بروتوكول ADS-B

سأخمن وأقول إن كل من قام أصدقاؤه أو عائلته بالطيران على متن طائرة ، قد استخدم Flightradar24 - خدمة مجانية ومريحة لتتبع الرحلات في الوقت الفعلي.

صورة

في الجزء الأول ، تم وصف الأفكار الأساسية للعملية. الآن دعنا نذهب إلى أبعد من ذلك ونكتشف ما هي البيانات التي ترسلها وتستقبلها بالضبط بين الطائرة والمحطة الأرضية. سنقوم أيضًا بفك تشفير هذه البيانات باستخدام Python.

تاريخ


يجب أن يكون واضحًا أن بيانات الطائرات ليست مخصصة للمستخدمين الذين يريدون رؤيتها على هواتفهم الذكية فقط. يسمى هذا النظام ADS - B (مراقبة تعتمد تلقائيًا - بث) ، وقد تم تصميمه للإرسال التلقائي لبيانات معلومات الطائرة إلى مركز التحكم - يتم إرسال معلمات مختلفة ، مثل الإحداثيات والسرعة والعنوان والارتفاع وغيرها. في السابق ، لم يكن بإمكان المرسل رؤية سوى نقطة على شاشة الرادار. وأصبح بالتأكيد لا يكفي عندما زاد عدد الطائرات بشكل كبير.

من الناحية الفنية ، يتكون ADS-B من جهاز إرسال داخل الطائرة ، يرسل بشكل دوري إطارات بيانات المعلومات بتردد عالٍ نسبياً يبلغ 1090 ميغاهيرتز (هناك بعض الأوضاع الأخرى ، لكنها ليست مثيرة للاهتمام بالنسبة لنا ، لأن الإحداثيات يتم إرسالها هنا فقط ). بالطبع ، هناك أيضًا جهاز استقبال في مكان ما في المطار ، ولكن بالنسبة لنا ، كما هو الحال بالنسبة للمستخدمين ، فإن جهاز الاستقبال الخاص بنا أكثر إثارة للاهتمام.

للمقارنة فقط ، ظهر أول نظام من هذا القبيل مصمم للمستخدمين العاديين ، وهو Airnav Radarbox ، في عام 2007 ، وبلغت تكلفته حوالي 900 دولار ، وحوالي 250 دولارًا في السنة كلف اشتراكًا في خدمات شبكتهم (الفكرة الرئيسية لهذا النظام هي جمع و مشاركة البيانات من العديد من أجهزة الاستقبال ، جهاز استقبال مستقل غير مجدية نسبيا).



الآن ، عندما أصبحت أجهزة الاستقبال RTL-SDR متاحة بدرجة أكبر بكثير ، يمكن صنع جهاز مماثل مقابل 30 دولارًا. يمكن العثور عليه في الجزء الأول من المقالة ، وسوف نذهب أبعد من ذلك ونصف البروتوكول نفسه - دعنا نرى كيف يعمل.

تلقي إشارة


أولاً ، نحتاج إلى تسجيل عينة إشارة. يبلغ طول الإشارة بأكملها 120 ميكروثانية فقط ، ولترى تفاصيلها بدقة "دقة" ، من الأفضل أن يكون لها راديو SDR بمعدل لا يقل عن 5 ميجاهرتز.

صورة

بعد التسجيل ، نحصل على ملف WAV بمعدل أخذ العينات 5000000 / ثانية ، 30 ثانية من هذا السجل بحجم حوالي 500 ميغابايت. بالطبع ، من غير المجدي أن تستمع إليه مع مشغل الوسائط المفضل لديك - الملف لا يحتوي على صوت ، ولكن إشارة راديو رقمية مباشرة نفسها - هذه هي بالضبط طريقة عمل Software Defined Radio.

يمكننا فتح ومعالجة هذا الملف مع بيثون. يمكن لأولئك الذين يرغبون في إجراء التجربة بأنفسهم ، تنزيل عينة من هذا الرابط .

يتيح فتح ملف ونرى ، ما هو في الداخل.

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

النتيجة: نرى بعض "النبضات" على الضوضاء.



كل "دفعة" هي إشارة ، يكون هيكلها مرئيًا بوضوح إذا قمنا بزيادة الدقة على الرسم البياني.



كما نرى ، كانت الصورة متوافقة تمامًا مع الوصف أعلاه. يمكننا الآن معالجة هذه البيانات.

فك


أولا ، نحن بحاجة إلى تيار قليلا. يتم ترميز الإشارة نفسها بترميز manchester:



من الاختلافات نصف لدغة ، يمكننا بسهولة الحصول على "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" 

تبدو بنية الإشارة نفسها كما يلي:



لنرى الحقول بمزيد من التفاصيل.

DF (تنسيق الوصلة الهابطة ، 5 بت) - يحدد نوع الرسالة. هناك عدة أنواع منها:


( مصدر الصفحة )

نحن مهتمون فقط بنوع DF17 ، لأن هذا واحد فقط يحتوي على إحداثيات الطائرة.

الايكاو (24 بت) - هو رمز دولي فريد للطائرة. يمكننا التحقق من الطائرة عن طريق الرمز الخاص بها على هذا الموقع (للأسف ، توقف المؤلف عن تحديث قاعدة البيانات ، لكنه لا يزال ذا صلة). على سبيل المثال ، بالنسبة للرمز 3c5ee2 ، يمكننا الحصول على المعلومات التالية:



البيانات (56 أو 112 بت) - هي البيانات نفسها ، والتي سنقوم بفك تشفيرها. أول 5 بتات من البيانات هي حقل Type Code ، الذي يحتوي على النوع الفرعي للبيانات التي يتم تخزينها (لا يتم خلطها مع حقل DF ). هناك الكثير من هذه الأنواع:


( مصدر الجدول )

لنلقي نظرة على بعض الأمثلة.

تحديد الطائرات

مثال في شكل ثنائي:

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.



مثال على إطارات البيانات الفردية والزوجية:

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

يستخدم حساب الإحداثيات نفسها صيغة صعبة بعض الشيء:


( المصدر )

أنا لست خبيرا في نظم المعلومات الجغرافية ، لذلك لا أعرف من أين تأتي. من يعرف ذلك بشكل أفضل ، يرجى كتابة التعليقات.

حساب الارتفاع أبسط - اعتمادًا على بت معين ، يمكن تمثيله على أنه مضاعف 25 أو 100 قدم.

السرعة المحمولة جوا

Dataframe with TC = 19. الشيء المثير للاهتمام هنا هو أن السرعة يمكن أن تكون ذات صلة بالأرض (أكثر دقة ، تسمى السرعة الأرضية) ، و Airspeed ، المقاسة بواسطة مستشعر هواء الطائرة (يمكن أن تكون أقل دقة بسبب الرياح). يتم نقل العديد من المجالات المختلفة الأخرى أيضًا:


( المصدر )

استنتاج


كما نرى ، أصبحت تقنية ADS-B تعايشًا مثيرًا للاهتمام ، عندما يكون المعيار قابلاً للاستخدام على نطاق واسع ليس فقط للمحترفين ، ولكن أيضًا للمستخدمين العاديين. ولكن من المؤكد أن الدور الرئيسي في ذلك تم بواسطة تخفيض تكلفة تقنية مستقبلات SDR الرقمية ، والتي تتيح استقبال الإشارات بتردد أعلى من جيجاهيرتز على جهاز رخيص للغاية.

المعيار نفسه ، بالطبع ، لديه المزيد من البيانات. يمكن للمهتمين عرض ملف PDF على الصفحة ICAO أو زيارة موقع mode-s.org المذكور أعلاه. من غير المحتمل أن يستخدم معظم القراء هذه المقالة حقًا ، لكنني آمل أن تكون الفكرة العامة حول كيفية عملها على الأقل أكثر وضوحًا الآن.

بالمناسبة ، وحدة فك ترميز ADS-B Python موجودة بالفعل ، ويمكن التحقيق في جيثب . يمكن لمالكي مستقبلات حقوق السحب الخاصة أيضًا إنشاء وتشغيل وحدة فك ترميز ADS-B جاهزة للاستخدام من هذه الصفحة ، و (سأكرر مرة أخرى) بعض التفاصيل التي قمنا أيضًا بتوضيحها في الجزء الأول من هذه المقالة.

يتم إعطاء رمز مصدر المحلل اللغوي ، الموضح أعلاه ، تحت المفسد. هذا مجرد مثال اختبار لا يتظاهر بجودة الإنتاج ، ولكنه يعمل بشكل عام ، ويمكنه تحليل ملف WAV ، المسجل أعلاه.

شفرة المصدر (بيثون)
 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: %fs" % (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() 


آمل أن تكون مفيدة ، شكرا للقراءة.

Source: https://habr.com/ru/post/ar447078/


All Articles