19 Commits

Author SHA1 Message Date
Junzi Sun
220b8e9716 update setup and test code 2016-10-26 21:03:14 +02:00
Junzi Sun
3691d6f73d fix vertical rate, scale 64 times 2016-10-26 20:52:33 +02:00
Junzi Sun
dad1cd89a9 Merge pull request #8 from rwnobrega/patch-1
Fix typos and remove redundant info in README.rst
2016-10-26 14:49:40 +02:00
Roberto Nobrega
3b3fc27a42 Fix typos and remove redundant info in README.rst 2016-10-25 17:26:09 -02:00
junzis
4bafa1de19 complete surface position decoding 2016-10-19 16:22:28 +02:00
junzis
b648f4e7a5 add EHS BDS44 decoding (not reliable though...) 2016-10-05 17:37:50 +02:00
junzis
a08c91a3a1 fix imports again and rename files 2016-10-05 15:40:56 +02:00
junzis
567fcda931 make tox to /tmp 2016-10-03 22:34:42 +02:00
Junzi Sun
56dbb618c6 use relative imports within pyModeS package 2016-10-03 21:31:22 +02:00
Junzi Sun
58447346aa Merge pull request #7 from watterso/watterso_refactor_automated_testing_merge
merge in watterso_setup_automated_testing
2016-10-03 21:29:50 +02:00
Junzi Sun
bccc319856 Merge branch 'master' of github.com:junzis/pyModeS 2016-10-03 21:18:41 +02:00
Junzi Sun
375041717b ehs new function 2016-10-03 21:18:17 +02:00
James Watterson
23192a97fd merge in watterso_setup_automated_testing
added .idea to .gitignore for pycharm
pulled tests out of run.py, created adsb_test, ehs_test, and util test
moved the rest of runp.py into decode_test_data
changed imports and __init__ file so it doesn't contain magic
2016-09-11 20:48:43 -07:00
Junzi Sun
240f706e81 update BDS(msg) docstring 2016-08-23 23:02:56 +02:00
junzis
168acfb88d update README and docs 2016-08-16 17:07:02 +02:00
junzis
70e9aa7c8d update docstrings 2016-08-16 16:59:11 +02:00
junzis
fc286299ec fix docstrings 2016-08-16 16:48:13 +02:00
junzis
700b290047 remove doc theme 2016-08-16 16:40:27 +02:00
junzis
55fbdcd029 rename doc files 2016-08-16 16:36:55 +02:00
17 changed files with 642 additions and 316 deletions

3
.gitignore vendored
View File

@@ -54,3 +54,6 @@ docs/_build/
# PyBuilder
target/
# PyCharm
.idea/

View File

@@ -1,29 +1,31 @@
A Python Mode-S Decoder
=======================
Python library for Mode-S message decoding. Two seprate methods are
develop to decode the following messages:
Python library for Mode-S message decoding. Two separate methods are
implemented to decode the following messages:
- Automatic Dependent Surveillance - Broadcast (ADS-B) (DF17)
- aircraft infomation that cotains: icao address, position,
altitude, velocity (ground speed), and callsign, etc.
- aircraft information that contains: ICAO address, position,
altitude, velocity (ground speed), callsign, etc.
- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21)
- additional information in response to SSR interogation, such as:
- additional information in response to SSR interrogation, such as:
true airspeed, indicated airspeed, mach number, track angle,
heading, and roll angle, etc.
heading, roll angle, etc.
A detailed manuel on Mode-S decoding is published by the author, at:
http://adsb-decode-guide.readthedocs.org
A detailed manual on Mode-S decoding is published by the author, at:
http://adsb-decode-guide.readthedocs.io
Source code
-----------
Checkourt and contribute to this open source project at:
Checkout and contribute to this open source project at:
https://github.com/junzis/pyModeS
API documentation at:
http://pymodes.readthedocs.io
Install
-------
@@ -47,7 +49,7 @@ Common function for Mode-S message:
.. code:: python
pms.df(msg) # Downlink Format
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.bin2int(str) # Convert binary string to integer
@@ -60,21 +62,30 @@ Core functions for ADS-B decoding:
pms.adsb.icao(msg)
pms.adsb.callsign(msg)
pms.adsb.position(msg_even, msg_odd, t_even, t_odd)
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)
pms.adsb.position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.surface_position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.altitude(msg)
pms.adsb.velocity(msg)
pms.adsb.speed_heading(msg)
**Hint: When you have a fix position of the aircraft or you know the
location of your receiver, it is convinent to use `position_with_ref()` method
to decode with only one position message (either odd or even)**
**Hint: When you have a fix position of the aircraft, it is convenient to
use `position_with_ref()` method to decode with only one position message
(either odd or even). This works with both airborne and surface position
messages. But the reference position shall be with in 180NM (airborne)
or 45NM (surface) of the true position.**
Core functions for EHS decoding:
.. code:: python
pms.ehs.icao(msg) # icao address
pms.ehs.icao(msg) # ICAO address
pms.ehs.BDS(msg) # Comm-B Data Selector Version
# for BDS version 2,0
@@ -99,11 +110,9 @@ Core functions for EHS decoding:
pms.ehs.baro_vr(msg) # barometric altitude rate (ft/min)
pms.ehs.ins_vr(msg) # inertial vertical speed (ft/min)
Some helper functions:
.. code:: python
pms.df(msg) # downlink format of a Mode-S message
pms.hex2bin(msg) # convert hexadecimal string to binary string
pms.hex2int(msg) # convert hexadecimal string to integer
pms.bin2int(msg) # convert binary string to integer
Developement
------------
To run tests, run the following commands:
```
$ tox
```

