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
+
+ # 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)
-Core functions for EHS decoding:
-********************************
+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,74 +59,18 @@ 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)
+
+ elif (20<=tc0<=22 and 20<=tc1<=22):
+ # Airborne position with GNSS 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
-
- # 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)
-
-
def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message,
knowing reference nearby location, such as previously
@@ -273,192 +87,19 @@ 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.
-
- 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
- """
-
- 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)
-
- lat = d_lat * (j + cprlat)
-
- ni = _cprNL(lat) - i
-
- if ni > 0:
- d_lon = 360.0 / ni
- 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.
-
- 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
- """
-
- 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)
-
- ni = _cprNL(lat) - i
-
- 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
-
-
def altitude(msg):
"""Decode aircraft altitude
@@ -469,23 +110,62 @@ def altitude(msg):
int: altitude in feet
"""
- if typecode(msg) < 5 or typecode(msg) > 18:
+ tc = typecode(msg)
+
+ if tc<5 or tc==19 or tc>22:
raise RuntimeError("%s: Not a position message" % msg)
- if typecode(msg) >=5 and typecode(msg) <=8:
+ if tc>=5 and tc<=8:
# surface position, altitude 0
return 0
- msgbin = util.hex2bin(msg)
+ msgbin = common.hex2bin(msg)
q = msgbin[47]
if q:
- n = util.bin2int(msgbin[40:47]+msgbin[48:52])
+ n = common.bin2int(msgbin[40:47]+msgbin[48:52])
alt = n * 25 - 1000
return alt
else:
return None
+def velocity(msg):
+ """Calculate the speed, heading, and vertical rate
+ (handles both airborne or surface message)
+
+ 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 5 <= typecode(msg) <= 8:
+ return surface_velocity(msg)
+
+ elif typecode(msg) == 19:
+ return airborne_velocity(msg)
+
+ else:
+ raise RuntimeError("incorrect or inconsistant message types, expecting 4 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