March 12

Разговоры в небе. Как самолеты передают данные по ADS-B

  1. Тестовый полигон
  2. Структура ADS-B
  3. Передача

Сов­ремен­ные самоле­ты неп­рерыв­но переда­ют друг дру­гу телемет­рию по про­токо­лу ADS-B. В этой статье мы с тобой сде­лаем собс­твен­ный вооб­ража­емый самолет, что­бы пос­мотреть, как работа­ет переда­ча дан­ных. Конеч­но, будем при этом соб­людать все законы и пра­вила безопас­ности.

ADS-B — это часть боль­шого про­токо­ла Mode S, который активно исполь­зует­ся для иден­тифика­ции и отсле­жива­ния воз­душных судов. ADS-B поз­воля­ет отсле­живать летатель­ные аппа­раты, на которых уста­нов­лен спе­циаль­ный тран­спон­дер. Он регуляр­но переда­ет дан­ные о самоле­те: высоту, коор­динаты, ско­рость и мно­гое дру­гое. При­нять их может любой жела­ющий, так как никакой защиты или шиф­рования в про­токо­ле не пре­дус­мотре­но.

На этот раз мы пол­ностью сос­редото­чим­ся на переда­че сво­его сиг­нала ADS-B. Что­бы никому не помешать, при­нимать мы его тоже будем на свой при­емник. Учти: это вов­се не пособие по осна­щению самоле­тов тран­спон­дерами. Ско­рее мы будем исполь­зовать этот любопыт­ный при­мер для демонс­тра­ции того, как кто‑то может разоб­рать и под­делать любой незащи­щен­ный сиг­нал.

ТЕСТОВЫЙ ПОЛИГОН

Тес­товое окру­жение необ­ходимо, что­бы видеть, что мы дела­ем, но при этом не вли­ять на обо­рудо­вание аэро­пор­тов. Вряд ли они оце­нят наши экспе­римен­ты! Зато ущерб впол­не могут оце­нить, а сум­му выс­тавить к опла­те. Что­бы это­го избе­жать, я взял два SDR и исполь­зовал на минималь­ной мощ­ности в экра­ниру­ющем помеще­нии. К тому же сиг­налы на час­тотах око­ло 1 ГГц, которые я исполь­зовал, прак­тичес­ки не рас­простра­няют­ся за горизонт.

В моем рас­поряже­нии ока­зались два неп­лохих и доволь­но популяр­ных SDR: HackRF и BladeRF. Пер­вый я буду исполь­зовать как передат­чик, а вто­рой — как при­емник. Прог­рам­мную часть сделаем с помощью программы, которая уме­ет пар­сить сиг­налы Mode S (частью которо­го и явля­ется ADS-B) в реаль­ном вре­мени и выводить их в чита­емом фор­мате.

Схе­ма уста­нов­ки будет выг­лядеть при­мер­но так.

Схе­ма лабора­тории

Для ста­биль­ной работы на оба SDR нуж­но нак­рутить любую шты­ревую антенну, так как без нее ничего работать не будет или будет, но очень‑очень пло­хо.

Настройка BladeRF

С завод­ской про­шив­кой BladeRF не уме­ет работать с dump1090 — при­дет­ся их под­ружить вруч­ную. Пер­вым шагом будет ска­чива­ние про­шив­ки, сов­мести­мой с ADS-B, с офи­циаль­ного сай­та раз­работ­чика:

wget https://www.nuand.com/fpga/adsbxA4.rbf

Пос­ле это­го сра­зу же заг­ружа­ем ска­чан­ный файл в BladeRF:

bladerf-cli -i adsbxA4.rbf

Даль­ше нам надо уста­новить еще две прог­раммы: bladeRF-adsb и саму dump1090. Пер­вая нуж­на прос­то для того, что­бы тран­сли­ровать дан­ные с SDR нап­рямую в dump1090.

Ска­чива­ем и собира­ем dump1090:

git clone https://github.com/mutability/dump1090.git cd dump1090 make ./dump1090 --net-only --raw --interactive

Пос­ле запус­ка dump1090 перехо­дим к мос­ту:

git clone https://github.com/Nuand/bladeRF-adsb cd bladeRF-adsb/bladeRF_adsb wget http://nuand.com/fpga/adsbx40.rbf wget http://nuand.com/fpga/adsbx115.rbf make ./bladeRF_adsb