View File

@@ -50,7 +50,7 @@ source_suffix = '.rst'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'pyModeS'
master_doc = 'index'
# General information about the project.
project = u'pyModeS'
@@ -124,7 +124,7 @@ todo_include_todos = True
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View File

@@ -4,9 +4,13 @@
contain the root `toctree` directive.
pyModeS API documents
pyModeS APIs
=====================
This document contains all the functions within pyModeS package.
Source code and user guide: https://github.com/junzis/pyModeS
pyModeS.adsb module
-------------------

View File

@@ -1,31 +1,32 @@
# 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/>.
"""
A python package for decoding ABS-D messages.
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/>.
"""
import math
from . import util
from .util import crc
def df(msg):
"""Get the downlink format (DF) number
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: DF number
"""
@@ -34,8 +35,10 @@ def 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
"""
@@ -49,8 +52,10 @@ def data(msg):
def typecode(msg):
"""Type code of ADS-B message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: type code number
"""
@@ -63,8 +68,10 @@ def typecode(msg):
# ---------------------------------------------
def category(msg):
"""Aircraft category number
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: category number
"""
@@ -77,8 +84,10 @@ def category(msg):
def callsign(msg):
"""Aircraft callsign
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
string: callsign
"""
@@ -112,8 +121,10 @@ def callsign(msg):
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
"""
@@ -126,8 +137,10 @@ def oe_flag(msg):
def cprlat(msg):
"""CPR encoded latitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: encoded latitude
"""
@@ -140,8 +153,10 @@ def cprlat(msg):
def cprlon(msg):
"""CPR encoded longitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: encoded longitude
"""
@@ -152,9 +167,26 @@ def cprlon(msg):
return util.bin2int(msgbin[71:88])
def position(msg0, msg1, t0, t1):
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
"""Decode position from a pair of even and odd position message
(works with both airborne and surface position messages)
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
"""
if (5 <= typecode(msg0) <= 8 and 5 <= typecode(msg1) <= 8):
return surface_position(msg0, msg1, t0, t1)
if (not lat_ref) or (not lon_ref):
raise RuntimeError("Surface position encountered, a reference \
position lat/lon required. Location of \
receiver can be used.")
else:
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
elif (9 <= typecode(msg0) <= 18 and 9 <= typecode(msg1) <= 18):
return airborne_position(msg0, msg1, t0, t1)
@@ -165,12 +197,13 @@ def position(msg0, msg1, t0, t1):
def airborne_position(msg0, msg1, t0, t1):
"""Decode airborn position from a pair of even and odd position message
131072 is 2^17, since CPR lat and lon are 17 bits each.
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
"""
@@ -178,6 +211,7 @@ def airborne_position(msg0, msg1, t0, t1):
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
@@ -204,17 +238,17 @@ def airborne_position(msg0, msg1, t0, t1):
# compute ni, longitude index m, and longitude
if (t0 > t1):
ni = max(_cprNL(lat_even), 1)
m = util.floor(cprlon_even * (_cprNL(lat_even)-1) -
cprlon_odd * _cprNL(lat_even) + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even)
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:
ni = max(_cprNL(lat_odd) - 1, 1)
m = util.floor(cprlon_even * (_cprNL(lat_odd)-1) -
cprlon_odd * _cprNL(lat_odd) + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_odd)
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
@@ -223,6 +257,22 @@ def airborne_position(msg0, msg1, t0, t1):
def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message,
knowing reference nearby location, such as previously
calculated location, ground station, or airport location, etc.
Works with both airborne and surface position messages.
The reference position shall be with in 180NM (airborne) or 45NM (surface)
of the true position.
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
"""
if 5 <= typecode(msg) <= 8:
return airborne_position_with_ref(msg, lat_ref, lon_ref)
@@ -234,12 +284,16 @@ def position_with_ref(msg, lat_ref, lon_ref):
def airborne_position_with_ref(msg, lat_ref, lon_ref):
"""Decode airborn position with one message,
knowing previous reference location
"""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
"""
@@ -271,19 +325,89 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
return round(lat, 5), round(lon, 5)
def surface_position(msg0, msg1, t0, t1):
# TODO: implement surface positon
raise RuntimeError('suface position decoding to be implemented soon...')
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 one message,
"""Decode surface position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc.
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
"""
@@ -316,6 +440,8 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
def _cprNL(lat):
"""NL() function in CPR decoding
"""
if lat == 0:
return 59
@@ -335,8 +461,10 @@ def _cprNL(lat):
def altitude(msg):
"""Decode aircraft altitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: altitude in feet
"""
@@ -355,8 +483,10 @@ def altitude(msg):
def nic(msg):
"""Calculate NIC, navigation integrity category
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: NIC number (from 0 to 11), -1 if not applicable
"""
@@ -404,8 +534,10 @@ def nic(msg):
def velocity(msg):
"""Calculate the speed, heading, and vertical rate
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): speed (kt), heading (degree),
rate of climb/descend (ft/min), and speed type
@@ -444,16 +576,18 @@ def velocity(msg):
tag = 'AS'
vr_sign = util.bin2int(msgbin[68])
vr = util.bin2int(msgbin[69:78]) # vertical rate
rocd = -1*vr if vr_sign else vr # rate of climb/descend
vr = (util.bin2int(msgbin[69:78]) - 1) * 64 # vertical rate, fpm
rocd = -1*vr if vr_sign else vr # rate of climb/descend
return int(spd), round(hdg, 1), int(rocd), tag
def speed_heading(msg):
"""Get speed and heading only from the velocity message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), heading (degree)
"""

View File

@@ -1,20 +1,20 @@
# Copyright (C) 2016 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/>.
"""
A python package for decoding ModeS (DF20, DF21) messages.
Copyright (C) 2016 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/>.
"""
from . import util
@@ -23,8 +23,10 @@ from .util import crc
def df(msg):
"""Get the downlink format (DF) number
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: DF number
"""
@@ -38,9 +40,11 @@ def data(msg):
def icao(msg):
"""Calculate the ICAO address from an Mode-S message
with DF4, DF5, DF20, DF21
with DF4, DF5, DF20, DF21
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
@@ -57,8 +61,9 @@ def icao(msg):
def checkbits(data, sb, msb, lsb):
"""Check if the status bit and field bits are consistency. This Function
is used for checking BDS code versions.
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])
@@ -76,11 +81,14 @@ def checkbits(data, sb, msb, lsb):
def isBDS20(msg):
"""Check if a message is likely to be BDS code 2,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
# status bit 1, 14, and 27
d = util.hex2bin(data(msg))
@@ -99,8 +107,10 @@ def isBDS20(msg):
def callsign(msg):
"""Aircraft callsign
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
string: callsign, max. 8 chars
"""
@@ -127,11 +137,14 @@ def callsign(msg):
def isBDS40(msg):
"""Check if a message is likely to be BDS code 4,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
# status bit 1, 14, and 27
d = util.hex2bin(data(msg))
@@ -152,8 +165,10 @@ def isBDS40(msg):
def alt_mcp(msg):
"""Selected altitude, MCP/FCU
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
int: altitude in feet
"""
@@ -164,8 +179,10 @@ def alt_mcp(msg):
def alt_fms(msg):
"""Selected altitude, FMS
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
int: altitude in feet
"""
@@ -176,8 +193,10 @@ def alt_fms(msg):
def pbaro(msg):
"""Barometric pressure setting
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
float: pressure in millibar
"""
@@ -186,17 +205,138 @@ def pbaro(msg):
return p
# ------------------------------------------
# DF 20/21, BDS 4,4
# ------------------------------------------
def isBDS44(msg):
"""Check if a message is likely to be BDS code 4,4
Meteorological routine air report
WARNING: there is two different definition for BDS4,4. This part of the
decoder is likely to be wrong...
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
# status bit 5, 15, 24, 36, 49
d = util.hex2bin(data(msg))
result = True
# --- current version ---
result = result & checkbits(d, 5, 6, 23) \
& checkbits(d, 35, 36, 46) & checkbits(d, 47, 48, 49) \
& checkbits(d, 50, 51, 56)
# # --- revised future version ---
# result = result & checkbits(d, 5, 6, 14) \
# & checkbits(d, 15, 16, 23) & checkbits(d, 24, 25, 35) \
# & checkbits(d, 36, 37, 47) & checkbits(d, 49, 50, 56)
if wind(msg) and wind(msg)[0] > 250:
result &= False
# if temperature(msg):
# if temperature(msg) > 60 or temperature(msg) < -80:
# result &= False
return result
def wind(msg):
"""reported wind speed and direction
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
Returns:
(int, float): speed (kt), direction (degree)
"""
d = util.hex2bin(data(msg))
status = int(d[4])
if not status:
return None
speed = util.bin2int(d[5:14]) # knots
direction = util.bin2int(d[14:23]) * 180.0 / 256.0 # degree
return round(speed, 0), round(direction, 1)
def temperature(msg):
"""reported air temperature
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
Returns:
float: tmeperature in Celsius degree
"""
d = util.hex2bin(data(msg))
sign = int(d[23]) # 1 -> minus
temp = util.bin2int(d[24:34]) * 0.25 # celsius
temp = round(temp, 1)
return -1 * temp if sign else temp
def pressure(msg):
"""reported average static pressure
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
Returns:
int: static pressure in hPa
"""
d = util.hex2bin(data(msg))
status = int(d[34])
if not status:
return None
p = util.bin2int(d[35:46]) # hPa
return p
def humidity(msg):
"""reported humidity
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
Returns:
float: percentage of humidity, [0 - 100] %
"""
d = util.hex2bin(data(msg))
status = int(d[49])
if not status:
return None
hm = util.bin2int(d[50:56]) * 100.0 / 64 # %
return round(hm, 1)
# ------------------------------------------
# DF 20/21, BDS 5,0
# ------------------------------------------
def isBDS50(msg):
"""Check if a message is likely to be BDS code 5,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
# status bit 1, 12, 24, 35, 46
d = util.hex2bin(data(msg))
@@ -226,8 +366,10 @@ def isBDS50(msg):
def roll(msg):
"""Aircraft roll angle
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
float: angle in degrees,
negative->left wing down, positive->right wing down
@@ -241,8 +383,10 @@ def roll(msg):
def track(msg):
"""True track angle
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
float: angle in degrees to true north (from 0 to 360)
"""
@@ -255,8 +399,10 @@ def track(msg):
def gs(msg):
"""Aircraft ground speed
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
int: ground speed in knots
"""
@@ -267,8 +413,10 @@ def gs(msg):
def rtrack(msg):
"""Track angle rate
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
float: angle rate in degrees/second
"""
@@ -281,8 +429,10 @@ def rtrack(msg):
def tas(msg):
"""Aircraft true airspeed
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
int: true airspeed in knots
"""
@@ -297,8 +447,10 @@ def tas(msg):
def isBDS60(msg):
"""Check if a message is likely to be BDS code 6,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
@@ -328,8 +480,10 @@ def isBDS60(msg):
def heading(msg):
"""Megnetic heading of aircraft
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
float: heading in degrees to megnetic north (from 0 to 360)
"""
@@ -342,8 +496,10 @@ def heading(msg):
def ias(msg):
"""Indicated airspeed
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: indicated airspeed in knots
"""
@@ -354,8 +510,10 @@ def ias(msg):
def mach(msg):
"""Aircraft MACH number
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
float: MACH number
"""
@@ -366,8 +524,10 @@ def mach(msg):
def baro_vr(msg):
"""Vertical rate from barometric measurement
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
@@ -380,8 +540,10 @@ def baro_vr(msg):
def ins_vr(msg):
"""Vertical rate messured by onbard equiments (IRS, AHRS)
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
@@ -394,23 +556,23 @@ def ins_vr(msg):
def BDS(msg):
"""Estimate the most likely BDS code of an message
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String|None: Version: "BDS40", "BDS50", or "BDS60". Or None, if nothing
matched
String or None: Version: "BDS20", "BDS40", "BDS50", or "BDS60". Or None, if nothing matched
"""
is2 = isBDS20(msg)
is4 = isBDS40(msg)
is5 = isBDS50(msg)
is6 = isBDS60(msg)
if is2 and not is4 and not is5 and not is6:
return "BDS20"
elif not is2 and is4 and not is5 and not is6:
return "BDS40"
elif not is2 and not is4 and is5 and not is6:
return "BDS50"
elif not is2 and not is4 and not is5 and is6:
return "BDS60"
is20 = isBDS20(msg)
is44 = isBDS44(msg)
is40 = isBDS40(msg)
is50 = isBDS50(msg)
is60 = isBDS60(msg)
BDS = ["BDS20", "BDS40", "BDS44", "BDS50", "BDS60"]
isBDS = [is20, is40, is44, is50, is60]
if sum(isBDS) == 1:
return BDS[isBDS.index(True)]
else:
return None

View File

@@ -1,22 +1,24 @@
# 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
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/>.
"""
import math
# the polynominal generattor code for CRC
@@ -32,10 +34,12 @@ def hex2bin(hexstr):
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)
@@ -75,7 +79,9 @@ def crc(msg, encode=False):
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
Defined as the greatest integer value k, such that k <= x
eg.: floor(3.6) = 3, while floor(-3.6) = -4
"""
return int(math.floor(x))

View File

@@ -23,7 +23,7 @@ setup(
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version='1.0.5',
version='1.0.7',
description='Python Mode-S Decoder',
long_description=long_description,

View File

@@ -1,3 +0,0 @@
# the inclusion of the tests module is not meant to offer best practices for
# testing in general, but rather to support the `find_packages` example in
# setup.py that excludes installing the "tests" package

View File

@@ -1,202 +0,0 @@
import os, sys, inspect
currentdir = os.path.dirname(os.path.abspath(
inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
import pyModeS as pms
from pyModeS import adsb
from pyModeS import ehs
from pyModeS import util
# === TEST common functions ===
def test_hex2bin():
assert util.hex2bin('6E406B') == "011011100100000001101011"
def test_crc():
# crc decoder
checksum = util.crc("8D406B902015A678D4D220AA4BDA")
assert checksum == "000000000000000000000000"
# crc encoder
parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert util.hex2bin("AA4BDA") == parity
# === TEST ADS-B package ===
def test_adsb_icao():
assert adsb.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
def test_adsb_category():
assert adsb.category("8D406B902015A678D4D220AA4BDA") == 5
def test_adsb_callsign():
assert adsb.callsign("8D406B902015A678D4D220AA4BDA") == "EZY85MH_"
def test_adsb_airborne_position():
pos = adsb.airborne_position("8D40058B58C901375147EFD09357",
"8D40058B58C904A87F402D3B8C59",
1446332400, 1446332405)
assert pos == (49.81755, 6.08442)
def test_adsb_airborne_position_with_ref():
pos = adsb.airborne_position_with_ref("8D40058B58C901375147EFD09357",
49.0, 6.0)
assert pos == (49.82410, 6.06785)
pos = adsb.airborne_position_with_ref("8D40058B58C904A87F402D3B8C59",
49.0, 6.0)
assert pos == (49.81755, 6.08442)
def test_adsb_surface_position_with_ref():
pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B22B4A8",
-43.5, 172.5)
assert pos == (-43.48564, 175.87195)
def test_adsb_alt():
assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000
def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
assert vgs == (159, 182.9, -14, 'GS')
assert vas == (376, 244.0, -37, 'AS')
def test_nic():
assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11
# === TEST Mode-S EHS package ===
def test_ehs_icao():
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
def test_ehs_BDS():
assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
assert ehs.BDS("A000029CFFBAA11E2004727281F1") == 'BDS60'
assert ehs.BDS("A0281838CAE9E12FA03FFF2DDDE5") is None
def test_ehs_BDS20_callsign():
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert ehs.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
def test_ehs_BDS40_functions():
assert ehs.alt_mcp("A000029C85E42F313000007047D3") == 3008
assert ehs.alt_fms("A000029C85E42F313000007047D3") == 3008
assert ehs.pbaro("A000029C85E42F313000007047D3") == 1020.0
def test_ehs_BDS50_functions():
assert ehs.roll("A000139381951536E024D4CCF6B5") == 2.1
assert ehs.track("A000139381951536E024D4CCF6B5") == 114.3
assert ehs.gs("A000139381951536E024D4CCF6B5") == 438
assert ehs.rtrack("A000139381951536E024D4CCF6B5") == 0.125
assert ehs.tas("A000139381951536E024D4CCF6B5") == 424
def test_ehs_BDS60_functions():
assert ehs.heading("A000029CFFBAA11E2004727281F1") == 180.9
assert ehs.ias("A000029CFFBAA11E2004727281F1") == 336
assert ehs.mach("A000029CFFBAA11E2004727281F1") == 0.48
assert ehs.baro_vr("A000029CFFBAA11E2004727281F1") == 0
assert ehs.ins_vr("A000029CFFBAA11E2004727281F1") == -3648
# === Decode sample data file ===
def adsb_decode_all(n=None):
print("===== Decode all ADS-B sample data=====")
import csv
f = open('adsb.csv', 'rt')
msg0 = None
msg1 = None
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[0]
m = r[1]
icao = adsb.icao(m)
tc = adsb.typecode(m)
if 1 <= tc <= 4:
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
if tc == 19:
print(ts, m, icao, tc, adsb.velocity(m))
if 5 <= tc <= 18:
if adsb.oe_flag(m):
msg1 = m
t1 = ts
else:
msg0 = m
t0 = ts
if msg0 and msg1:
pos = adsb.position(msg0, msg1, t0, t1)
alt = adsb.altitude(m)
print(ts, m, icao, tc, pos, alt)
def ehs_decode_all(n=None):
print("===== Decode all Mode-S EHS sample data=====")
import csv
f = open('ehs.csv', 'rt')
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[1]
m = r[2]
icao = ehs.icao(m)
vBDS = ehs.BDS(m)
if vBDS:
if vBDS == "BDS20":
print(ts, m, icao, vBDS, ehs.callsign(m))
if vBDS == "BDS40":
print(ts, m, icao, vBDS, ehs.alt_mcp(m), \
ehs.alt_fms(m), ehs.pbaro(m))
if vBDS == "BDS50":
print(ts, m, icao, vBDS, ehs.roll(m), ehs.track(m), \
ehs.gs(m), ehs.rtrack(m), ehs.tas(m))
if vBDS == "BDS60":
print(ts, m, icao, vBDS, ehs.heading(m), ehs.ias(m), \
ehs.mach(m), ehs.baro_vr(m), ehs.ins_vr(m))
else:
print(ts, m, icao, vBDS)
if __name__ == '__main__':
adsb_decode_all(100)
ehs_decode_all(100)

79
tests/sample_data_test.py Normal file
View File

@@ -0,0 +1,79 @@
# If you get import error run with ipython
from pyModeS import adsb
from pyModeS import ehs
from pyModeS import util
# === Decode sample data file ===
def adsb_decode_all(n=None):
print("===== Decode all ADS-B sample data=====")
import csv
f = open('tests/sample_data_adsb.csv', 'rt')
msg0 = None
msg1 = None
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[0]
m = r[1]
icao = adsb.icao(m)
tc = adsb.typecode(m)
if 1 <= tc <= 4:
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
if tc == 19:
print(ts, m, icao, tc, adsb.velocity(m))
if 5 <= tc <= 18:
if adsb.oe_flag(m):
msg1 = m
t1 = ts
else:
msg0 = m
t0 = ts
if msg0 and msg1:
pos = adsb.position(msg0, msg1, t0, t1)
alt = adsb.altitude(m)
print(ts, m, icao, tc, pos, alt)
def ehs_decode_all(n=None):
print("===== Decode all Mode-S EHS sample data=====")
import csv
f = open('tests/sample_data_ehs.csv', 'rt')
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[1]
m = r[2]
icao = ehs.icao(m)
vBDS = ehs.BDS(m)
if vBDS:
if vBDS == "BDS20":
print(ts, m, icao, vBDS, ehs.callsign(m))
if vBDS == "BDS40":
print(ts, m, icao, vBDS, ehs.alt_mcp(m),
ehs.alt_fms(m), ehs.pbaro(m))
if vBDS == "BDS44":
print(ts, m, icao, vBDS, ehs.wind(m))
if vBDS == "BDS50":
print(ts, m, icao, vBDS, ehs.roll(m), ehs.track(m),
ehs.gs(m), ehs.rtrack(m), ehs.tas(m))
if vBDS == "BDS60":
print(ts, m, icao, vBDS, ehs.heading(m), ehs.ias(m),
ehs.mach(m), ehs.baro_vr(m), ehs.ins_vr(m))
else:
print(ts, m, icao, 'UNKNOWN')
if __name__ == '__main__':
adsb_decode_all(100)
ehs_decode_all(100)

70
tests/test_adsb.py Normal file
View File

@@ -0,0 +1,70 @@
from pyModeS import adsb
# === TEST ADS-B package ===
def test_adsb_icao():
assert adsb.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
def test_adsb_category():
assert adsb.category("8D406B902015A678D4D220AA4BDA") == 5
def test_adsb_callsign():
assert adsb.callsign("8D406B902015A678D4D220AA4BDA") == "EZY85MH_"
def test_adsb_position():
pos = adsb.position("8D40058B58C901375147EFD09357",
"8D40058B58C904A87F402D3B8C59",
1446332400, 1446332405)
assert pos == (49.81755, 6.08442)
def test_adsb_airborne_position_with_ref():
pos = adsb.airborne_position_with_ref("8D40058B58C901375147EFD09357",
49.0, 6.0)
assert pos == (49.82410, 6.06785)
pos = adsb.airborne_position_with_ref("8D40058B58C904A87F402D3B8C59",
49.0, 6.0)
assert pos == (49.81755, 6.08442)
def test_adsb_surface_position_with_ref():
pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B22B4A8",
-43.5, 172.5)
assert pos == (-43.48564, 175.87195)
def test_adsb_surface_position():
pos = adsb.surface_position("8CC8200A3AC8F009BCDEF2000000",
"8FC8200A3AB8F5F893096B000000",
0, 2,
-43.496, 172.558)
assert pos == (-43.48564, 172.53942)
def test_adsb_alt():
assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000
def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
assert vgs == (159, 182.9, -832, 'GS')
assert vas == (376, 244.0, -2304, 'AS')
def test_nic():
assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11

43
tests/test_ehs.py Normal file
View File

@@ -0,0 +1,43 @@
from pyModeS import ehs
def test_ehs_icao():
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
def test_ehs_BDS():
assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
assert ehs.BDS("A000029CFFBAA11E2004727281F1") == 'BDS60'
assert ehs.BDS("A0281838CAE9E12FA03FFF2DDDE5") == 'BDS44'
assert ehs.BDS("A00017B0C8480030A4000024512F") is None
def test_ehs_BDS20_callsign():
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert ehs.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
def test_ehs_BDS40_functions():
assert ehs.alt_mcp("A000029C85E42F313000007047D3") == 3008
assert ehs.alt_fms("A000029C85E42F313000007047D3") == 3008
assert ehs.pbaro("A000029C85E42F313000007047D3") == 1020.0
def test_ehs_BDS50_functions():
assert ehs.roll("A000139381951536E024D4CCF6B5") == 2.1
assert ehs.track("A000139381951536E024D4CCF6B5") == 114.3
assert ehs.gs("A000139381951536E024D4CCF6B5") == 438
assert ehs.rtrack("A000139381951536E024D4CCF6B5") == 0.125
assert ehs.tas("A000139381951536E024D4CCF6B5") == 424
def test_ehs_BDS60_functions():
assert ehs.heading("A000029CFFBAA11E2004727281F1") == 180.9
assert ehs.ias("A000029CFFBAA11E2004727281F1") == 336
assert ehs.mach("A000029CFFBAA11E2004727281F1") == 0.48
assert ehs.baro_vr("A000029CFFBAA11E2004727281F1") == 0
assert ehs.ins_vr("A000029CFFBAA11E2004727281F1") == -3648

15
tests/test_util.py Normal file
View File

@@ -0,0 +1,15 @@
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

6
tox.ini Normal file
View File

@@ -0,0 +1,6 @@
[tox]
toxworkdir=/tmp/tox
envlist = py26,py27,py35
[testenv]
deps=pytest
commands=py.test