You are on page 1of 10

1 # Copyright (C) 2015 Junzi Sun (TU Delft)

2
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 """
17 A python package for decoding ABS-D messages.
18 """
19
20 import math
21
22 # the polynominal generattor code for CRC
23 GENERATOR = "1111111111111010000001001"
24
25
26 def hex2bin(hexstr):
27 """Convert a hexdecimal string to binary string, with zero fillings. """
28 scale = 16
29 num_of_bits = len(hexstr) * math.log(scale, 2)
30 binstr = bin(int(hexstr, scale))[2:].zfill(int(num_of_bits))
31 return binstr
32
33
34 def bin2int(binstr):
35 """Convert a binary string to integer. """
36 return int(binstr, 2)
37
38
39 def hex2int(hexstr):
40 """Convert a hexdecimal string to integer. """
41 return int(hexstr, 16)
42
43
44 def df(msg):
45 """Decode Downlink Format vaule, bits 1 to 5."""
46 msgbin = hex2bin(msg)
47 return bin2int(msgbin[0:5])
48
49
50 def crc(msg, encode=False):
51 """Mode-S Cyclic Redundancy Check
52 Detect if bit error occurs in the Mode-S message
53 Args:
54 msg (string): 28 bytes hexadecimal message string
55 encode (bool): True to encode the date only and return the checksum
56 Returns:
57 string: message checksum, or partity bits (encoder)
58 """
59
60 msgbin = list(hex2bin(msg))
61
62 if encode:
63 msgbin[-24:] = ['0'] * 24
64
65 # loop all bits, except last 24 piraty bits
66 for i in range(len(msgbin)-24):
67 # if 1, perform modulo 2 multiplication,
68 if msgbin[i] == '1':
69 for j in range(len(GENERATOR)):
70 # modulo 2 multiplication = XOR
71 msgbin[i+j] = str((int(msgbin[i+j]) ^ int(GENERATOR[j])))
72
73 # last 24 bits
74 reminder = ''.join(msgbin[-24:])
75 return reminder
76
77
78 def floor(x):
79 """ Mode-S floor function
80
81 Defined as the greatest integer value k, such that k <= x
82
83 eg.: floor(3.6) = 3, while floor(-3.6) = -4
84 """
85 return int(math.floor(x))
86
87 def df(msg):
88 """Get the downlink format (DF) number
89
90 Args:
91 msg (string): 28 bytes hexadecimal message string
92
93 Returns:
94 int: DF number
95 """
96 return df(msg)
97
98
99 def icao(msg):
100 """Get the ICAO 24 bits address, bytes 3 to 8.
101
102 Args:
103 msg (string): 28 bytes hexadecimal message string
104
105 Returns:
106 String: ICAO address in 6 bytes hexadecimal string
107 """
108 return msg[2:8]
109
110
111 def data(msg):
112 """Return the data frame in the message, bytes 9 to 22"""
113 return msg[8:22]
114
115
116 def typecode(msg):
117 """Type code of ADS-B message
118
119 Args:
120 msg (string): 28 bytes hexadecimal message string
121
122 Returns:
123 int: type code number
124 """
125 msgbin = hex2bin(msg)
126 return bin2int(msgbin[32:37])
127
128
129 # ---------------------------------------------
130 # Aircraft Identification
131 # ---------------------------------------------
132 def category(msg):
133 """Aircraft category number
134
135 Args:
136 msg (string): 28 bytes hexadecimal message string
137
138 Returns:
139 int: category number
140 """
141
142 if typecode(msg) < 1 or typecode(msg) > 4:
143 raise RuntimeError("%s: Not a identification message" % msg)
144 msgbin = hex2bin(msg)
145 return bin2int(msgbin[5:8])
146
147
148 def callsign(msg):
149 """Aircraft callsign
150
151 Args:
152 msg (string): 28 bytes hexadecimal message string
153
154 Returns:
155 string: callsign
156 """
157
158 if typecode(msg) < 1 or typecode(msg) > 4:
159 raise RuntimeError("%s: Not a identification message" % msg)
160
161 chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
162 msgbin = hex2bin(msg)
163 csbin = msgbin[40:96]
164
165 cs = ''
166 cs += chars[bin2int(csbin[0:6])]
167 cs += chars[bin2int(csbin[6:12])]
168 cs += chars[bin2int(csbin[12:18])]
169 cs += chars[bin2int(csbin[18:24])]
170 cs += chars[bin2int(csbin[24:30])]
171 cs += chars[bin2int(csbin[30:36])]
172 cs += chars[bin2int(csbin[36:42])]
173 cs += chars[bin2int(csbin[42:48])]
174
175 # clean string, remove spaces and marks, if any.
176 # cs = cs.replace('_', '')
177 cs = cs.replace('#', '')
178 return cs
179
180
181 # ---------------------------------------------
182 # Positions
183 # ---------------------------------------------
184
185 def oe_flag(msg):
186 """Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
187
188 Args:
189 msg (string): 28 bytes hexadecimal message string
190
191 Returns:
192 int: 0 or 1, for even or odd frame
193 """
194 if typecode(msg) < 5 or typecode(msg) > 18:
195 raise RuntimeError("%s: Not a position message" % msg)
196
197 msgbin = hex2bin(msg)
198 return int(msgbin[53])
199
200
201 def cprlat(msg):
202 """CPR encoded latitude
203
204 Args:
205 msg (string): 28 bytes hexadecimal message string
206
207 Returns:
208 int: encoded latitude
209 """
210 if typecode(msg) < 5 or typecode(msg) > 18:
211 raise RuntimeError("%s: Not a position message" % msg)
212
213 msgbin = hex2bin(msg)
214 return bin2int(msgbin[54:71])
215
216
217 def cprlon(msg):
218 """CPR encoded longitude
219
220 Args:
221 msg (string): 28 bytes hexadecimal message string
222
223 Returns:
224 int: encoded longitude
225 """
226 if typecode(msg) < 5 or typecode(msg) > 18:
227 raise RuntimeError("%s: Not a position message" % msg)
228
229 msgbin = hex2bin(msg)
230 return bin2int(msgbin[71:88])
231
232
233 def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
234 """Decode position from a pair of even and odd position message
235 (works with both airborne and surface position messages)
236
237 Args:
238 msg0 (string): even message (28 bytes hexadecimal string)
239 msg1 (string): odd message (28 bytes hexadecimal string)
240 t0 (int): timestamps for the even message
241 t1 (int): timestamps for the odd message
242
243 Returns:
244 (float, float): (latitude, longitude) of the aircraft
245 """
246 if (5 <= typecode(msg0) <= 8 and 5 <= typecode(msg1) <= 8):
247 if (not lat_ref) or (not lon_ref):
248 raise RuntimeError("Surface position encountered, a reference \
249 position lat/lon required. Location of \
250 receiver can be used.")
251 else:
252 return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
253
254 elif (9 <= typecode(msg0) <= 18 and 9 <= typecode(msg1) <= 18):
255 return airborne_position(msg0, msg1, t0, t1)
256
257 else:
258 raise RuntimeError("incorrect or inconsistant message types")
259
260
261 def airborne_position(msg0, msg1, t0, t1):
262 """Decode airborn position from a pair of even and odd position message
263
264 Args:
265 msg0 (string): even message (28 bytes hexadecimal string)
266 msg1 (string): odd message (28 bytes hexadecimal string)
267 t0 (int): timestamps for the even message
268 t1 (int): timestamps for the odd message
269
270 Returns:
271 (float, float): (latitude, longitude) of the aircraft
272 """
273
274 msgbin0 = hex2bin(msg0)
275 msgbin1 = hex2bin(msg1)
276
277 # 131072 is 2^17, since CPR lat and lon are 17 bits each.
278 cprlat_even = bin2int(msgbin0[54:71]) / 131072.0
279 cprlon_even = bin2int(msgbin0[71:88]) / 131072.0
280 cprlat_odd = bin2int(msgbin1[54:71]) / 131072.0
281 cprlon_odd = bin2int(msgbin1[71:88]) / 131072.0
282
283 air_d_lat_even = 360.0 / 60
284 air_d_lat_odd = 360.0 / 59
285
286 # compute latitude index 'j'
287 j = floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
288
289 lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
290 lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
291
292 if lat_even >= 270:
293 lat_even = lat_even - 360
294
295 if lat_odd >= 270:
296 lat_odd = lat_odd - 360
297
298 # check if both are in the same latidude zone, exit if not
299 if _cprNL(lat_even) != _cprNL(lat_odd):
300 return None
301
302 # compute ni, longitude index m, and longitude
303 if (t0 > t1):
304 lat = lat_even
305 nl = _cprNL(lat)
306 ni = max(_cprNL(lat)- 0, 1)
307 m = floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
308 lon = (360.0 / ni) * (m % ni + cprlon_even)
309 else:
310 lat = lat_odd
311 nl = _cprNL(lat)
312 ni = max(_cprNL(lat) - 1, 1)
313 m = floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
314 lon = (360.0 / ni) * (m % ni + cprlon_odd)
315
316 if lon > 180:
317 lon = lon - 360
318
319 return round(lat, 5), round(lon, 5)
320
321
322 def position_with_ref(msg, lat_ref, lon_ref):
323 """Decode position with only one message,
324 knowing reference nearby location, such as previously
325 calculated location, ground station, or airport location, etc.
326 Works with both airborne and surface position messages.
327 The reference position shall be with in 180NM (airborne) or 45NM (surface)
328 of the true position.
329
330 Args:
331 msg0 (string): even message (28 bytes hexadecimal string)
332 msg1 (string): odd message (28 bytes hexadecimal string)
333 t0 (int): timestamps for the even message
334 t1 (int): timestamps for the odd message
335
336 Returns:
337 (float, float): (latitude, longitude) of the aircraft
338 """
339 if 5 <= typecode(msg) <= 8:
340 return airborne_position_with_ref(msg, lat_ref, lon_ref)
341
342 elif 9 <= typecode(msg) <= 18:
343 return surface_position_with_ref(msg, lat_ref, lon_ref)
344
345 else:
346 raise RuntimeError("incorrect or inconsistant message types")
347
348
349 def airborne_position_with_ref(msg, lat_ref, lon_ref):
350 """Decode airborne position with only one message,
351 knowing reference nearby location, such as previously calculated location,
352 ground station, or airport location, etc. The reference position shall
353 be with in 180NM of the true position.
354
355 Args:
356 msg (string): even message (28 bytes hexadecimal string)
357 lat_ref: previous known latitude
358 lon_ref: previous known longitude
359
360 Returns:
361 (float, float): (latitude, longitude) of the aircraft
362 """
363
364 i = oe_flag(msg)
365 d_lat = 360.0/59 if i else 360.0/60
366
367 msgbin = hex2bin(msg)
368 cprlat = bin2int(msgbin[54:71]) / 131072.0
369 cprlon = bin2int(msgbin[71:88]) / 131072.0
370
371 j = floor(lat_ref / d_lat) \
372 + floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
373
374 lat = d_lat * (j + cprlat)
375
376 ni = _cprNL(lat) - i
377
378 if ni > 0:
379 d_lon = 360.0 / ni
380 else:
381 d_lon = 360.0
382
383 m = floor(lon_ref / d_lon) \
384 + floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
385
386 lon = d_lon * (m + cprlon)
387
388 return round(lat, 5), round(lon, 5)
389
390
391 def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
392 """Decode surface position from a pair of even and odd position message,
393 the lat/lon of receiver must be provided to yield the correct solution.
394
395 Args:
396 msg0 (string): even message (28 bytes hexadecimal string)
397 msg1 (string): odd message (28 bytes hexadecimal string)
398 t0 (int): timestamps for the even message
399 t1 (int): timestamps for the odd message
400 lat_ref (float): latitude of the receiver
401 lon_ref (float): longitude of the receiver
402
403 Returns:
404 (float, float): (latitude, longitude) of the aircraft
405 """
406
407 msgbin0 = hex2bin(msg0)
408 msgbin1 = hex2bin(msg1)
409
410 # 131072 is 2^17, since CPR lat and lon are 17 bits each.
411 cprlat_even = bin2int(msgbin0[54:71]) / 131072.0
412 cprlon_even = bin2int(msgbin0[71:88]) / 131072.0
413 cprlat_odd = bin2int(msgbin1[54:71]) / 131072.0
414 cprlon_odd = bin2int(msgbin1[71:88]) / 131072.0
415
416 air_d_lat_even = 90.0 / 60
417 air_d_lat_odd = 90.0 / 59
418
419 # compute latitude index 'j'
420 j = floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
421
422 # solution for north hemisphere
423 lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even))
424 lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
425
426 # solution for north hemisphere
427 lat_even_s = lat_even_n - 90.0
428 lat_odd_s = lat_odd_n - 90.0
429
430 # chose which solution corrispondes to receiver location
431 lat_even = lat_even_n if lat_ref > 0 else lat_even_s
432 lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s
433
434 # check if both are in the same latidude zone, rare but possible
435 if _cprNL(lat_even) != _cprNL(lat_odd):
436 return None
437
438 # compute ni, longitude index m, and longitude
439 if (t0 > t1):
440 lat = lat_even
441 nl = _cprNL(lat_even)
442 ni = max(_cprNL(lat_even) - 0, 1)
443 m = floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
444 lon = (90.0 / ni) * (m % ni + cprlon_even)
445 else:
446 lat = lat_odd
447 nl = _cprNL(lat_odd)
448 ni = max(_cprNL(lat_odd) - 1, 1)
449 m = floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
450 lon = (90.0 / ni) * (m % ni + cprlon_odd)
451
452 # four possible longitude solutions
453 lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
454
455 # the closest solution to receiver is the correct one
456 dls = [abs(lon_ref - l) for l in lons]
457 imin = min(range(4), key=dls.__getitem__)
458 lon = lons[imin]
459
460 return round(lat, 5), round(lon, 5)
461
462
463 def surface_position_with_ref(msg, lat_ref, lon_ref):
464 """Decode surface position with only one message,
465 knowing reference nearby location, such as previously calculated location,
466 ground station, or airport location, etc. The reference position shall
467 be with in 45NM of the true position.
468
469 Args:
470 msg (string): even message (28 bytes hexadecimal string)
471 lat_ref: previous known latitude
472 lon_ref: previous known longitude
473
474 Returns:
475 (float, float): (latitude, longitude) of the aircraft
476 """
477
478 i = oe_flag(msg)
479 d_lat = 90.0/59 if i else 90.0/60
480
481 msgbin = hex2bin(msg)
482 cprlat = bin2int(msgbin[54:71]) / 131072.0
483 cprlon = bin2int(msgbin[71:88]) / 131072.0
484
485 j = floor(lat_ref / d_lat) \
486 + floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
487
488 lat = d_lat * (j + cprlat)
489
490 ni = _cprNL(lat) - i
491
492 if ni > 0:
493 d_lon = 90.0 / ni
494 else:
495 d_lon = 90.0
496
497 m = floor(lon_ref / d_lon) \
498 + floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
499
500 lon = d_lon * (m + cprlon)
501
502 return round(lat, 5), round(lon, 5)
503
504
505 def _cprNL(lat):
506 """NL() function in CPR decoding
507 """
508 if lat == 0:
509 return 59
510
511 if lat == 87 or lat == -87:
512 return 2
513
514 if lat > 87 or lat < -87:
515 return 1
516
517 nz = 15
518 a = 1 - math.cos(math.pi / (2 * nz))
519 b = math.cos(math.pi / 180.0 * abs(lat)) ** 2
520 nl = 2 * math.pi / (math.acos(1 - a/b))
521 NL = floor(nl)
522 return NL
523
524
525 def altitude(msg):
526 """Decode aircraft altitude
527
528 Args:
529 msg (string): 28 bytes hexadecimal message string
530
531 Returns:
532 int: altitude in feet
533 """
534 if typecode(msg) < 9 or typecode(msg) > 18:
535 raise RuntimeError("%s: Not a position message" % msg)
536
537 msgbin = hex2bin(msg)
538 q = msgbin[47]
539 if q:
540 n = bin2int(msgbin[40:47]+msgbin[48:52])
541 alt = n * 25 - 1000
542 return alt
543 else:
544 return None
545
546
547 def nic(msg):
548 """Calculate NIC, navigation integrity category
549
550 Args:
551 msg (string): 28 bytes hexadecimal message string
552
553 Returns:
554 int: NIC number (from 0 to 11), -1 if not applicable
555 """
556 if typecode(msg) < 9 or typecode(msg) > 18:
557 raise RuntimeError("%s: Not a airborne position message" % msg)
558
559 msgbin = hex2bin(msg)
560 tc = typecode(msg)
561 nic_sup_b = bin2int(msgbin[39])
562
563 if tc in [0, 18, 22]:
564 nic = 0
565 elif tc == 17:
566 nic = 1
567 elif tc == 16:
568 if nic_sup_b:
569 nic = 3
570 else:
571 nic = 2
572 elif tc == 15:
573 nic = 4
574 elif tc == 14:
575 nic = 5
576 elif tc == 13:
577 nic = 6
578 elif tc == 12:
579 nic = 7
580 elif tc == 11:
581 if nic_sup_b:
582 nic = 9
583 else:
584 nic = 8
585 elif tc in [10, 21]:
586 nic = 10
587 elif tc in [9, 20]:
588 nic = 11
589 else:
590 nic = -1
591 return nic
592
593
594 # ---------------------------------------------
595 # Velocity
596 # ---------------------------------------------
597
598 def velocity(msg):
599 """Calculate the speed, heading, and vertical rate
600
601 Args:
602 msg (string): 28 bytes hexadecimal message string
603
604 Returns:
605 (int, float, int, string): speed (kt), heading (degree),
606 rate of climb/descend (ft/min), and speed type
607 ('GS' for ground speed, 'AS' for airspeed)
608 """
609
610 if typecode(msg) != 19:
611 raise RuntimeError("%s: Not a airborne velocity message" % msg)
612
613 msgbin = hex2bin(msg)
614
615 subtype = bin2int(msgbin[37:40])
616
617 if subtype in (1, 2):
618 v_ew_sign = bin2int(msgbin[45])
619 v_ew = bin2int(msgbin[46:56]) - 1 # east-west velocity
620
621 v_ns_sign = bin2int(msgbin[56])
622 v_ns = bin2int(msgbin[57:67]) - 1 # north-south velocity
623
624 v_we = -1*v_ew if v_ew_sign else v_ew
625 v_sn = -1*v_ns if v_ns_sign else v_ns
626
627 spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts
628
629 hdg = math.atan2(v_we, v_sn)
630 hdg = math.degrees(hdg) # convert to degrees
631 hdg = hdg if hdg >= 0 else hdg + 360 # no negative val
632
633 tag = 'GS'
634
635 else:
636 hdg = bin2int(msgbin[46:56]) / 1024.0 * 360.0
637 spd = bin2int(msgbin[57:67])
638
639 tag = 'AS'
640
641 vr_sign = bin2int(msgbin[68])
642 vr = (bin2int(msgbin[69:78]) - 1) * 64 # vertical rate, fpm
643 rocd = -1*vr if vr_sign else vr # rate of climb/descend
644
645 return int(spd), round(hdg, 1), int(rocd), tag
646
647
648 def speed_heading(msg):
649 """Get speed and heading only from the velocity message
650
651 Args:
652 msg (string): 28 bytes hexadecimal message string
653
654 Returns:
655 (int, float): speed (kt), heading (degree)
656 """
657 spd, hdg, rocd, tag = velocity(msg)
658 return spd, hdg
659