diff --git a/README.rst b/README.rst index 2fe6930..017a0d3 100644 --- a/README.rst +++ b/README.rst @@ -18,6 +18,7 @@ Python library for Mode-S message decoding. Support Downlink Formats (DF) are: - DF21: Squawk code - BDS 2,0 Aircraft identification - BDS 2,1 Aircraft and airline registration markings + - BDS 3,0 ACAS active resolution advisory - BDS 4,0 Selected vertical intention - BDS 4,4 Meteorological routine air report - BDS 5,0 Track and turn report @@ -30,6 +31,7 @@ http://adsb-decode-guide.readthedocs.io New features in v2.0 --------------------- +- New structure of the libraries - ADS-B and EHS data streaming - Active aircraft viewing (in terminal) - More advanced BDS identification in Enhanced Mode-S @@ -39,22 +41,16 @@ New features in v2.0 Source code ----------- Checkout and contribute to this open source project at: -https://github.com/junzis/pyModeS +https://github.com/junzis/pyModeS/tree/dev-2.0 API documentation at: http://pymodes.readthedocs.io +[To be updated] Install ------- -The easiest installation (stable version of 1.x) is to use pip: - -:: - - pip install pyModeS - - To install latest development version (dev-2.0) from the GitHub: :: @@ -76,13 +72,13 @@ Common functions: .. code:: python pms.df(msg) # Downlink Format + pms.icao(msg) # Infer the ICAO address from the message pms.crc(msg, encode=False) # Perform CRC or generate parity bit - pms.hex2bin(str) # Convert hexadecimal string to binary string - pms.bin2int(str) # Convert binary string to integer - pms.hex2int(str) # Convert hexadecimal string to integer - - pms.gray2int(str) # Convert grey code to interger + pms.hex2bin(str) # Convert hexadecimal string to binary string + pms.bin2int(str) # Convert binary string to integer + pms.hex2int(str) # Convert hexadecimal string to integer + pms.gray2int(str) # Convert grey code to interger Core functions for ADS-B decoding: @@ -96,7 +92,7 @@ Core functions for ADS-B decoding: # typecode 1-4 pms.adsb.callsign(msg) - # typecode 5-8 (surface) and 9-18 (airborne) + # typecode 5-8 (surface), 9-18 (airborne, barometric height), and 9-18 (airborne, GNSS height) pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None) pms.adsb.airborne_position(msg_even, msg_odd, t_even, t_odd) pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref) @@ -120,68 +116,74 @@ use `position_with_ref()` method to decode with only one position message messages. But the reference position shall be with in 180NM (airborne) or 45NM (surface) of the true position. -Core functions for ELS decoding: -******************************** + +Common Mode-S functions +************************ .. code:: python - pms.els.icao(msg) # ICAO address - pms.els.df4alt(msg) # Altitude from any DF4 message - pms.ehs.df5id(msg) # Squawk code from any DF5 message + pms.icao(msg) # Infer the ICAO address from the message + pms.bds.infer(msg) # Infer the Modes-S BDS code + pms.bds.is10(msg) # check if BDS is 1,0 explicitly + pms.bds.is17(msg) # check if BDS is 1,7 explicitly + pms.bds.is20(msg) # check if BDS is 2,0 explicitly + pms.bds.is30(msg) # check if BDS is 3,0 explicitly + pms.bds.is40(msg) # check if BDS is 4,0 explicitly + pms.bds.is44(msg) # check if BDS is 4,4 explicitly + pms.bds.is50(msg) # check if BDS is 5,0 explicitly + pms.bds.is60(msg) # check if BDS is 6,0 explicitly -Core functions for EHS decoding: -******************************** + # check if BDS is 5,0 or 6,0, give reference spd, trk, alt (from ADS-B) + pms.bds.is50or60(msg, spd_ref, trk_ref, alt_ref) + + +Mode-S elementary surveillance (ELS) +************************************* .. code:: python - pms.ehs.icao(msg) # ICAO address - pms.ehs.df20alt(msg) # Altitude from any DF20 message - pms.ehs.df21id(msg) # Squawk code from any DF21 message + pms.els.ovc10(msg) # overlay capability, BDS 1,0 + pms.els.cap17(msg) # GICB capability, BDS 1,7 + pms.els.cs20(msg) # callsign, BDS 2,0 - pms.ehs.BDS(msg) # Comm-B Data Selector Version - # for BDS version 2,0 - pms.ehs.isBDS20(msg) # Check if message is BDS 2,0 - pms.ehs.callsign(msg) # Aircraft callsign +Mode-S enhanced surveillance (EHS) +*********************************** + +.. code:: python # for BDS version 4,0 - pms.ehs.isBDS40(msg) # Check if message is BDS 4,0 pms.ehs.alt40mcp(msg) # MCP/FCU selected altitude (ft) pms.ehs.alt40fms(msg) # FMS selected altitude (ft) pms.ehs.p40baro(msg) # Barometric pressure (mb) - # for BDS version 4,4 - pms.ehs.isBDS44(msg, rev=False) # Check if message is BDS 4,4 - pms.ehs.wind44(msg, rev=False) # wind speed (kt) and heading (deg) - pms.ehs.temp44(msg, rev=False) # temperature (C) - pms.ehs.p44(msg, rev=False) # pressure (hPa) - pms.ehs.hum44(msg, rev=False) # humidity (%) - # for BDS version 5,0 - pms.ehs.isBDS50(msg) # Check if message is BDS 5,0 pms.ehs.roll50(msg) # roll angle (deg) pms.ehs.trk50(msg) # track angle (deg) pms.ehs.gs50(msg) # ground speed (kt) pms.ehs.rtrk50(msg) # track angle rate (deg/sec) pms.ehs.tas50(msg) # true airspeed (kt) - # for BDS version 5,3 - pms.ehs.isBDS53(msg) # Check if message is BDS 5,3 - pms.ehs.hdg53(msg) # magnetic heading (deg) - pms.ehs.ias53(msg) # indicated airspeed (kt) - pms.ehs.mach53(msg) # MACH number - pms.ehs.tas53(msg) # true airspeed (kt) - pms.ehs.vr53(msg) # vertical rate (fpm) - # for BDS version 6,0 - pms.ehs.isBDS60(msg) # Check if message is BDS 6,0 pms.ehs.hdg60(msg) # heading (deg) pms.ehs.ias60(msg) # indicated airspeed (kt) pms.ehs.mach60(msg) # MACH number pms.ehs.vr60baro(msg) # barometric altitude rate (ft/min) pms.ehs.vr60ins(msg) # inertial vertical speed (ft/min) + +Meteorological routine air report (MRAR) [Experimental] +******************************************************* + +.. code:: python + + # for BDS version 4,4 + pms.ehs.wind44(msg, rev=False) # wind speed (kt) and heading (deg) + pms.ehs.temp44(msg, rev=False) # temperature (C) + pms.ehs.p44(msg, rev=False) # pressure (hPa) + pms.ehs.hum44(msg, rev=False) # humidity (%) + Developement ------------ To perform unit tests. First install ``tox`` through pip, Then, run the following commands: diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py index b8a4cca..894af68 100644 --- a/pyModeS/__init__.py +++ b/pyModeS/__init__.py @@ -1,11 +1,10 @@ from __future__ import absolute_import, print_function, division -from .decoder.util import * +from .decoder.common import * from .decoder import adsb from .decoder import els from .decoder import ehs -from .decoder import util -from .decoder import modes +from .decoder import common from .decoder import bds from .extra import aero from .extra import beastclient diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index 6e0b706..d63ddff 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -14,159 +14,26 @@ # along with this program. If not, see . """ -A python package for decoding ABS-D messages. +The wrapper for decoding ADS-B messages """ from __future__ import absolute_import, print_function, division -import math -from . import util +from pyModeS.decoder import common +# from pyModeS.decoder.bds import bds05, bds06, bds09 +from pyModeS.decoder.bds.bds05 import airborne_position, airborne_position_with_ref, altitude +from pyModeS.decoder.bds.bds06 import surface_position, surface_position_with_ref, surface_velocity +from pyModeS.decoder.bds.bds08 import category, callsign +from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff def df(msg): - """Get the downlink format (DF) number - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - int: DF number - """ - return util.df(msg) - + return common.df(msg) def icao(msg): - """Get the ICAO 24 bits address, bytes 3 to 8. - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - String: ICAO address in 6 bytes hexadecimal string - """ - return msg[2:8] - - -def data(msg): - """Return the data frame in the message, bytes 9 to 22""" - return msg[8:22] - + return common.icao(msg) def typecode(msg): - """Type code of ADS-B message - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - int: type code number - """ - msgbin = util.hex2bin(msg) - return util.bin2int(msgbin[32:37]) - - -# --------------------------------------------- -# Aircraft Identification -# --------------------------------------------- -def category(msg): - """Aircraft category number - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - int: category number - """ - - if typecode(msg) < 1 or typecode(msg) > 4: - raise RuntimeError("%s: Not a identification message" % msg) - msgbin = util.hex2bin(msg) - return util.bin2int(msgbin[5:8]) - - -def callsign(msg): - """Aircraft callsign - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - string: callsign - """ - - if typecode(msg) < 1 or typecode(msg) > 4: - raise RuntimeError("%s: Not a identification message" % msg) - - chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######' - msgbin = util.hex2bin(msg) - csbin = msgbin[40:96] - - cs = '' - cs += chars[util.bin2int(csbin[0:6])] - cs += chars[util.bin2int(csbin[6:12])] - cs += chars[util.bin2int(csbin[12:18])] - cs += chars[util.bin2int(csbin[18:24])] - cs += chars[util.bin2int(csbin[24:30])] - cs += chars[util.bin2int(csbin[30:36])] - cs += chars[util.bin2int(csbin[36:42])] - cs += chars[util.bin2int(csbin[42:48])] - - # clean string, remove spaces and marks, if any. - # cs = cs.replace('_', '') - cs = cs.replace('#', '') - return cs - - -# --------------------------------------------- -# Positions -# --------------------------------------------- - -def oe_flag(msg): - """Check the odd/even flag. Bit 54, 0 for even, 1 for odd. - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - int: 0 or 1, for even or odd frame - """ - if typecode(msg) < 5 or typecode(msg) > 18: - raise RuntimeError("%s: Not a position message" % msg) - - msgbin = util.hex2bin(msg) - return int(msgbin[53]) - - -def cprlat(msg): - """CPR encoded latitude - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - int: encoded latitude - """ - if typecode(msg) < 5 or typecode(msg) > 18: - raise RuntimeError("%s: Not a position message" % msg) - - msgbin = util.hex2bin(msg) - return util.bin2int(msgbin[54:71]) - - -def cprlon(msg): - """CPR encoded longitude - - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - int: encoded longitude - """ - if typecode(msg) < 5 or typecode(msg) > 18: - raise RuntimeError("%s: Not a position message" % msg) - - msgbin = util.hex2bin(msg) - return util.bin2int(msgbin[71:88]) - + return common.typecode(msg) def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None): """Decode position from a pair of even and odd position message @@ -181,7 +48,10 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None): Returns: (float, float): (latitude, longitude) of the aircraft """ - if (5 <= typecode(msg0) <= 8 and 5 <= typecode(msg1) <= 8): + tc0 = typecode(msg0) + tc1 = typecode(msg1) + + if (5<=tc0<=8 and 5<=tc1<=8): if (not lat_ref) or (not lon_ref): raise RuntimeError("Surface position encountered, a reference \ position lat/lon required. Location of \ @@ -189,72 +59,16 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None): else: return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref) - elif (9 <= typecode(msg0) <= 18 and 9 <= typecode(msg1) <= 18): + elif (9<=tc0<=18 and 9<=tc1<=18): + # Airborne position with barometric height return airborne_position(msg0, msg1, t0, t1) - else: - raise RuntimeError("incorrect or inconsistant message types") - - -def airborne_position(msg0, msg1, t0, t1): - """Decode airborn position from a pair of even and odd position message - - Args: - msg0 (string): even message (28 bytes hexadecimal string) - msg1 (string): odd message (28 bytes hexadecimal string) - t0 (int): timestamps for the even message - t1 (int): timestamps for the odd message - - Returns: - (float, float): (latitude, longitude) of the aircraft - """ - - msgbin0 = util.hex2bin(msg0) - msgbin1 = util.hex2bin(msg1) - - # 131072 is 2^17, since CPR lat and lon are 17 bits each. - cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0 - cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0 - cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0 - cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0 - - air_d_lat_even = 360.0 / 60 - air_d_lat_odd = 360.0 / 59 - - # compute latitude index 'j' - j = util.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5) - - 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 - - # check if both are in the same latidude zone, exit if not - if _cprNL(lat_even) != _cprNL(lat_odd): - return None + elif (20<=tc0<=22 and 20<=tc1<=22): + # Airborne position with GNSS height + return airborne_position(msg0, msg1, t0, t1) - # compute ni, longitude index m, and longitude - if (t0 > t1): - lat = lat_even - nl = _cprNL(lat) - ni = max(_cprNL(lat)- 0, 1) - m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) - lon = (360.0 / ni) * (m % ni + cprlon_even) else: - lat = lat_odd - nl = _cprNL(lat) - ni = max(_cprNL(lat) - 1, 1) - m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) - lon = (360.0 / ni) * (m % ni + cprlon_odd) - - if lon > 180: - lon = lon - 360 - - return round(lat, 5), round(lon, 5) + raise RuntimeError("incorrect or inconsistant message types") def position_with_ref(msg, lat_ref, lon_ref): @@ -273,217 +87,83 @@ def position_with_ref(msg, lat_ref, lon_ref): Returns: (float, float): (latitude, longitude) of the aircraft """ - if 5 <= typecode(msg) <= 8: + + tc = typecode(msg) + + if 5<=tc<=8: return surface_position_with_ref(msg, lat_ref, lon_ref) - elif 9 <= typecode(msg) <= 18: + elif 9<=tc<=18 or 20<=tc<=22: return airborne_position_with_ref(msg, lat_ref, lon_ref) else: raise RuntimeError("incorrect or inconsistant message types") -def airborne_position_with_ref(msg, lat_ref, lon_ref): - """Decode airborne position with only one message, - knowing reference nearby location, such as previously calculated location, - ground station, or airport location, etc. The reference position shall - be with in 180NM of the true position. +def altitude(msg): + """Decode aircraft altitude Args: - msg (string): even message (28 bytes hexadecimal string) - lat_ref: previous known latitude - lon_ref: previous known longitude + msg (string): 28 bytes hexadecimal message string Returns: - (float, float): (latitude, longitude) of the aircraft + int: altitude in feet """ - i = oe_flag(msg) - d_lat = 360.0/59 if i else 360.0/60 - - msgbin = util.hex2bin(msg) - cprlat = util.bin2int(msgbin[54:71]) / 131072.0 - cprlon = util.bin2int(msgbin[71:88]) / 131072.0 - - j = util.floor(lat_ref / d_lat) \ - + util.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat) + tc = typecode(msg) - lat = d_lat * (j + cprlat) + if tc<5 or tc==19 or tc>22: + raise RuntimeError("%s: Not a position message" % msg) - ni = _cprNL(lat) - i + if tc>=5 and tc<=8: + # surface position, altitude 0 + return 0 - if ni > 0: - d_lon = 360.0 / ni + msgbin = common.hex2bin(msg) + q = msgbin[47] + if q: + n = common.bin2int(msgbin[40:47]+msgbin[48:52]) + alt = n * 25 - 1000 + return alt else: - d_lon = 360.0 - - m = util.floor(lon_ref / d_lon) \ - + util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon) - - lon = d_lon * (m + cprlon) - - return round(lat, 5), round(lon, 5) - - -def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref): - """Decode surface position from a pair of even and odd position message, - the lat/lon of receiver must be provided to yield the correct solution. - - Args: - msg0 (string): even message (28 bytes hexadecimal string) - msg1 (string): odd message (28 bytes hexadecimal string) - t0 (int): timestamps for the even message - t1 (int): timestamps for the odd message - lat_ref (float): latitude of the receiver - lon_ref (float): longitude of the receiver - - Returns: - (float, float): (latitude, longitude) of the aircraft - """ - - msgbin0 = util.hex2bin(msg0) - msgbin1 = util.hex2bin(msg1) - - # 131072 is 2^17, since CPR lat and lon are 17 bits each. - cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0 - cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0 - cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0 - cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0 - - air_d_lat_even = 90.0 / 60 - air_d_lat_odd = 90.0 / 59 - - # compute latitude index 'j' - j = util.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5) - - # solution for north hemisphere - lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even)) - lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd)) - - # solution for north hemisphere - lat_even_s = lat_even_n - 90.0 - lat_odd_s = lat_odd_n - 90.0 - - # chose which solution corrispondes to receiver location - lat_even = lat_even_n if lat_ref > 0 else lat_even_s - lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s - - # check if both are in the same latidude zone, rare but possible - if _cprNL(lat_even) != _cprNL(lat_odd): return None - # compute ni, longitude index m, and longitude - if (t0 > t1): - lat = lat_even - nl = _cprNL(lat_even) - ni = max(_cprNL(lat_even) - 0, 1) - m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) - lon = (90.0 / ni) * (m % ni + cprlon_even) - else: - lat = lat_odd - nl = _cprNL(lat_odd) - ni = max(_cprNL(lat_odd) - 1, 1) - m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) - lon = (90.0 / ni) * (m % ni + cprlon_odd) - - # four possible longitude solutions - lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0] - - # the closest solution to receiver is the correct one - dls = [abs(lon_ref - l) for l in lons] - imin = min(range(4), key=dls.__getitem__) - lon = lons[imin] - - return round(lat, 5), round(lon, 5) - -def surface_position_with_ref(msg, lat_ref, lon_ref): - """Decode surface position with only one message, - knowing reference nearby location, such as previously calculated location, - ground station, or airport location, etc. The reference position shall - be with in 45NM of the true position. +def velocity(msg): + """Calculate the speed, heading, and vertical rate + (handles both airborne or surface message) Args: - msg (string): even message (28 bytes hexadecimal string) - lat_ref: previous known latitude - lon_ref: previous known longitude + msg (string): 28 bytes hexadecimal message string Returns: - (float, float): (latitude, longitude) of the aircraft + (int, float, int, string): speed (kt), ground track or heading (degree), + rate of climb/descend (ft/min), and speed type + ('GS' for ground speed, 'AS' for airspeed) """ - i = oe_flag(msg) - d_lat = 90.0/59 if i else 90.0/60 - - msgbin = util.hex2bin(msg) - cprlat = util.bin2int(msgbin[54:71]) / 131072.0 - cprlon = util.bin2int(msgbin[71:88]) / 131072.0 - - j = util.floor(lat_ref / d_lat) \ - + util.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat) - - lat = d_lat * (j + cprlat) + if 5 <= typecode(msg) <= 8: + return surface_velocity(msg) - ni = _cprNL(lat) - i + elif typecode(msg) == 19: + return airborne_velocity(msg) - if ni > 0: - d_lon = 90.0 / ni else: - d_lon = 90.0 - - m = util.floor(lon_ref / d_lon) \ - + util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon) - - lon = d_lon * (m + cprlon) - - return round(lat, 5), round(lon, 5) - - -def _cprNL(lat): - """NL() function in CPR decoding - """ - if lat == 0: - return 59 - - if lat == 87 or lat == -87: - return 2 - - if lat > 87 or lat < -87: - return 1 - - 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)) - NL = util.floor(nl) - return NL + raise RuntimeError("incorrect or inconsistant message types, expecting 4 18: - raise RuntimeError("%s: Not a position message" % msg) - - if typecode(msg) >=5 and typecode(msg) <=8: - # surface position, altitude 0 - return 0 - - msgbin = util.hex2bin(msg) - q = msgbin[47] - if q: - n = util.bin2int(msgbin[40:47]+msgbin[48:52]) - alt = n * 25 - 1000 - return alt - else: - return None + spd, trk_or_hdg, rocd, tag = velocity(msg) + return spd, trk_or_hdg def nic(msg): @@ -498,9 +178,9 @@ def nic(msg): if typecode(msg) < 9 or typecode(msg) > 18: raise RuntimeError("%s: Not a airborne position message, expecting 8= 0 else trk + 360 # no negative val - - tag = 'GS' - trk_or_hdg = trk - - else: - hdg = util.bin2int(msgbin[46:56]) / 1024.0 * 360.0 - spd = util.bin2int(msgbin[57:67]) - - tag = 'AS' - trk_or_hdg = hdg - - vr_sign = -1 if int(msgbin[68]) else 1 - vr = (util.bin2int(msgbin[69:78]) - 1) * 64 # vertical rate, fpm - rocd = vr_sign * vr - - return int(spd), round(trk_or_hdg, 1), int(rocd), tag - - -def surface_velocity(msg): - """Decode surface velocity from from a surface position message - Args: - msg (string): 28 bytes hexadecimal message string - - Returns: - (int, float, int, string): speed (kt), ground track (degree), - rate of climb/descend (ft/min), and speed type - ('GS' for ground speed, 'AS' for airspeed) - """ - - if typecode(msg) < 5 or typecode(msg) > 8: - raise RuntimeError("%s: Not a surface message, expecting 5 124: - spd = None - elif mov == 1: - spd = 0 - elif mov == 124: - spd = 175 - else: - movs = [2, 9, 13, 39, 94, 109, 124] - kts = [0.125, 1, 2, 15, 70, 100, 175] - i = next(m[0] for m in enumerate(movs) if m[1] > mov) - step = (kts[i] - kts[i-1]) * 1.0 / (movs[i]-movs[i-1]) - spd = kts[i-1] + (mov-movs[i-1]) * step - spd = round(spd, 2) - - return spd, trk, 0, 'GS' - -def altitude_diff(msg): - """Decode the differece between GNSS and barometric altitude - - Args: - msg (string): 28 bytes hexadecimal message string, TC=19 - - Returns: - int: Altitude difference in ft. Negative value indicates GNSS altitude - below barometric altitude. - """ - - if typecode(msg) != 19: - raise RuntimeError("incorrect message types, expecting TC=19") - - msgbin = util.hex2bin(msg) - sign = -1 if int(msgbin[80]) else 1 - value = util.bin2int(msgbin[81:88]) - - if value == 0 or value == 127: - return None - else: - return sign * (value - 1) * 25 # in ft. diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py index bf10e0a..257debd 100644 --- a/pyModeS/decoder/bds/__init__.py +++ b/pyModeS/decoder/bds/__init__.py @@ -1,15 +1,35 @@ -from __future__ import absolute_import, print_function, division +# Copyright (C) 2015 Junzi Sun (TU Delft) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Common functions for Mode-S decoding +""" + +from __future__ import absolute_import, print_function, division import numpy as np + from pyModeS.extra import aero -from pyModeS.decoder.modes import allzeros -from pyModeS.decoder.bds.bds10 import isBDS10 -from pyModeS.decoder.bds.bds17 import isBDS17 -from pyModeS.decoder.bds.bds20 import isBDS20 -from pyModeS.decoder.bds.bds30 import isBDS30 -from pyModeS.decoder.bds.bds40 import isBDS40 -from pyModeS.decoder.bds.bds50 import isBDS50, trk50, gs50 -from pyModeS.decoder.bds.bds60 import isBDS60, hdg60, mach60, ias60 +from pyModeS.decoder.common import allzeros +from pyModeS.decoder.bds.bds10 import is10 +from pyModeS.decoder.bds.bds17 import is17 +from pyModeS.decoder.bds.bds20 import is20 +from pyModeS.decoder.bds.bds30 import is30 +from pyModeS.decoder.bds.bds40 import is40 +from pyModeS.decoder.bds.bds50 import is50, trk50, gs50 +from pyModeS.decoder.bds.bds60 import is60, hdg60, mach60, ias60 def is50or60(msg, spd_ref, trk_ref, alt_ref): @@ -29,7 +49,7 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref): vy = v * np.cos(np.deg2rad(angle)) return vx, vy - if not (isBDS50(msg) and isBDS60(msg)): + if not (is50(msg) and is60(msg)): return None h50 = trk50(msg) @@ -79,21 +99,21 @@ def infer(msg): if allzeros(msg): return None - is10 = isBDS10(msg) - is17 = isBDS17(msg) - is20 = isBDS20(msg) - is30 = isBDS30(msg) - is40 = isBDS40(msg) - is50 = isBDS50(msg) - is60 = isBDS60(msg) + IS10 = is10(msg) + IS17 = is17(msg) + IS20 = is20(msg) + IS30 = is30(msg) + IS40 = is40(msg) + IS50 = is50(msg) + IS60 = is60(msg) allbds = np.array([ "BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60" ]) - isBDS = [is10, is17, is20, is30, is40, is50, is60] + mask = [IS10, IS17, IS20, IS30, IS40, IS50, IS60] - bds = ','.join(sorted(allbds[isBDS])) + bds = ','.join(sorted(allbds[mask])) if len(bds) == 0: return None diff --git a/pyModeS/decoder/bds/bds05.py b/pyModeS/decoder/bds/bds05.py new file mode 100644 index 0000000..f175843 --- /dev/null +++ b/pyModeS/decoder/bds/bds05.py @@ -0,0 +1,162 @@ +# Copyright (C) 2018 Junzi Sun (TU Delft) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +------------------------------------------ + BDS 0,5 + ADS-B TC=9-18 + Airborn position +------------------------------------------ +""" + +from __future__ import absolute_import, print_function, division +from pyModeS.decoder import common + +def airborne_position(msg0, msg1, t0, t1): + """Decode airborn position from a pair of even and odd position message + + Args: + msg0 (string): even message (28 bytes hexadecimal string) + msg1 (string): odd message (28 bytes hexadecimal string) + t0 (int): timestamps for the even message + t1 (int): timestamps for the odd message + + Returns: + (float, float): (latitude, longitude) of the aircraft + """ + + mb0 = common.hex2bin(msg0)[32:] + mb1 = common.hex2bin(msg1)[32:] + + # 131072 is 2^17, since CPR lat and lon are 17 bits each. + cprlat_even = common.bin2int(mb0[22:39]) / 131072.0 + cprlon_even = common.bin2int(mb0[39:56]) / 131072.0 + cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0 + cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0 + + air_d_lat_even = 360.0 / 60 + air_d_lat_odd = 360.0 / 59 + + # compute latitude index 'j' + j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5) + + 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 + + # check if both are in the same latidude zone, exit if not + if common.cprNL(lat_even) != common.cprNL(lat_odd): + return None + + # compute ni, longitude index m, and longitude + if (t0 > t1): + lat = lat_even + nl = common.cprNL(lat) + ni = max(common.cprNL(lat)- 0, 1) + m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) + lon = (360.0 / ni) * (m % ni + cprlon_even) + else: + lat = lat_odd + nl = common.cprNL(lat) + ni = max(common.cprNL(lat) - 1, 1) + m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) + lon = (360.0 / ni) * (m % ni + cprlon_odd) + + if lon > 180: + lon = lon - 360 + + return round(lat, 5), round(lon, 5) + + +def airborne_position_with_ref(msg, lat_ref, lon_ref): + """Decode airborne position with only one message, + knowing reference nearby location, such as previously calculated location, + ground station, or airport location, etc. The reference position shall + be with in 180NM of the true position. + + Args: + msg (string): even message (28 bytes hexadecimal string) + lat_ref: previous known latitude + lon_ref: previous known longitude + + Returns: + (float, float): (latitude, longitude) of the aircraft + """ + + + mb = common.hex2bin(msg)[32:] + + cprlat = common.bin2int(mb[22:39]) / 131072.0 + cprlon = common.bin2int(mb[39:56]) / 131072.0 + + i = int(mb[21]) + d_lat = 360.0/59 if i else 360.0/60 + + j = common.floor(lat_ref / d_lat) \ + + common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat) + + lat = d_lat * (j + cprlat) + + ni = common.cprNL(lat) - i + + if ni > 0: + d_lon = 360.0 / ni + else: + d_lon = 360.0 + + m = common.floor(lon_ref / d_lon) \ + + common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon) + + lon = d_lon * (m + cprlon) + + return round(lat, 5), round(lon, 5) + + +def altitude(msg): + """Decode aircraft altitude + + Args: + msg (string): 28 bytes hexadecimal message string + + Returns: + int: altitude in feet + """ + + tc = common.typecode(msg) + + if tc<9 or tc==19 or tc>22: + raise RuntimeError("%s: Not a airborn position message" % msg) + + mb = common.hex2bin(msg)[32:] + + if tc < 19: + # barometric altitude + q = mb[15] + if q: + n = common.bin2int(mb[8:15]+mb[16:20]) + alt = n * 25 - 1000 + else: + alt = None + else: + # GNSS altitude, meters -> feet + alt = common.bin2int(mb[8:20]) * 3.28084 + + return alt diff --git a/pyModeS/decoder/bds/bds06.py b/pyModeS/decoder/bds/bds06.py new file mode 100644 index 0000000..0c43640 --- /dev/null +++ b/pyModeS/decoder/bds/bds06.py @@ -0,0 +1,187 @@ +# Copyright (C) 2018 Junzi Sun (TU Delft) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +------------------------------------------ + BDS 0,6 + ADS-B TC=5-8 + Surface position +------------------------------------------ +""" + +from __future__ import absolute_import, print_function, division +from pyModeS.decoder import common +import math + + +def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref): + """Decode surface position from a pair of even and odd position message, + the lat/lon of receiver must be provided to yield the correct solution. + + Args: + msg0 (string): even message (28 bytes hexadecimal string) + msg1 (string): odd message (28 bytes hexadecimal string) + t0 (int): timestamps for the even message + t1 (int): timestamps for the odd message + lat_ref (float): latitude of the receiver + lon_ref (float): longitude of the receiver + + Returns: + (float, float): (latitude, longitude) of the aircraft + """ + + msgbin0 = common.hex2bin(msg0) + msgbin1 = common.hex2bin(msg1) + + # 131072 is 2^17, since CPR lat and lon are 17 bits each. + cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0 + cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0 + cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0 + cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0 + + air_d_lat_even = 90.0 / 60 + air_d_lat_odd = 90.0 / 59 + + # compute latitude index 'j' + j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5) + + # solution for north hemisphere + lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even)) + lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd)) + + # solution for north hemisphere + lat_even_s = lat_even_n - 90.0 + lat_odd_s = lat_odd_n - 90.0 + + # chose which solution corrispondes to receiver location + lat_even = lat_even_n if lat_ref > 0 else lat_even_s + lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s + + # check if both are in the same latidude zone, rare but possible + if common.cprNL(lat_even) != common.cprNL(lat_odd): + return None + + # compute ni, longitude index m, and longitude + if (t0 > t1): + lat = lat_even + nl = common.cprNL(lat_even) + ni = max(common.cprNL(lat_even) - 0, 1) + m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) + lon = (90.0 / ni) * (m % ni + cprlon_even) + else: + lat = lat_odd + nl = common.cprNL(lat_odd) + ni = max(common.cprNL(lat_odd) - 1, 1) + m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) + lon = (90.0 / ni) * (m % ni + cprlon_odd) + + # four possible longitude solutions + lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0] + + # the closest solution to receiver is the correct one + dls = [abs(lon_ref - l) for l in lons] + imin = min(range(4), key=dls.__getitem__) + lon = lons[imin] + + return round(lat, 5), round(lon, 5) + + +def surface_position_with_ref(msg, lat_ref, lon_ref): + """Decode surface position with only one message, + knowing reference nearby location, such as previously calculated location, + ground station, or airport location, etc. The reference position shall + be with in 45NM of the true position. + + Args: + msg (string): even message (28 bytes hexadecimal string) + lat_ref: previous known latitude + lon_ref: previous known longitude + + Returns: + (float, float): (latitude, longitude) of the aircraft + """ + + + mb = common.hex2bin(msg)[32:] + + cprlat = common.bin2int(mb[22:39]) / 131072.0 + cprlon = common.bin2int(mb[39:56]) / 131072.0 + + i = int(mb[21]) + d_lat = 90.0/59 if i else 90.0/60 + + j = common.floor(lat_ref / d_lat) \ + + common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat) + + lat = d_lat * (j + cprlat) + + ni = common.cprNL(lat) - i + + if ni > 0: + d_lon = 90.0 / ni + else: + d_lon = 90.0 + + m = common.floor(lon_ref / d_lon) \ + + common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon) + + lon = d_lon * (m + cprlon) + + return round(lat, 5), round(lon, 5) + + +def surface_velocity(msg): + """Decode surface velocity from from a surface position message + Args: + msg (string): 28 bytes hexadecimal message string + + Returns: + (int, float, int, string): speed (kt), ground track (degree), + rate of climb/descend (ft/min), and speed type + ('GS' for ground speed, 'AS' for airspeed) + """ + + if common.typecode(msg) < 5 or common.typecode(msg) > 8: + raise RuntimeError("%s: Not a surface message, expecting 5 124: + spd = None + elif mov == 1: + spd = 0 + elif mov == 124: + spd = 175 + else: + movs = [2, 9, 13, 39, 94, 109, 124] + kts = [0.125, 1, 2, 15, 70, 100, 175] + i = next(m[0] for m in enumerate(movs) if m[1] > mov) + step = (kts[i] - kts[i-1]) * 1.0 / (movs[i]-movs[i-1]) + spd = kts[i-1] + (mov-movs[i-1]) * step + spd = round(spd, 2) + + return spd, trk, 0, 'GS' diff --git a/pyModeS/decoder/bds/bds08.py b/pyModeS/decoder/bds/bds08.py new file mode 100644 index 0000000..56cb30d --- /dev/null +++ b/pyModeS/decoder/bds/bds08.py @@ -0,0 +1,75 @@ +# Copyright (C) 2018 Junzi Sun (TU Delft) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +------------------------------------------ + BDS 0,8 + ADS-B TC=1-4 + Aircraft identitification and category +------------------------------------------ +""" + +from __future__ import absolute_import, print_function, division +from pyModeS.decoder import common + +def category(msg): + """Aircraft category number + + Args: + msg (string): 28 bytes hexadecimal message string + + Returns: + int: category number + """ + + if common.typecode(msg) < 1 or common.typecode(msg) > 4: + raise RuntimeError("%s: Not a identification message" % msg) + + msgbin = common.hex2bin(msg) + return common.bin2int(msgbin[5:8]) + + +def callsign(msg): + """Aircraft callsign + + Args: + msg (string): 28 bytes hexadecimal message string + + Returns: + string: callsign + """ + + if common.typecode(msg) < 1 or common.typecode(msg) > 4: + raise RuntimeError("%s: Not a identification message" % msg) + + chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######' + msgbin = common.hex2bin(msg) + csbin = msgbin[40:96] + + cs = '' + cs += chars[common.bin2int(csbin[0:6])] + cs += chars[common.bin2int(csbin[6:12])] + cs += chars[common.bin2int(csbin[12:18])] + cs += chars[common.bin2int(csbin[18:24])] + cs += chars[common.bin2int(csbin[24:30])] + cs += chars[common.bin2int(csbin[30:36])] + cs += chars[common.bin2int(csbin[36:42])] + cs += chars[common.bin2int(csbin[42:48])] + + # clean string, remove spaces and marks, if any. + # cs = cs.replace('_', '') + cs = cs.replace('#', '') + return cs diff --git a/pyModeS/decoder/bds/bds09.py b/pyModeS/decoder/bds/bds09.py new file mode 100644 index 0000000..38cd5e6 --- /dev/null +++ b/pyModeS/decoder/bds/bds09.py @@ -0,0 +1,116 @@ +# Copyright (C) 2018 Junzi Sun (TU Delft) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +------------------------------------------ + BDS 0,9 + ADS-B TC=19 + Aircraft Airborn velocity +------------------------------------------ +""" + +from __future__ import absolute_import, print_function, division +from pyModeS.decoder import common +import math + + +def airborne_velocity(msg): + """Calculate the speed, track (or heading), and vertical rate + + Args: + msg (string): 28 bytes hexadecimal message string + + Returns: + (int, float, int, string): speed (kt), ground track or heading (degree), + rate of climb/descend (ft/min), and speed type + ('GS' for ground speed, 'AS' for airspeed) + """ + + if common.typecode(msg) != 19: + raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg) + + mb = common.hex2bin(msg)[32:] + + subtype = common.bin2int(mb[5:8]) + + if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0: + return None + + if subtype in (1, 2): + v_ew_sign = -1 if mb[13]=='1' else 1 + v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity + + v_ns_sign = -1 if mb[24]=='1' else 1 + v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity + + + v_we = v_ew_sign * v_ew + v_sn = v_ns_sign * v_ns + + spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts + + trk = math.atan2(v_we, v_sn) + trk = math.degrees(trk) # convert to degrees + trk = trk if trk >= 0 else trk + 360 # no negative val + + tag = 'GS' + trk_or_hdg = trk + + else: + if mb[13] == '0': + hdg = None + else: + hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0 + + trk_or_hdg = hdg + + spd = common.bin2int(mb[25:35]) + spd = None if spd==0 else spd-1 + + if mb[24]=='0': + tag = 'IAS' + else: + tag = 'TAS' + + vr_sign = -1 if mb[36]=='1' else 1 + vr = common.bin2int(mb[37:46]) + rocd = None if vr==0 else vr_sign*(vr-1)*64 + + return int(spd), round(trk_or_hdg, 2), int(rocd), tag + +def altitude_diff(msg): + """Decode the differece between GNSS and barometric altitude + + Args: + msg (string): 28 bytes hexadecimal message string, TC=19 + + Returns: + int: Altitude difference in ft. Negative value indicates GNSS altitude + below barometric altitude. + """ + tc = common.typecode(msg) + + if tc != 19: + raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg) + + msgbin = common.hex2bin(msg) + sign = -1 if int(msgbin[80]) else 1 + value = common.bin2int(msgbin[81:88]) + + if value == 0 or value == 127: + return None + else: + return sign * (value - 1) * 25 # in ft. diff --git a/pyModeS/decoder/bds/bds10.py b/pyModeS/decoder/bds/bds10.py index 26b28bf..9317902 100644 --- a/pyModeS/decoder/bds/bds10.py +++ b/pyModeS/decoder/bds/bds10.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros # ------------------------------------------ # BDS 1,0 # Data link capability report # ------------------------------------------ -def isBDS10(msg): +def is10(msg): """Check if a message is likely to be BDS code 1,0 Args: diff --git a/pyModeS/decoder/bds/bds17.py b/pyModeS/decoder/bds/bds17.py index 5c6c5dd..3d1e2a9 100644 --- a/pyModeS/decoder/bds/bds17.py +++ b/pyModeS/decoder/bds/bds17.py @@ -15,15 +15,16 @@ from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros -# ------------------------------------------ -# BDS 1,7 -# Common usage GICB capability report -# ------------------------------------------ +""" +------------------------------------------ + BDS 1,7 + Common usage GICB capability report +------------------------------------------ +""" -def isBDS17(msg): +def is17(msg): """Check if a message is likely to be BDS code 1,7 Args: diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py index 7d40a55..4a63844 100644 --- a/pyModeS/decoder/bds/bds20.py +++ b/pyModeS/decoder/bds/bds20.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros # ------------------------------------------ # BDS 2,0 # Aircraft identification # ------------------------------------------ -def isBDS20(msg): +def is20(msg): """Check if a message is likely to be BDS code 2,0 Args: @@ -42,7 +41,7 @@ def isBDS20(msg): if bin2int(d[0:4]) != 2 or bin2int(d[4:8]) != 0: return False - cs = callsign(msg) + cs = cs20(msg) if '#' in cs: return False @@ -50,7 +49,7 @@ def isBDS20(msg): return True -def callsign(msg): +def cs20(msg): """Aircraft callsign Args: diff --git a/pyModeS/decoder/bds/bds30.py b/pyModeS/decoder/bds/bds30.py index c075130..50e43fa 100644 --- a/pyModeS/decoder/bds/bds30.py +++ b/pyModeS/decoder/bds/bds30.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros # ------------------------------------------ # BDS 3,0 # ACAS active resolution advisory # ------------------------------------------ -def isBDS30(msg): +def is30(msg): """Check if a message is likely to be BDS code 2,0 Args: diff --git a/pyModeS/decoder/bds/bds40.py b/pyModeS/decoder/bds/bds40.py index 00c719e..4908976 100644 --- a/pyModeS/decoder/bds/bds40.py +++ b/pyModeS/decoder/bds/bds40.py @@ -15,15 +15,14 @@ from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros, wrongstatus +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus # ------------------------------------------ # BDS 4,0 # Selected vertical intention # ------------------------------------------ -def isBDS40(msg): +def is40(msg): """Check if a message is likely to be BDS code 4,0 Args: diff --git a/pyModeS/decoder/bds/bds44.py b/pyModeS/decoder/bds/bds44.py index d3a9ef2..954af63 100644 --- a/pyModeS/decoder/bds/bds44.py +++ b/pyModeS/decoder/bds/bds44.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros, wrongstatus +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus # ------------------------------------------ # BDS 4,4 # Meteorological routine air report # ------------------------------------------ -def isBDS44(msg, rev=False): +def is44(msg, rev=False): """Check if a message is likely to be BDS code 4,4 Meteorological routine air report diff --git a/pyModeS/decoder/bds/bds50.py b/pyModeS/decoder/bds/bds50.py index 2584db9..fc7353b 100644 --- a/pyModeS/decoder/bds/bds50.py +++ b/pyModeS/decoder/bds/bds50.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros, wrongstatus +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus # ------------------------------------------ # BDS 5,0 # Track and turn report # ------------------------------------------ -def isBDS50(msg): +def is50(msg): """Check if a message is likely to be BDS code 5,0 (Track and turn report) diff --git a/pyModeS/decoder/bds/bds53.py b/pyModeS/decoder/bds/bds53.py index 6275548..56ee27c 100644 --- a/pyModeS/decoder/bds/bds53.py +++ b/pyModeS/decoder/bds/bds53.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros, wrongstatus +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus # ------------------------------------------ # BDS 5,3 # Air-referenced state vector # ------------------------------------------ -def isBDS53(msg): +def is53(msg): """Check if a message is likely to be BDS code 5,3 (Air-referenced state vector) diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py index 9836942..4a1c7f1 100644 --- a/pyModeS/decoder/bds/bds60.py +++ b/pyModeS/decoder/bds/bds60.py @@ -14,15 +14,14 @@ # along with this program. If not, see . from __future__ import absolute_import, print_function, division -from pyModeS.decoder.util import hex2bin, bin2int -from pyModeS.decoder.modes import data, allzeros, wrongstatus +from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus # ------------------------------------------ # BDS 6,0 # Heading and speed report # ------------------------------------------ -def isBDS60(msg): +def is60(msg): """Check if a message is likely to be BDS code 6,0 Args: diff --git a/pyModeS/decoder/common.py b/pyModeS/decoder/common.py new file mode 100644 index 0000000..3fd293c --- /dev/null +++ b/pyModeS/decoder/common.py @@ -0,0 +1,313 @@ +from __future__ import absolute_import, print_function, division +import numpy as np + +def hex2bin(hexstr): + """Convert a hexdecimal string to binary string, with zero fillings. """ + num_of_bits = len(hexstr) * 4 + binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits)) + return binstr + + +def bin2int(binstr): + """Convert a binary string to integer. """ + return int(binstr, 2) + + +def hex2int(hexstr): + """Convert a hexdecimal string to integer. """ + return int(hexstr, 16) + + +def bin2np(binstr): + """Convert a binary string to numpy array. """ + return np.array([int(i) for i in binstr]) + + +def np2bin(npbin): + """Convert a binary numpy array to string. """ + return np.array2string(npbin, separator='')[1:-1] + + +def df(msg): + """Decode Downlink Format vaule, bits 1 to 5.""" + msgbin = hex2bin(msg) + return bin2int(msgbin[0:5]) + + +def crc(msg, encode=False): + """Mode-S Cyclic Redundancy Check + Detect if bit error occurs in the Mode-S message + Args: + msg (string): 28 bytes hexadecimal message string + encode (bool): True to encode the date only and return the checksum + Returns: + string: message checksum, or partity bits (encoder) + """ + + # the polynominal generattor code for CRC [1111111111111010000001001] + generator = np.array([1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,1,0,0,1]) + ng = len(generator) + + msgnpbin = bin2np(hex2bin(msg)) + + if encode: + msgnpbin[-24:] = [0] * 24 + + # loop all bits, except last 24 piraty bits + for i in range(len(msgnpbin)-24): + if msgnpbin[i] == 0: + continue + + # perform XOR, when 1 + msgnpbin[i:i+ng] = np.bitwise_xor(msgnpbin[i:i+ng], generator) + + # last 24 bits + reminder = np2bin(msgnpbin[-24:]) + return reminder + + +def floor(x): + """ Mode-S floor function + + Defined as the greatest integer value k, such that k <= x + + eg.: floor(3.6) = 3, while floor(-3.6) = -4 + """ + return int(np.floor(x)) + + +def icao(msg): + """Calculate the ICAO address from an Mode-S message + with DF4, DF5, DF20, DF21 + + Args: + msg (String): 28 bytes hexadecimal message string + + Returns: + String: ICAO address in 6 bytes hexadecimal string + """ + + DF = df(msg) + + if DF in (17, 18): + addr = msg[2:8] + elif DF in (4, 5, 20, 21): + c0 = bin2int(crc(msg, encode=True)) + c1 = hex2int(msg[-6:]) + addr = '%06X' % (c0 ^ c1) + else: + addr = None + + return addr + + +def is_icao_assigned(icao): + """ Check whether the ICAO address is assigned (Annex 10, Vol 3)""" + + if (icao is None) or (not isinstance(icao, str)) or (len(icao)!=6): + return False + + icaoint = hex2int(icao) + + if 0x200000 < icaoint < 0x27FFFF: return False # AFI + if 0x280000 < icaoint < 0x28FFFF: return False # SAM + if 0x500000 < icaoint < 0x5FFFFF: return False # EUR, NAT + if 0x600000 < icaoint < 0x67FFFF: return False # MID + if 0x680000 < icaoint < 0x6F0000: return False # ASIA + if 0x900000 < icaoint < 0x9FFFFF: return False # NAM, PAC + if 0xB00000 < icaoint < 0xBFFFFF: return False # CAR + if 0xD00000 < icaoint < 0xDFFFFF: return False # future + if 0xF00000 < icaoint < 0xFFFFFF: return False # future + + return True + +def typecode(msg): + """Type code of ADS-B message + + Args: + msg (string): 28 bytes hexadecimal message string + + Returns: + int: type code number + """ + if df(msg) not in (17, 18): + return None + + msgbin = hex2bin(msg) + return bin2int(msgbin[32:37]) + + +def cprNL(lat): + """NL() function in CPR decoding""" + + if lat == 0: + return 59 + + if lat == 87 or lat == -87: + return 2 + + if lat > 87 or lat < -87: + return 1 + + nz = 15 + a = 1 - np.cos(np.pi / (2 * nz)) + b = np.cos(np.pi / 180.0 * abs(lat)) ** 2 + nl = 2 * np.pi / (np.arccos(1 - a/b)) + NL = floor(nl) + return NL + +def idcode(msg): + """Computes identity (squawk code) from DF5 or DF21 message, bit 20-32. + credit: @fbyrkjeland + + Args: + msg (String): 28 bytes hexadecimal message string + + Returns: + string: squawk code + """ + + if df(msg) not in [5, 21]: + raise RuntimeError("Message must be Downlink Format 5 or 21.") + + mbin = hex2bin(msg) + + C1 = mbin[19] + A1 = mbin[20] + C2 = mbin[21] + A2 = mbin[22] + C4 = mbin[23] + A4 = mbin[24] + # _ = mbin[25] + B1 = mbin[26] + D1 = mbin[27] + B2 = mbin[28] + D2 = mbin[29] + B4 = mbin[30] + D4 = mbin[31] + + byte1 = int(A4+A2+A1, 2) + byte2 = int(B4+B2+B1, 2) + byte3 = int(C4+C2+C1, 2) + byte4 = int(D4+D2+D1, 2) + + return str(byte1) + str(byte2) + str(byte3) + str(byte4) + + +def altcode(msg): + """Computes the altitude from DF4 or DF20 message, bit 20-32. + credit: @fbyrkjeland + + Args: + msg (String): 28 bytes hexadecimal message string + + Returns: + int: altitude in ft + """ + + if df(msg) not in [4, 20]: + raise RuntimeError("Message must be Downlink Format 4 or 20.") + + # Altitude code, bit 20-32 + mbin = hex2bin(msg) + + mbit = mbin[25] # M bit: 26 + qbit = mbin[27] # Q bit: 28 + + + if mbit == '0': # unit in ft + if qbit == '1': # 25ft interval + vbin = mbin[19:25] + mbin[26] + mbin[28:32] + alt = bin2int(vbin) * 25 - 1000 + if qbit == '0': # 100ft interval, above 50175ft + C1 = mbin[19] + A1 = mbin[20] + C2 = mbin[21] + A2 = mbin[22] + C4 = mbin[23] + A4 = mbin[24] + # _ = mbin[25] + B1 = mbin[26] + # D1 = mbin[27] # always zero + B2 = mbin[28] + D2 = mbin[29] + B4 = mbin[30] + D4 = mbin[31] + + graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4 + alt = gray2alt(graystr) + + if mbit == '1': # unit in meter + vbin = mbin[19:25] + mbin[26:31] + alt = int(bin2int(vbin) * 3.28084) # convert to ft + + return alt + + +def gray2alt(codestr): + gc500 = codestr[:8] + n500 = gray2int(gc500) + + # in 100-ft step must be converted first + gc100 = codestr[8:] + n100 = gray2int(gc100) + + if n100 in [0, 5, 6]: + return None + + if n100 == 7: + n100 = 5 + + if n500%2: + n100 = 6 - n100 + + alt = (n500*500 + n100*100) - 1300 + return alt + + +def gray2int(graystr): + """Convert greycode to binary""" + num = bin2int(graystr) + num ^= (num >> 8) + num ^= (num >> 4) + num ^= (num >> 2) + num ^= (num >> 1) + return num + + +def data(msg): + """Return the data frame in the message, bytes 9 to 22""" + return msg[8:-6] + + +def allzeros(msg): + """check if the data bits are all zeros + + Args: + msg (String): 28 bytes hexadecimal message string + + Returns: + bool: True or False + """ + d = hex2bin(data(msg)) + + if bin2int(d) > 0: + return False + else: + return True + + +def wrongstatus(data, sb, msb, lsb): + """Check if the status bit and field bits are consistency. This Function + is used for checking BDS code versions. + """ + + # status bit, most significant bit, least significant bit + status = int(data[sb-1]) + value = bin2int(data[msb-1:lsb]) + + if not status: + if value != 0: + return True + + return False diff --git a/pyModeS/decoder/ehs.py b/pyModeS/decoder/ehs.py index b5ec48c..ad1570b 100644 --- a/pyModeS/decoder/ehs.py +++ b/pyModeS/decoder/ehs.py @@ -1,9 +1,7 @@ from __future__ import absolute_import, print_function, division from pyModeS.decoder.bds.bds40 import * -from pyModeS.decoder.bds.bds44 import * from pyModeS.decoder.bds.bds50 import * -from pyModeS.decoder.bds.bds53 import * from pyModeS.decoder.bds.bds60 import * def BDS(msg): @@ -14,8 +12,5 @@ def BDS(msg): return infer(msg) def icao(msg): - import warnings - from pyModeS.decoder.modes import icao - warnings.simplefilter('always', DeprecationWarning) - warnings.warn("pms.ehs.icao() deprecated, please use pms.modes.icao() instead", DeprecationWarning) + from pyModeS.decoder.common import icao return icao(msg) diff --git a/pyModeS/decoder/modes.py b/pyModeS/decoder/modes.py deleted file mode 100644 index e61776f..0000000 --- a/pyModeS/decoder/modes.py +++ /dev/null @@ -1,180 +0,0 @@ -from __future__ import absolute_import, print_function, division -from . import util - - -def icao(msg): - """Calculate the ICAO address from an Mode-S message - with DF4, DF5, DF20, DF21 - - Args: - msg (String): 28 bytes hexadecimal message string - - Returns: - String: ICAO address in 6 bytes hexadecimal string - """ - - if util.df(msg) not in (4, 5, 20, 21): - # raise RuntimeError("Message DF must be in (4, 5, 20, 21)") - return None - - c0 = util.bin2int(util.crc(msg, encode=True)) - c1 = util.hex2int(msg[-6:]) - addr = '%06X' % (c0 ^ c1) - return addr - - -def idcode(msg): - """Computes identity (squawk code) from DF5 or DF21 message, bit 20-32. - credit: @fbyrkjeland - - Args: - msg (String): 28 bytes hexadecimal message string - - Returns: - string: squawk code - """ - - if util.df(msg) not in [5, 21]: - raise RuntimeError("Message must be Downlink Format 5 or 21.") - - mbin = util.hex2bin(msg) - - C1 = mbin[19] - A1 = mbin[20] - C2 = mbin[21] - A2 = mbin[22] - C4 = mbin[23] - A4 = mbin[24] - # _ = mbin[25] - B1 = mbin[26] - D1 = mbin[27] - B2 = mbin[28] - D2 = mbin[29] - B4 = mbin[30] - D4 = mbin[31] - - byte1 = int(A4+A2+A1, 2) - byte2 = int(B4+B2+B1, 2) - byte3 = int(C4+C2+C1, 2) - byte4 = int(D4+D2+D1, 2) - - return str(byte1) + str(byte2) + str(byte3) + str(byte4) - - -def altcode(msg): - """Computes the altitude from DF4 or DF20 message, bit 20-32. - credit: @fbyrkjeland - - Args: - msg (String): 28 bytes hexadecimal message string - - Returns: - int: altitude in ft - """ - - if util.df(msg) not in [4, 20]: - raise RuntimeError("Message must be Downlink Format 4 or 20.") - - # Altitude code, bit 20-32 - mbin = util.hex2bin(msg) - - mbit = mbin[25] # M bit: 26 - qbit = mbin[27] # Q bit: 28 - - - if mbit == '0': # unit in ft - if qbit == '1': # 25ft interval - vbin = mbin[19:25] + mbin[26] + mbin[28:32] - alt = util.bin2int(vbin) * 25 - 1000 - if qbit == '0': # 100ft interval, above 50175ft - C1 = mbin[19] - A1 = mbin[20] - C2 = mbin[21] - A2 = mbin[22] - C4 = mbin[23] - A4 = mbin[24] - # _ = mbin[25] - B1 = mbin[26] - # D1 = mbin[27] # always zero - B2 = mbin[28] - D2 = mbin[29] - B4 = mbin[30] - D4 = mbin[31] - - graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4 - alt = gray2alt(graystr) - - if mbit == '1': # unit in meter - vbin = mbin[19:25] + mbin[26:31] - alt = int(util.bin2int(vbin) * 3.28084) # convert to ft - - return alt - - -def gray2alt(codestr): - gc500 = codestr[:8] - n500 = gray2int(gc500) - - # in 100-ft step must be converted first - gc100 = codestr[8:] - n100 = gray2int(gc100) - - if n100 in [0, 5, 6]: - return None - - if n100 == 7: - n100 = 5 - - if n500%2: - n100 = 6 - n100 - - alt = (n500*500 + n100*100) - 1300 - return alt - - -def gray2int(graystr): - """Convert greycode to binary""" - num = util.bin2int(graystr) - num ^= (num >> 8) - num ^= (num >> 4) - num ^= (num >> 2) - num ^= (num >> 1) - return num - - -def data(msg): - """Return the data frame in the message, bytes 9 to 22""" - return msg[8:-6] - - -def allzeros(msg): - """check if the data bits are all zeros - - Args: - msg (String): 28 bytes hexadecimal message string - - Returns: - bool: True or False - """ - d = util.hex2bin(data(msg)) - - if util.bin2int(d) > 0: - return False - else: - return True - - -def wrongstatus(data, sb, msb, lsb): - """Check if the status bit and field bits are consistency. This Function - is used for checking BDS code versions. - """ - - # status bit, most significant bit, least significant bit - status = int(data[sb-1]) - value = util.bin2int(data[msb-1:lsb]) - - if not status: - if value != 0: - return True - - return False diff --git a/pyModeS/decoder/util.py b/pyModeS/decoder/util.py deleted file mode 100644 index ae82162..0000000 --- a/pyModeS/decoder/util.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (C) 2015 Junzi Sun (TU Delft) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -""" -Common functions for ADS-B and Mode-S EHS decoder -""" - - -import numpy as np - - -def hex2bin(hexstr): - """Convert a hexdecimal string to binary string, with zero fillings. """ - num_of_bits = len(hexstr) * 4 - binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits)) - return binstr - - -def bin2int(binstr): - """Convert a binary string to integer. """ - return int(binstr, 2) - - -def hex2int(hexstr): - """Convert a hexdecimal string to integer. """ - return int(hexstr, 16) - - -def bin2np(binstr): - """Convert a binary string to numpy array. """ - return np.array([int(i) for i in binstr]) - - -def np2bin(npbin): - """Convert a binary numpy array to string. """ - return np.array2string(npbin, separator='')[1:-1] - - -def df(msg): - """Decode Downlink Format vaule, bits 1 to 5.""" - msgbin = hex2bin(msg) - return bin2int(msgbin[0:5]) - - -def crc(msg, encode=False): - """Mode-S Cyclic Redundancy Check - Detect if bit error occurs in the Mode-S message - Args: - msg (string): 28 bytes hexadecimal message string - encode (bool): True to encode the date only and return the checksum - Returns: - string: message checksum, or partity bits (encoder) - """ - - # the polynominal generattor code for CRC [1111111111111010000001001] - generator = np.array([1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,1,0,0,1]) - ng = len(generator) - - msgnpbin = bin2np(hex2bin(msg)) - - if encode: - msgnpbin[-24:] = [0] * 24 - - # loop all bits, except last 24 piraty bits - for i in range(len(msgnpbin)-24): - if msgnpbin[i] == 0: - continue - - # perform XOR, when 1 - msgnpbin[i:i+ng] = np.bitwise_xor(msgnpbin[i:i+ng], generator) - - # last 24 bits - reminder = np2bin(msgnpbin[-24:]) - return reminder - - -def floor(x): - """ Mode-S floor function - - Defined as the greatest integer value k, such that k <= x - - eg.: floor(3.6) = 3, while floor(-3.6) = -4 - """ - return int(np.floor(x)) - - -def is_icao_assigned(icao): - """ Check whether the ICAO address is assigned (Annex 10, Vol 3)""" - - if (icao is None) or (not isinstance(icao, str)) or (len(icao)!=6): - return False - - icaoint = hex2int(icao) - - if 0x200000 < icaoint < 0x27FFFF: return False # AFI - if 0x280000 < icaoint < 0x28FFFF: return False # SAM - if 0x500000 < icaoint < 0x5FFFFF: return False # EUR, NAT - if 0x600000 < icaoint < 0x67FFFF: return False # MID - if 0x680000 < icaoint < 0x6F0000: return False # ASIA - if 0x900000 < icaoint < 0x9FFFFF: return False # NAM, PAC - if 0xB00000 < icaoint < 0xBFFFFF: return False # CAR - if 0xD00000 < icaoint < 0xDFFFFF: return False # future - if 0xF00000 < icaoint < 0xFFFFFF: return False # future - - return True diff --git a/pyModeS/streamer/pmstream.py b/pyModeS/streamer/pmstream.py index 3e65a11..450d4e9 100644 --- a/pyModeS/streamer/pmstream.py +++ b/pyModeS/streamer/pmstream.py @@ -6,7 +6,7 @@ import curses import numpy as np import time from threading import Lock -from pyModeS.decoder import util +import pyModeS as pms from pyModeS.extra.beastclient import BaseClient from pyModeS.streamer.stream import Stream from pyModeS.streamer.screen import Screen @@ -43,7 +43,7 @@ class ModesClient(BaseClient): if len(msg) < 28: # only process long messages continue - df = util.df(msg) + df = pms.df(msg) if df == 17 or df == 18: local_buffer_adsb_msg.append(msg) diff --git a/tests/test_adsb.py b/tests/test_adsb.py index 40c88bb..63b38f8 100644 --- a/tests/test_adsb.py +++ b/tests/test_adsb.py @@ -58,8 +58,8 @@ def test_adsb_velocity(): vgs = adsb.velocity("8D485020994409940838175B284F") vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F") vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000") - assert vgs == (159, 182.9, -832, 'GS') - assert vas == (376, 244.0, -2304, 'AS') + assert vgs == (159, 182.88, -832, 'GS') + assert vas == (375, 243.98, -2304, 'TAS') assert vgs_surface == (19.0, 42.2, 0 , 'GS') assert adsb.altitude_diff('8D485020994409940838175B284F') == 550 diff --git a/tests/test_modes.py b/tests/test_bds.py similarity index 55% rename from tests/test_modes.py rename to tests/test_bds.py index e3772a3..951802c 100644 --- a/tests/test_modes.py +++ b/tests/test_bds.py @@ -1,17 +1,4 @@ -from pyModeS import bds, modes - -def test_ehs_icao(): - assert modes.icao("A0001839CA3800315800007448D9") == '400940' - assert modes.icao("A000139381951536E024D4CCF6B5") == '3C4DD2' - assert modes.icao("A000029CFFBAA11E2004727281F1") == '4243D0' - - -def test_modes_altcode(): - assert modes.altcode("A02014B400000000000000F9D514") == 32300 - -def test_modes_idcode(): - assert modes.idcode("A800292DFFBBA9383FFCEB903D01") == '1346' - +from pyModeS import bds, ehs, els def test_bds_infer(): assert bds.infer("A0001838201584F23468207CDFA5") == 'BDS20' @@ -25,13 +12,18 @@ def test_bds_is50or60(): assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == 'BDS60' def test_els_BDS20_callsign(): - assert bds.bds20.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_' - assert bds.bds20.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_' + assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_' + assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_' + assert els.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_' + assert els.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_' def test_ehs_BDS40_functions(): assert bds.bds40.alt40mcp("A000029C85E42F313000007047D3") == 3008 assert bds.bds40.alt40fms("A000029C85E42F313000007047D3") == 3008 assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0 + assert ehs.alt40mcp("A000029C85E42F313000007047D3") == 3008 + assert ehs.alt40fms("A000029C85E42F313000007047D3") == 3008 + assert ehs.p40baro("A000029C85E42F313000007047D3") == 1020.0 def test_ehs_BDS50_functions(): assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1 @@ -40,6 +32,12 @@ def test_ehs_BDS50_functions(): assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438 assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125 assert bds.bds50.tas50("A000139381951536E024D4CCF6B5") == 424 + assert ehs.roll50("A000139381951536E024D4CCF6B5") == 2.1 + assert ehs.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value + assert ehs.trk50("A000139381951536E024D4CCF6B5") == 114.258 + assert ehs.gs50("A000139381951536E024D4CCF6B5") == 438 + assert ehs.rtrk50("A000139381951536E024D4CCF6B5") == 0.125 + assert ehs.tas50("A000139381951536E024D4CCF6B5") == 424 def test_ehs_BDS60_functions(): assert bds.bds60.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715 @@ -47,20 +45,8 @@ def test_ehs_BDS60_functions(): assert bds.bds60.mach60("A00004128F39F91A7E27C46ADC21") == 0.42 assert bds.bds60.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920 assert bds.bds60.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920 - -def test_graycode_to_altitude(): - assert modes.gray2alt('00000000010') == -1000 - assert modes.gray2alt('00000001010') == -500 - assert modes.gray2alt('00000011011') == -100 - assert modes.gray2alt('00000011010') == 0 - assert modes.gray2alt('00000011110') == 100 - assert modes.gray2alt('00000010011') == 600 - assert modes.gray2alt('00000110010') == 1000 - assert modes.gray2alt('00001001001') == 5800 - assert modes.gray2alt('00011100100') == 10300 - assert modes.gray2alt('01100011010') == 32000 - assert modes.gray2alt('01110000100') == 46300 - assert modes.gray2alt('01010101100') == 50200 - assert modes.gray2alt('11011110100') == 73200 - assert modes.gray2alt('10000000011') == 126600 - assert modes.gray2alt('10000000001') == 126700 + assert ehs.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715 + assert ehs.ias60("A00004128F39F91A7E27C46ADC21") == 252 + assert ehs.mach60("A00004128F39F91A7E27C46ADC21") == 0.42 + assert ehs.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920 + assert ehs.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920 diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 0000000..b83968a --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,42 @@ +from pyModeS import common + + +def test_hex2bin(): + assert common.hex2bin('6E406B') == "011011100100000001101011" + +def test_crc_decode(): + checksum = common.crc("8D406B902015A678D4D220AA4BDA") + assert checksum == "000000000000000000000000" + +def test_crc_encode(): + parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True) + assert common.hex2bin("AA4BDA") == parity + +def test_icao(): + assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90" + assert common.icao("A0001839CA3800315800007448D9") == '400940' + assert common.icao("A000139381951536E024D4CCF6B5") == '3C4DD2' + assert common.icao("A000029CFFBAA11E2004727281F1") == '4243D0' + +def test_modes_altcode(): + assert common.altcode("A02014B400000000000000F9D514") == 32300 + +def test_modes_idcode(): + assert common.idcode("A800292DFFBBA9383FFCEB903D01") == '1346' + +def test_graycode_to_altitude(): + assert common.gray2alt('00000000010') == -1000 + assert common.gray2alt('00000001010') == -500 + assert common.gray2alt('00000011011') == -100 + assert common.gray2alt('00000011010') == 0 + assert common.gray2alt('00000011110') == 100 + assert common.gray2alt('00000010011') == 600 + assert common.gray2alt('00000110010') == 1000 + assert common.gray2alt('00001001001') == 5800 + assert common.gray2alt('00011100100') == 10300 + assert common.gray2alt('01100011010') == 32000 + assert common.gray2alt('01110000100') == 46300 + assert common.gray2alt('01010101100') == 50200 + assert common.gray2alt('11011110100') == 73200 + assert common.gray2alt('10000000011') == 126600 + assert common.gray2alt('10000000001') == 126700 diff --git a/tests/test_util.py b/tests/test_util.py deleted file mode 100644 index d4cc6e3..0000000 --- a/tests/test_util.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyModeS import util - - -def test_hex2bin(): - assert util.hex2bin('6E406B') == "011011100100000001101011" - - -def test_crc_decode(): - checksum = util.crc("8D406B902015A678D4D220AA4BDA") - assert checksum == "000000000000000000000000" - - -def test_crc_encode(): - parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True) - assert util.hex2bin("AA4BDA") == parity