major update; adsb restructure; rename combining util/modes to common.

This commit is contained in:
Junzi Sun 2018-03-28 13:31:24 +02:00
parent ef8f4b3b7f
commit 362b92de7a
26 changed files with 1104 additions and 1010 deletions

View File

@ -18,6 +18,7 @@ Python library for Mode-S message decoding. Support Downlink Formats (DF) are:
- DF21: Squawk code - DF21: Squawk code
- BDS 2,0 Aircraft identification - BDS 2,0 Aircraft identification
- BDS 2,1 Aircraft and airline registration markings - BDS 2,1 Aircraft and airline registration markings
- BDS 3,0 ACAS active resolution advisory
- BDS 4,0 Selected vertical intention - BDS 4,0 Selected vertical intention
- BDS 4,4 Meteorological routine air report - BDS 4,4 Meteorological routine air report
- BDS 5,0 Track and turn report - BDS 5,0 Track and turn report
@ -30,6 +31,7 @@ http://adsb-decode-guide.readthedocs.io
New features in v2.0 New features in v2.0
--------------------- ---------------------
- New structure of the libraries
- ADS-B and EHS data streaming - ADS-B and EHS data streaming
- Active aircraft viewing (in terminal) - Active aircraft viewing (in terminal)
- More advanced BDS identification in Enhanced Mode-S - More advanced BDS identification in Enhanced Mode-S
@ -39,22 +41,16 @@ New features in v2.0
Source code Source code
----------- -----------
Checkout and contribute to this open source project at: 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: API documentation at:
http://pymodes.readthedocs.io http://pymodes.readthedocs.io
[To be updated]
Install 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: To install latest development version (dev-2.0) from the GitHub:
:: ::
@ -76,13 +72,13 @@ Common functions:
.. code:: python .. code:: python
pms.df(msg) # Downlink Format 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.crc(msg, encode=False) # Perform CRC or generate parity bit
pms.hex2bin(str) # Convert hexadecimal string to binary string pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer pms.hex2int(str) # Convert hexadecimal string to integer
pms.gray2int(str) # Convert grey code to interger
pms.gray2int(str) # Convert grey code to interger
Core functions for ADS-B decoding: Core functions for ADS-B decoding:
@ -96,7 +92,7 @@ Core functions for ADS-B decoding:
# typecode 1-4 # typecode 1-4
pms.adsb.callsign(msg) 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.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.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) 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) messages. But the reference position shall be with in 180NM (airborne)
or 45NM (surface) of the true position. or 45NM (surface) of the true position.
Core functions for ELS decoding:
******************************** Common Mode-S functions
************************
.. code:: python .. code:: python
pms.els.icao(msg) # ICAO address pms.icao(msg) # Infer the ICAO address from the message
pms.els.df4alt(msg) # Altitude from any DF4 message pms.bds.infer(msg) # Infer the Modes-S BDS code
pms.ehs.df5id(msg) # Squawk code from any DF5 message
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 .. code:: python
pms.ehs.icao(msg) # ICAO address pms.els.ovc10(msg) # overlay capability, BDS 1,0
pms.ehs.df20alt(msg) # Altitude from any DF20 message pms.els.cap17(msg) # GICB capability, BDS 1,7
pms.ehs.df21id(msg) # Squawk code from any DF21 message pms.els.cs20(msg) # callsign, BDS 2,0
pms.ehs.BDS(msg) # Comm-B Data Selector Version
# for BDS version 2,0 Mode-S enhanced surveillance (EHS)
pms.ehs.isBDS20(msg) # Check if message is BDS 2,0 ***********************************
pms.ehs.callsign(msg) # Aircraft callsign
.. code:: python
# for BDS version 4,0 # 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.alt40mcp(msg) # MCP/FCU selected altitude (ft)
pms.ehs.alt40fms(msg) # FMS selected altitude (ft) pms.ehs.alt40fms(msg) # FMS selected altitude (ft)
pms.ehs.p40baro(msg) # Barometric pressure (mb) 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 # 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.roll50(msg) # roll angle (deg)
pms.ehs.trk50(msg) # track angle (deg) pms.ehs.trk50(msg) # track angle (deg)
pms.ehs.gs50(msg) # ground speed (kt) pms.ehs.gs50(msg) # ground speed (kt)
pms.ehs.rtrk50(msg) # track angle rate (deg/sec) pms.ehs.rtrk50(msg) # track angle rate (deg/sec)
pms.ehs.tas50(msg) # true airspeed (kt) 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 # for BDS version 6,0
pms.ehs.isBDS60(msg) # Check if message is BDS 6,0
pms.ehs.hdg60(msg) # heading (deg) pms.ehs.hdg60(msg) # heading (deg)
pms.ehs.ias60(msg) # indicated airspeed (kt) pms.ehs.ias60(msg) # indicated airspeed (kt)
pms.ehs.mach60(msg) # MACH number pms.ehs.mach60(msg) # MACH number
pms.ehs.vr60baro(msg) # barometric altitude rate (ft/min) pms.ehs.vr60baro(msg) # barometric altitude rate (ft/min)
pms.ehs.vr60ins(msg) # inertial vertical speed (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 Developement
------------ ------------
To perform unit tests. First install ``tox`` through pip, Then, run the following commands: To perform unit tests. First install ``tox`` through pip, Then, run the following commands:

View File

@ -1,11 +1,10 @@
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from .decoder.util import * from .decoder.common import *
from .decoder import adsb from .decoder import adsb
from .decoder import els from .decoder import els
from .decoder import ehs from .decoder import ehs
from .decoder import util from .decoder import common
from .decoder import modes
from .decoder import bds from .decoder import bds
from .extra import aero from .extra import aero
from .extra import beastclient from .extra import beastclient

View File

@ -14,159 +14,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
A python package for decoding ABS-D messages. The wrapper for decoding ADS-B messages
""" """
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
import math from pyModeS.decoder import common
from . import util # 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): def df(msg):
"""Get the downlink format (DF) number return common.df(msg)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: DF number
"""
return util.df(msg)
def icao(msg): def icao(msg):
"""Get the ICAO 24 bits address, bytes 3 to 8. return common.icao(msg)
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]
def typecode(msg): def typecode(msg):
"""Type code of ADS-B message return common.typecode(msg)
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])
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None): def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
"""Decode position from a pair of even and odd position message """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: Returns:
(float, float): (latitude, longitude) of the aircraft (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): if (not lat_ref) or (not lon_ref):
raise RuntimeError("Surface position encountered, a reference \ raise RuntimeError("Surface position encountered, a reference \
position lat/lon required. Location of \ position lat/lon required. Location of \
@ -189,74 +59,18 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
else: else:
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref) 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) return airborne_position(msg0, msg1, t0, t1)
else: else:
raise RuntimeError("incorrect or inconsistant message types") 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): def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message, """Decode position with only one message,
knowing reference nearby location, such as previously knowing reference nearby location, such as previously
@ -273,192 +87,19 @@ def position_with_ref(msg, lat_ref, lon_ref):
Returns: Returns:
(float, float): (latitude, longitude) of the aircraft (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) 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) return airborne_position_with_ref(msg, lat_ref, lon_ref)
else: else:
raise RuntimeError("incorrect or inconsistant message types") 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): def altitude(msg):
"""Decode aircraft altitude """Decode aircraft altitude
@ -469,23 +110,62 @@ def altitude(msg):
int: altitude in feet 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) 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 # surface position, altitude 0
return 0 return 0
msgbin = util.hex2bin(msg) msgbin = common.hex2bin(msg)
q = msgbin[47] q = msgbin[47]
if q: if q:
n = util.bin2int(msgbin[40:47]+msgbin[48:52]) n = common.bin2int(msgbin[40:47]+msgbin[48:52])
alt = n * 25 - 1000 alt = n * 25 - 1000
return alt return alt
else: else:
return None 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<TC<9 or TC=19")
def speed_heading(msg):
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), ground track or heading (degree)
"""
spd, trk_or_hdg, rocd, tag = velocity(msg)
return spd, trk_or_hdg
def nic(msg): def nic(msg):
"""Calculate NIC, navigation integrity category """Calculate NIC, navigation integrity category
@ -498,9 +178,9 @@ def nic(msg):
if typecode(msg) < 9 or typecode(msg) > 18: if typecode(msg) < 9 or typecode(msg) > 18:
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg) raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
msgbin = util.hex2bin(msg) msgbin = common.hex2bin(msg)
tc = typecode(msg) tc = typecode(msg)
nic_sup_b = util.bin2int(msgbin[39]) nic_sup_b = common.bin2int(msgbin[39])
if tc in [0, 18, 22]: if tc in [0, 18, 22]:
nic = 0 nic = 0
@ -531,166 +211,3 @@ def nic(msg):
else: else:
nic = -1 nic = -1
return nic return nic
# ---------------------------------------------
# Velocity
# ---------------------------------------------
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<TC<9 or TC=19")
def speed_heading(msg):
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), ground track or heading (degree)
"""
spd, trk_or_hdg, rocd, tag = velocity(msg)
return spd, trk_or_hdg
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 typecode(msg) != 19:
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
msgbin = util.hex2bin(msg)
subtype = util.bin2int(msgbin[37:40])
if util.bin2int(msgbin[46:56]) == 0 or util.bin2int(msgbin[57:67]) == 0:
return None
if subtype in (1, 2):
v_ew_sign = -1 if int(msgbin[45]) else 1
v_ew = util.bin2int(msgbin[46:56]) - 1 # east-west velocity
v_ns_sign = -1 if int(msgbin[56]) else 1
v_ns = util.bin2int(msgbin[57:67]) - 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:
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<TC<8" % msg)
msgbin = util.hex2bin(msg)
# ground track
trk_status = int(msgbin[44])
if trk_status == 1:
trk = util.bin2int(msgbin[45:52]) * 360.0 / 128.0
trk = round(trk, 1)
else:
trk = None
# ground movment / speed
mov = util.bin2int(msgbin[37:44])
if mov == 0 or mov > 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.

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
Common functions for Mode-S decoding
"""
from __future__ import absolute_import, print_function, division
import numpy as np import numpy as np
from pyModeS.extra import aero from pyModeS.extra import aero
from pyModeS.decoder.modes import allzeros from pyModeS.decoder.common import allzeros
from pyModeS.decoder.bds.bds10 import isBDS10 from pyModeS.decoder.bds.bds10 import is10
from pyModeS.decoder.bds.bds17 import isBDS17 from pyModeS.decoder.bds.bds17 import is17
from pyModeS.decoder.bds.bds20 import isBDS20 from pyModeS.decoder.bds.bds20 import is20
from pyModeS.decoder.bds.bds30 import isBDS30 from pyModeS.decoder.bds.bds30 import is30
from pyModeS.decoder.bds.bds40 import isBDS40 from pyModeS.decoder.bds.bds40 import is40
from pyModeS.decoder.bds.bds50 import isBDS50, trk50, gs50 from pyModeS.decoder.bds.bds50 import is50, trk50, gs50
from pyModeS.decoder.bds.bds60 import isBDS60, hdg60, mach60, ias60 from pyModeS.decoder.bds.bds60 import is60, hdg60, mach60, ias60
def is50or60(msg, spd_ref, trk_ref, alt_ref): 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)) vy = v * np.cos(np.deg2rad(angle))
return vx, vy return vx, vy
if not (isBDS50(msg) and isBDS60(msg)): if not (is50(msg) and is60(msg)):
return None return None
h50 = trk50(msg) h50 = trk50(msg)
@ -79,21 +99,21 @@ def infer(msg):
if allzeros(msg): if allzeros(msg):
return None return None
is10 = isBDS10(msg) IS10 = is10(msg)
is17 = isBDS17(msg) IS17 = is17(msg)
is20 = isBDS20(msg) IS20 = is20(msg)
is30 = isBDS30(msg) IS30 = is30(msg)
is40 = isBDS40(msg) IS40 = is40(msg)
is50 = isBDS50(msg) IS50 = is50(msg)
is60 = isBDS60(msg) IS60 = is60(msg)
allbds = np.array([ allbds = np.array([
"BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60" "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: if len(bds) == 0:
return None return None

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
------------------------------------------
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

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
------------------------------------------
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<TC<8" % msg)
mb = common.hex2bin(msg)[32:]
# ground track
trk_status = int(mb[12])
if trk_status == 1:
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
trk = round(trk, 1)
else:
trk = None
# ground movment / speed
mov = common.bin2int(mb[5:12])
if mov == 0 or mov > 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'

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
------------------------------------------
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

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
------------------------------------------
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.

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from pyModeS.decoder.modes import data, allzeros
# ------------------------------------------ # ------------------------------------------
# BDS 1,0 # BDS 1,0
# Data link capability report # Data link capability report
# ------------------------------------------ # ------------------------------------------
def isBDS10(msg): def is10(msg):
"""Check if a message is likely to be BDS code 1,0 """Check if a message is likely to be BDS code 1,0
Args: Args:

View File

@ -15,15 +15,16 @@
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from pyModeS.decoder.modes import 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 """Check if a message is likely to be BDS code 1,7
Args: Args:

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from pyModeS.decoder.modes import data, allzeros
# ------------------------------------------ # ------------------------------------------
# BDS 2,0 # BDS 2,0
# Aircraft identification # Aircraft identification
# ------------------------------------------ # ------------------------------------------
def isBDS20(msg): def is20(msg):
"""Check if a message is likely to be BDS code 2,0 """Check if a message is likely to be BDS code 2,0
Args: Args:
@ -42,7 +41,7 @@ def isBDS20(msg):
if bin2int(d[0:4]) != 2 or bin2int(d[4:8]) != 0: if bin2int(d[0:4]) != 2 or bin2int(d[4:8]) != 0:
return False return False
cs = callsign(msg) cs = cs20(msg)
if '#' in cs: if '#' in cs:
return False return False
@ -50,7 +49,7 @@ def isBDS20(msg):
return True return True
def callsign(msg): def cs20(msg):
"""Aircraft callsign """Aircraft callsign
Args: Args:

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
from pyModeS.decoder.modes import data, allzeros
# ------------------------------------------ # ------------------------------------------
# BDS 3,0 # BDS 3,0
# ACAS active resolution advisory # ACAS active resolution advisory
# ------------------------------------------ # ------------------------------------------
def isBDS30(msg): def is30(msg):
"""Check if a message is likely to be BDS code 2,0 """Check if a message is likely to be BDS code 2,0
Args: Args:

View File

@ -15,15 +15,14 @@
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from pyModeS.decoder.modes import data, allzeros, wrongstatus
# ------------------------------------------ # ------------------------------------------
# BDS 4,0 # BDS 4,0
# Selected vertical intention # Selected vertical intention
# ------------------------------------------ # ------------------------------------------
def isBDS40(msg): def is40(msg):
"""Check if a message is likely to be BDS code 4,0 """Check if a message is likely to be BDS code 4,0
Args: Args:

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from pyModeS.decoder.modes import data, allzeros, wrongstatus
# ------------------------------------------ # ------------------------------------------
# BDS 4,4 # BDS 4,4
# Meteorological routine air report # 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 """Check if a message is likely to be BDS code 4,4
Meteorological routine air report Meteorological routine air report

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from pyModeS.decoder.modes import data, allzeros, wrongstatus
# ------------------------------------------ # ------------------------------------------
# BDS 5,0 # BDS 5,0
# Track and turn report # Track and turn report
# ------------------------------------------ # ------------------------------------------
def isBDS50(msg): def is50(msg):
"""Check if a message is likely to be BDS code 5,0 """Check if a message is likely to be BDS code 5,0
(Track and turn report) (Track and turn report)

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from pyModeS.decoder.modes import data, allzeros, wrongstatus
# ------------------------------------------ # ------------------------------------------
# BDS 5,3 # BDS 5,3
# Air-referenced state vector # Air-referenced state vector
# ------------------------------------------ # ------------------------------------------
def isBDS53(msg): def is53(msg):
"""Check if a message is likely to be BDS code 5,3 """Check if a message is likely to be BDS code 5,3
(Air-referenced state vector) (Air-referenced state vector)

View File

@ -14,15 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.util import hex2bin, bin2int from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
from pyModeS.decoder.modes import data, allzeros, wrongstatus
# ------------------------------------------ # ------------------------------------------
# BDS 6,0 # BDS 6,0
# Heading and speed report # Heading and speed report
# ------------------------------------------ # ------------------------------------------
def isBDS60(msg): def is60(msg):
"""Check if a message is likely to be BDS code 6,0 """Check if a message is likely to be BDS code 6,0
Args: Args:

313
pyModeS/decoder/common.py Normal file
View File

@ -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

View File

@ -1,9 +1,7 @@
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
from pyModeS.decoder.bds.bds40 import * from pyModeS.decoder.bds.bds40 import *
from pyModeS.decoder.bds.bds44 import *
from pyModeS.decoder.bds.bds50 import * from pyModeS.decoder.bds.bds50 import *
from pyModeS.decoder.bds.bds53 import *
from pyModeS.decoder.bds.bds60 import * from pyModeS.decoder.bds.bds60 import *
def BDS(msg): def BDS(msg):
@ -14,8 +12,5 @@ def BDS(msg):
return infer(msg) return infer(msg)
def icao(msg): def icao(msg):
import warnings from pyModeS.decoder.common import icao
from pyModeS.decoder.modes import icao
warnings.simplefilter('always', DeprecationWarning)
warnings.warn("pms.ehs.icao() deprecated, please use pms.modes.icao() instead", DeprecationWarning)
return icao(msg) return icao(msg)

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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

View File

@ -6,7 +6,7 @@ import curses
import numpy as np import numpy as np
import time import time
from threading import Lock from threading import Lock
from pyModeS.decoder import util import pyModeS as pms
from pyModeS.extra.beastclient import BaseClient from pyModeS.extra.beastclient import BaseClient
from pyModeS.streamer.stream import Stream from pyModeS.streamer.stream import Stream
from pyModeS.streamer.screen import Screen from pyModeS.streamer.screen import Screen
@ -43,7 +43,7 @@ class ModesClient(BaseClient):
if len(msg) < 28: # only process long messages if len(msg) < 28: # only process long messages
continue continue
df = util.df(msg) df = pms.df(msg)
if df == 17 or df == 18: if df == 17 or df == 18:
local_buffer_adsb_msg.append(msg) local_buffer_adsb_msg.append(msg)

View File

@ -58,8 +58,8 @@ def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F") vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F") vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000") vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, 182.9, -832, 'GS') assert vgs == (159, 182.88, -832, 'GS')
assert vas == (376, 244.0, -2304, 'AS') assert vas == (375, 243.98, -2304, 'TAS')
assert vgs_surface == (19.0, 42.2, 0 , 'GS') assert vgs_surface == (19.0, 42.2, 0 , 'GS')
assert adsb.altitude_diff('8D485020994409940838175B284F') == 550 assert adsb.altitude_diff('8D485020994409940838175B284F') == 550

View File

@ -1,17 +1,4 @@
from pyModeS import bds, modes from pyModeS import bds, ehs, els
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'
def test_bds_infer(): def test_bds_infer():
assert bds.infer("A0001838201584F23468207CDFA5") == 'BDS20' assert bds.infer("A0001838201584F23468207CDFA5") == 'BDS20'
@ -25,13 +12,18 @@ def test_bds_is50or60():
assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == 'BDS60' assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == 'BDS60'
def test_els_BDS20_callsign(): def test_els_BDS20_callsign():
assert bds.bds20.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_' assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert bds.bds20.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_' assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
assert els.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert els.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
def test_ehs_BDS40_functions(): def test_ehs_BDS40_functions():
assert bds.bds40.alt40mcp("A000029C85E42F313000007047D3") == 3008 assert bds.bds40.alt40mcp("A000029C85E42F313000007047D3") == 3008
assert bds.bds40.alt40fms("A000029C85E42F313000007047D3") == 3008 assert bds.bds40.alt40fms("A000029C85E42F313000007047D3") == 3008
assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0 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(): def test_ehs_BDS50_functions():
assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1 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.gs50("A000139381951536E024D4CCF6B5") == 438
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125 assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
assert bds.bds50.tas50("A000139381951536E024D4CCF6B5") == 424 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(): def test_ehs_BDS60_functions():
assert bds.bds60.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715 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.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
assert bds.bds60.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920 assert bds.bds60.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
assert bds.bds60.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920 assert bds.bds60.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
assert ehs.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
def test_graycode_to_altitude(): assert ehs.ias60("A00004128F39F91A7E27C46ADC21") == 252
assert modes.gray2alt('00000000010') == -1000 assert ehs.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
assert modes.gray2alt('00000001010') == -500 assert ehs.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
assert modes.gray2alt('00000011011') == -100 assert ehs.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
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

42
tests/test_common.py Normal file
View File

@ -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

View File

@ -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