Те­перь, ког­да все готово для при­ема и декоди­рова­ния сиг­налов ADS-B, уже мож­но при­нять какие‑то сиг­налы от самоле­тов (да, даже в ком­нате), осо­бен­но если самолет близ­ко или если у тебя хорошая антенна.

ViralAir

Что­бы генери­ровать свои сооб­щения ADS-B, я соз­дал ути­литу ViralAir и выложил ее на GitHub. Что­бы пов­торять за мной, ска­чай и ском­пилируй ее:

git clone https://github.com/st3rv04ka/ViralAir cd ViralAir go build cmd/viralair/main.go

Пос­ле сбор­ки появит­ся файл main, который и будет генери­ровать готовый для переда­чи файл. Фор­мат это­го фай­ла пол­ностью под­ходит для HackRF, но может быть сов­местим и с дру­гими SDR (я не про­верял). Что­бы про­верить, что все хорошо, мож­но соз­дать тес­товый самолет с позыв­ным 0xDEADBE на высоте 9999 футов сле­дующей коман­дой:

./main -altitude 9999.0 --icao 0xDEADBE

В текущей дирек­тории дол­жен появить­ся файл Samples.iq8s. Если он дей­стви­тель­но есть, зна­чит, все работа­ет и мож­но про­дол­жать.

СТРУКТУРА ADS-B

Подготовка

Да­вай нем­ного осве­жим зна­ния про струк­туру сооб­щения ADS-B из прош­лой статьи. Сооб­щение может быть раз­ной дли­ны — 56 либо 112 бит. От дли­ны зависит тип переда­ваемой информа­ции, а сам тип записы­вает­ся в поле TC (Type Code) сооб­щения. Обыч­ное 112-бит­ное сооб­щение выг­лядит сле­дующим обра­зом.

Как ты мог заметить, никако­го поля TC тут нет — и все вер­но, оно спря­тано внут­ри поля ME. Дело в том, что ADS-B — это все­го лишь часть боль­шого про­токо­ла Mode S, и выше пред­став­лена струк­тура имен­но сооб­щения Mode S, в то вре­мя как все спе­цифич­ные для ADS-B поля находят­ся внут­ри поля дан­ных (ME) Mode S.

Да­вай осве­жим в памяти наз­начение осталь­ных полей пакета:

  • DF (downlink format) отве­чает за тип сиг­нала в Mode S. Для ADS-B всег­да равен 17;
  • CA (transponder capability) отве­чает за тип тран­спон­дера, который мы теоре­тичес­ки можем пос­тавить какой угод­но, но на прак­тике будем исполь­зовать 6 (что озна­чает тран­спон­дер уров­ня 2+);
  • пос­ле CA идет ICAO — уни­каль­ный номер самоле­та. В эти 24 бита мы можем запих­нуть что угод­но, а в рам­ках этой статьи будем запус­кать самолет 0xDEADBE;
  • ME — само сооб­щение ABS-B. Это может быть высота, ско­рость или коор­динаты. Мы будем переда­вать толь­ко коор­динаты (TC равен 11), без ско­рос­ти и высоты. Ско­рость и высота ничем не хуже, прос­то мне хочет­ся разоб­рать под­робнее кодиро­вание CPR.

CPR

Воз­можно, ты читал прош­лую статью и еще пом­нишь, как я мучил­ся с CPR и упро­щен­ными кар­тами для переда­чи в малень­ких сооб­щени­ях. Из‑за этих карт в ADS-B есть два вида сооб­щений с коор­дината­ми: чет­ное и нечет­ное, и, толь­ко имея оба сооб­щения, мож­но опре­делить точ­ную локацию самоле­та. Соз­давать такие пакеты еще слож­нее, чем при­нимать.

Да­вай про­бежим­ся по моему коду на Go, который перево­дит коор­динаты с CPR:

func nl(declatIn float64) float64 { // Близко к полюсам секторов мало, возвращаем всего один if math.Abs(declatIn) >= 87.0 { return 1.0 } return math.Floor( (2.0 * math.Pi) * math.Pow( math.Acos(1.0 - (1.0-math.Cos(math.Pi/(2.0*latz))) / math.Pow(math.Cos((math.Pi/180.0) * math.Abs(declatIn)), 2)), -1))}func dlon(declatIn float64, ctype int, surface int) float64 { var tmp float64 if surface == 1 { tmp = 90.0 } else { tmp = 360.0 } nlcalc := math.Max(nl(declatIn)-float64(ctype), 1) return tmp / nlcalc} // Кодируем в CPRfunc CprEncode(lat float64, lon float64, ctype int, surface int) (int, int) { var scalar float64 if surface == 1 { scalar = math.Pow(2, 19) } else { scalar = math.Pow(2, 17) } dlati := dlat(ctype, surface) yz := math.Floor(scalar*((math.Mod(lat, dlati))/dlati) + 0.5) dloni := dlon(lat, ctype, surface) xz := math.Floor(scalar*((math.Mod(lon, dloni))/dloni) + 0.5) return int(yz) & ((1 << 17) - 1), int(xz) & ((1 << 17) - 1)}

Да, в этом коде черт ногу сло­мит, но на самом деле за слож­ной матема­тикой сто­ят доволь­но прос­тые алго­рит­мы.

Нач­нем по поряд­ку, с фун­кции nl(). Она рас­счи­тыва­ет количес­тво зон на задан­ной нами широте. Логика прос­тая: чем бли­же к полюсам, тем мень­ше зон задан­ного раз­мера помес­тится на одной широте. Вот так выг­лядят реаль­ные зоны в CPR.

Кар­та зон в CPR

Что­бы не морочить себе голову слож­ной матема­тикой, при широте боль­ше 87 гра­дусов будем воз­вра­щать количес­тво делений рав­ное еди­нице. Фор­мула для рас­чета взя­та из офи­циаль­ной докумен­тации к про­токо­лу, и в самом докумен­те она выг­лядит доволь­но страш­но.

Фор­мула для рас­чета количес­тва зон

Пос­ле nl() идет фун­кция dlon(). Количес­тво делений на широте у нас уже есть, а теперь нуж­но рас­счи­тать интервал, что­бы рав­номер­но рас­пре­делить эти деления по кру­гу и что­бы все они были оди­нако­выми. При этом важ­но учи­тывать, где находит­ся самолет, — на зем­ле или в воз­духе. Если самолет в воз­духе, мы делим 360 на количес­тво интерва­лов минус тип сооб­щения (even или odd). Получен­ный резуль­тат воз­вра­щает­ся в кон­це фун­кции. Фун­кция dlat() дела­ет плюс‑минус то же самое для дол­готы, так что не будем оста­нав­ливать­ся на ней отдель­но.

Те­перь перей­дем к основной фун­кции рас­чета CPR. Пос­ле получе­ния количес­тва зон мож­но рас­счи­тать непос­редс­твен­но коор­динаты самоле­та. Для это­го учтем мно­житель, который мож­но най­ти в докумен­тации про­токо­ла (он скрыт глу­боко в фор­мулах). Для воз­духа он будет равен 217, а для зем­ли — 219.

Фор­мула для воз­духа
Фор­мула для зем­ли

По­лучен­ное пос­ле кодиро­вания чис­ло нуж­но сжать до 17 бит, что и дела­ется при воз­вра­те резуль­татов из фун­кции.

PI

Ес­ли ты выдох­нул с мыслью, что все слож­ное уже позади, вды­хай обратно! Вто­рая слож­ность при переда­че сво­их сиг­налов — это рас­чет кон­троль­ной сум­мы (CRC), или parity bits. Нуж­но пос­читать и добавить в конец сооб­щения 24 бита, которые исполь­зуют­ся для про­вер­ки целос­тнос­ти сооб­щения. Имен­но на них ори­енти­рует­ся, нап­ример, dump1090. Заг­воз­дка в том, что фор­мула для рас­чета выг­лядит так.

Ал­горитм под­сче­та parity bits

В псев­докоде эта фун­кция выг­лядит при­мер­но так:

generator = 1111111111111010000001001 # 11 + 3 нулевых байта data_hex = 8D406B902015A678D4D220[000000] # 88 бит data = 1000110101000000011010 1110010000001000000001 0101101001100111100011 0101001101001000100000 [000000000000000000000000] # 24 бита FOR i FROM 0 TO (112-24): IF data[i] IS 1: data[i:i+24] = data[i:i+24] XOR generator remainder = data[-24:] # Результат: 101010100100101111011010, или AA4BDA в HEX

Ге­нера­тор — это кон­стан­та, которую наш­ли спе­циаль­но для это­го алго­рит­ма и которая наибо­лее эффектив­на. В цик­ле мы прос­то про­ходим по всем битам от 0 до 88 (112 минус 24, потому что пос­ледние 24 бита мы сей­час и запол­няем) и при­меня­ем исклю­чающее ИЛИ с генера­тором. Получен­ные 24 бита нуж­но добавить к нашему сооб­щению, что­бы у нас был закон­ченный пакет дан­ных. Вот как я это сде­лал в ViralAir:

const ( GENERATOR = "1111111111111010000001001" ) func Crc(msg string, encode bool) string { msgbin := []rune(misc.Hex2bin(msg)) generator := []int{} for _, char := range GENERATOR { generator = append(generator, int(char-'0')) } if encode { for i := len(msgbin) - 24; i < len(msgbin); i++ { msgbin[i] = '0' } } for i := 0; i < len(msgbin)-24; i++ { if msgbin[i] == '1' { for j := range generator { msgbin[i+j] = rune('0' + (int(msgbin[i+j]-'0') ^ generator[j])) } } } reminder := string(msgbin[len(msgbin)-24:]) return reminder}

Ре­али­зация на Go похожа на наш псев­докод, так что раз­бирать под­робно ее не буду.

Собираем всё вместе

Те­перь, ког­да у нас есть все слу­жеб­ные фун­кции, мож­но соз­давать пакет! Начина­ется всё с кодиро­вания высоты, за это отве­чает фун­кция encodeAltModes().

// Encode altitude func encodeAltModes(alt float64, surface int) int { mbit := 0 qbit := 1 encalt := int((int(alt) + 1000) / 25) var tmp1, tmp2 int if surface == 1 { tmp1 = (encalt & 0xfe0) << 2 tmp2 = (encalt & 0x010) << 1 } else { tmp1 = (encalt & 0xff8) << 1 tmp2 = 0 } return (encalt & 0x0F) | tmp1 | tmp2 | (mbit << 6) | (qbit << 4)}

В зависи­мос­ти от высоты при­меня­ются раз­ные делите­ли. Для обыч­ных самоле­тов — 25, но есть и дру­гие, нап­ример 100. Они нуж­ны для самоле­тов, которые лета­ют выше, чем обыч­ные. За выбор делите­ля отве­чает параметр qbit. Смысл делите­ля — ука­зать, какого раз­мера взят интервал для высоты.

Пос­коль­ку ука­зывать высоту в нор­маль­ных величи­нах, а не в футах — это слиш­ком прос­то, авто­ры про­токо­ла при­дума­ли делить высоту на интерва­лы по N футов и ука­зывать номер такого интерва­ла, а N — это и есть наш делитель (сколь­ко футов в одном интерва­ле). Делитель в 25 футов для нашего самоле­та дает точ­ность око­ло 7,6 мет­ра.

По поводу самого qbit докумен­тация про­токо­ла говорит сле­дующее:

This field will contain barometric altitude encoded in 25 or 100-foot increments (as indicated by the Q Bit). All zeroes in this field will indicate that there is no altitude data.

То есть если qbit уста­новить в 0, то делитель ста­нет рав­ным 100, что быва­ет полез­но, если высота слиш­ком боль­шая и ее слож­но умес­тить в сооб­щение. В ViralAir пре­дус­мотрен толь­ко делитель 25, хотя добавить 100 мож­но доволь­но лег­ко — поп­робуй реали­зовать это сам на досуге!

Ре­зуль­тат работы фун­кции для высоты в 9999 футов ты можешь уви­деть ниже:

2024/01/15 03:40:37 [+] Encode altitude [9999.000000] with the surface flag [0]
2024/01/15 03:40:37 [+] Encoded altitude [0x377]

Сле­дующая задача — соз­дать два сооб­щения (even и odd), и нач­нем мы с even. Пер­вая часть любого сооб­щения ADS-B — это его тип. DF (тип сооб­щения Mode S) для ADS-B всег­да равен 17, так что это кон­стан­та в коде, а пос­ле него идут CA (уро­вень тран­спон­дера) и ICAO (номер самоле­та). CA и ICAO мож­но задать свои как аргу­мен­ты при запус­ке ViralAir.

// Format + CA + ICAO dataEven = append(dataEven, byte((format<<3)|ca)) dataEven = append(dataEven, byte((icao>>16)&0xff)) dataEven = append(dataEven, byte((icao>>8)&0xff)) dataEven = append(dataEven, byte((icao)&0xff))

Те­перь добавим к фрей­му дол­готу, широту и высоту:

// Even log.Printf("[+] Encode even frame with lat [%f] and lon [%f]", lat, lon) evenLat, evenLon := cpr.CprEncode(lat, lon, 0, surface) log.Printf("[+] Encoded even frame lat [0x%02x] and lon [0x%02x]", evenLat, evenLon) // Odd log.Printf("[+] Encode odd frame with lat [%f] and lon [%f]", lat, lon) oddLat, oddLon := cpr.CprEncode(lat, lon, 1, surface) log.Printf("[+] Encoded odd frame lat [0x%02x] and lon [0x%02x]", oddLat, oddLon)

При запус­ке прог­раммы закоди­рован­ные зна­чения выводят­ся в кон­соль:

./main -altitude 9999.0 -icao 0xDEADBE -latitude 11.33 -longitude 11.22
<...>
2024/01/15 03:40:37 [+] Encode even frame with lat [11.330000] and lon [11.220000]
2024/01/15 03:40:37 [+] Encoded even frame lat [0x1c6d4] and lon [0x19d86]
2024/01/15 03:40:37 [+] Encode odd frame with lat [11.330000] and lon [11.220000]
2024/01/15 03:40:37 [+] Encoded odd frame lat [0x1b6b6] and lon [0x18d91]
<...>

Бай­ты для обо­их пакетов готовы, добав­ляем их к нашему фрей­му:

// Lat + Lot + Alt (even) dataEven = append(dataEven, byte((tc<<3)|(ss<<1)|nicsb)) dataEven = append(dataEven, byte((encAlt>>4)&0xff)) dataEven = append(dataEven, byte((encAlt&0xf)<<4|(time<<3)|(ff<<2)|(evenLat>>15))) dataEven = append(dataEven, byte((evenLat>>7)&0xff)) dataEven = append(dataEven, byte(((evenLat&0x7f)<<1)|(evenLon>>16))) dataEven = append(dataEven, byte((evenLon>>8)&0xff)) dataEven = append(dataEven, byte((evenLon)&0xff))

Дан­ные фрей­ма готовы, оста­лось дописать к ним кон­троль­ную сум­му.

// Convert to hex var sbEven strings.Builderfor _, b := range dataEven[:11] { sbEven.WriteString(fmt.Sprintf("%02x", b))} dataEvenString := sbEven.String()log.Printf("[+] Even frame without CRC [%s]", dataEvenString) // Calculate CRC dataEvenCRC := misc.Bin2int(crc.Crc(dataEvenString+"000000", true)) log.Printf("[+] Even data CRC [%02x]", dataEvenCRC) // Append CRC dataEven = append(dataEven, byte((dataEvenCRC>>16)&0xff)) dataEven = append(dataEven, byte((dataEvenCRC>>8)&0xff)) dataEven = append(dataEven, byte((dataEvenCRC)&0xff))log.Printf("[+] Even data [%02x]", dataEven)

Пос­ле это­го в тер­минал вывалит­ся пол­ный фрейм Mode S, который мож­но исполь­зовать как угод­но: передать, декоди­ровать онлайн или даже рас­печатать и повесить на стен­ку.

2024/01/15 03:40:37 [+] Even data [8ddeadbe5837738da99d861b04b3]
2024/01/15 03:40:37 [+] Odd data [8ddeadbe5837776d6d8d9121b103]

От отправ­ки сиг­нала нас отде­лает все­го одна мелочь: нуж­но смо­дули­ровать наш сиг­нал.

ПЕРЕДАЧА

Модуляция

Что­бы передать наши бай­ты в эфир, нуж­но пре­обра­зовать их в фор­мат, который пой­мет HackRF. Фор­мат очень прос­той: нуж­но закоди­ровать все бай­ты в ком­плексные чис­ла и сох­ранить их в файл.

Для ADS-B при­меня­ется ман­честер­ское кодиро­вание, то есть еди­ница кодиру­ется как 01, а ноль как 10. Что­бы луч­ше это понять, пос­мотри на кар­тинку ниже.

Ко­диро­вание битов

То есть нуж­но дво­ичное пред­став­ление каж­дого бай­та закоди­ровать по опи­сан­ной выше схе­ме — и дело в шля­пе. Все это базовые опе­рации с чис­лами, которые мож­но лег­ко сде­лать на любом язы­ке. Ниже — моя реали­зация на Go.

func Frame1090esPpmModulate(even, odd []byte) []byte { var ppm []byte for i := 0; i < 48; i++ { ppm = append(ppm, 0) } ppm = append(ppm, 0xA1, 0x40) for _, byteVal := range even { word16 := misc.Packbits(manchesterEncode(^byteVal)) ppm = append(ppm, word16[0]) ppm = append(ppm, word16[1]) } for i := 0; i < 100; i++ { ppm = append(ppm, 0) } ppm = append(ppm, 0xA1, 0x40) for _, byteVal := range odd { word16 := misc.Packbits(manchesterEncode(^byteVal)) ppm = append(ppm, word16[0]) ppm = append(ppm, word16[1]) } for i := 0; i < 48; i++ { ppm = append(ppm, 0) } return ppm}

Нуж­но толь­ко не забыть о наличии пре­амбу­лы — спе­циаль­ной пос­ледова­тель­нос­ти битов в начале фрей­ма, по которой при­емник опре­деля­ет начало сооб­щения. В нашем слу­чае это 0xA1 0х40, что соот­ветс­тву­ет пре­амбу­ле Mode S.

По­лучен­ные биты нуж­но модули­ровать в PPM. Как работа­ет PPM и чем отли­чает­ся от дру­гих спо­собов модуля­ции, наг­лядно вид­но на кар­тинке ниже.

PPM-модуля­ция

Толь­ко вот кодиро­вать мы будем не ана­лого­вый сиг­нал, а уже готовый циф­ровой, то есть пред­став­лять наш сиг­нал в виде ком­плексных чисел. Углублять­ся во все их тон­кости мы сей­час не ста­нем, пос­коль­ку для наших задач нуж­но толь­ко два таких чис­ла, с которы­ми мы даже не будем про­изво­дить никаких рас­четов. Ком­плексное чис­ло сос­тоит из двух ком­понент (I и Q), которые для бита с высоким уров­нем мы уста­новим в мак­сималь­ное зна­чение, а для низ­кого — в минималь­ное:

func GenerateSDROutput(ppm []byte) []byte { bits := misc.Unpackbits(ppm) var signal []byte for _, bit := range bits { var I, Q byte if bit == 1 { I = byte(127) Q = byte(127) } else { I = 0 Q = 0 } signal = append(signal, I, Q) } return signal}

Те­перь записы­ваем резуль­тат в файл и получа­ем наш завет­ный Samples.iq8s.

В эфир

Ну вот, теперь можешь выдыхать с облегче­нием. Пос­ле сбор­ки тес­тового стен­да, генера­ции фай­ла с сиг­налом и нас­трой­ки все­го обо­рудо­вания мы наконец‑то можем передать дан­ные о нашем самоле­те самим себе!

Вклю­чаем при­ем, как опи­сано в начале, и переда­ем сиг­нал через HackRF:

~/P/ViralAir (main)> hackrf_transfer -t Samples.iq8s -f 1090000000 -s 2000000 -x 47 -R
call hackrf_set_sample_rate(2000000 Hz/2.000 MHz)
call hackrf_set_hw_sync_mode(0)
call hackrf_set_freq(1090000000 Hz/1090.000 MHz)
Stop with Ctrl-C
3.9 MiB / 1.000 sec = 3.9 MiB/second, average power -6.5 dBfs
3.9 MiB / 1.000 sec = 3.9 MiB/second, average power -6.5 dBfs
3.9 MiB / 1.000 sec = 3.9 MiB/second, average power -6.5 dBfs
4.2 MiB / 1.000 sec = 4.2 MiB/second, average power -6.5 dBfs

Ар­гумент -t — это файл с сиг­налом, который мы сге­нери­рова­ли; -f зада­ет час­тоту переда­чи (для ADS-B это 1090 МГц, или 1 090 000 000 Гц), а -s — час­тоту дис­кре­тиза­ции. Пос­ледние два аргу­мен­та отве­чают за мощ­ность переда­чи и ее зацик­ливание. Зачем зацик­ливать? Ведь у нас все­го два сооб­щения, и сра­зу пос­ле окон­чания переда­чи они про­падут из окна dump1090.

Да­вай теперь взгля­нем на при­емник!

Вы­вод dump1090

В таб­лице виден наш самолет DEADBE! Он летит на высоте 9975 футов (ска­зыва­ется неточ­ность кодиро­вания высоты), и его даже мож­но най­ти на кар­те.

Пря­мо сей­час он летит за тобой где‑то над Нигери­ей.