Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c52db318d | ||
|
|
e692d30d66 | ||
|
|
fd8bb8386f | ||
|
|
2b1f2a5878 | ||
|
|
3538645e22 | ||
|
|
a9887d6238 | ||
|
|
7c8fd74db7 | ||
|
|
35b0d63fa9 | ||
|
|
2ae7bf4c19 | ||
|
|
e8449154ca | ||
|
|
fbe5b63286 | ||
|
|
45c32cd7aa | ||
|
|
c708d57fcc | ||
|
|
53a258bd35 | ||
|
|
d9e277dc54 | ||
|
|
854386fbd4 | ||
|
|
3117febac0 | ||
|
|
8693c51998 | ||
|
|
8c90371111 | ||
|
|
c6952e4e63 | ||
|
|
98e5d81ae1 | ||
|
|
fd557d1c40 | ||
|
|
cdbcf47bc2 | ||
|
|
5f7e28950c | ||
|
|
c1d0a925d5 | ||
|
|
b0a71717f0 | ||
|
|
25e5a4e412 | ||
|
|
d3022c6fe5 | ||
|
|
aa9f49b470 | ||
|
|
27daf52850 | ||
|
|
0e29a4d18a | ||
|
|
1e842e4789 | ||
|
|
1220368ada | ||
|
|
fb32ace095 | ||
|
|
abafd97b3f | ||
|
|
0a38231713 | ||
|
|
2fd822d275 | ||
|
|
8de58bb01f | ||
|
|
140f312c11 | ||
|
|
fefd26a787 | ||
|
|
1e82b5c59c | ||
|
|
a1615767b6 | ||
|
|
03a62dc68c | ||
|
|
46fee6b7dc | ||
|
|
8aa821c8dd | ||
|
|
621d3e7580 | ||
|
|
3b3609bf2b | ||
|
|
ef2268127c | ||
|
|
cc66e2f4e4 | ||
|
|
128163b41d | ||
|
|
8933afb1c1 | ||
|
|
15f2833aee | ||
|
|
2b3e2c62d0 | ||
|
|
cadcbb1756 | ||
|
|
b9c6db6f65 | ||
|
|
220b8e9716 | ||
|
|
3691d6f73d | ||
|
|
dad1cd89a9 | ||
|
|
3b3fc27a42 | ||
|
|
4bafa1de19 | ||
|
|
b648f4e7a5 | ||
|
|
a08c91a3a1 | ||
|
|
567fcda931 | ||
|
|
56dbb618c6 | ||
|
|
58447346aa | ||
|
|
bccc319856 | ||
|
|
375041717b | ||
|
|
23192a97fd | ||
|
|
240f706e81 | ||
|
|
168acfb88d | ||
|
|
70e9aa7c8d | ||
|
|
fc286299ec | ||
|
|
700b290047 | ||
|
|
55fbdcd029 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -54,3 +54,6 @@ docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
193
README.rst
193
README.rst
@@ -1,109 +1,182 @@
|
||||
A Python Mode-S Decoder
|
||||
=======================
|
||||
The 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. Support Downlink Formats (DF) are:
|
||||
|
||||
- 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)
|
||||
- Mode-S Elementary Surveillance (ELS) (DF4 and DF5).
|
||||
|
||||
- additional information in response to SSR interogation, such as:
|
||||
true airspeed, indicated airspeed, mach number, track angle,
|
||||
heading, and roll angle, etc.
|
||||
- DF4: Altitude
|
||||
- DF5: Squawk code
|
||||
|
||||
A detailed manuel on Mode-S decoding is published by the author, at:
|
||||
http://adsb-decode-guide.readthedocs.org
|
||||
- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21). Additional information in response to SSR interrogation, such as: true airspeed, indicated airspeed, mach number, wind, temperature, etc.
|
||||
|
||||
- DF20: Altitude
|
||||
- DF21: Squawk code
|
||||
- BDS 2,0 Aircraft identification
|
||||
- BDS 2,1 Aircraft and airline registration markings
|
||||
- BDS 4,0 Selected vertical intention
|
||||
- BDS 4,4 Meteorological routine air report
|
||||
- BDS 5,0 Track and turn report
|
||||
- BDS 5,3 Air-referenced state vector
|
||||
- BDS 6,0 Heading and speed report
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
Checkout source code, or install using pip:
|
||||
The easiest installation is to use pip:
|
||||
|
||||
::
|
||||
|
||||
pip install pyModeS
|
||||
pip install pyModeS
|
||||
|
||||
Usage
|
||||
-----
|
||||
To install latest devlopment version from the GitHub:
|
||||
|
||||
::
|
||||
|
||||
pip install git+https://github.com/junzis/pyModeS
|
||||
|
||||
|
||||
Use the library
|
||||
---------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pyModeS as pms
|
||||
import pyModeS as pms
|
||||
|
||||
|
||||
Common function for Mode-S message:
|
||||
Common functions:
|
||||
*****************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.df(msg) # Downlink Format
|
||||
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
|
||||
pms.df(msg) # Downlink Format
|
||||
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
|
||||
|
||||
pms.hex2bin(str) # Convert hexadecimal string to binary string
|
||||
pms.bin2int(str) # Convert binary string to integer
|
||||
pms.hex2int(str) # Convert hexadecimal string to integer
|
||||
pms.hex2bin(str) # Convert hexadecimal string to binary string
|
||||
pms.bin2int(str) # Convert binary string to integer
|
||||
pms.hex2int(str) # Convert hexadecimal string to integer
|
||||
|
||||
pms.gray2int(str) # Convert grey code to interger
|
||||
|
||||
|
||||
Core functions for ADS-B decoding:
|
||||
**********************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.adsb.icao(msg)
|
||||
pms.adsb.callsign(msg)
|
||||
pms.adsb.position(msg_even, msg_odd, t_even, t_odd)
|
||||
pms.adsb.position_with_ref(msg, lat_ref, lon_ref)
|
||||
pms.adsb.altitude(msg)
|
||||
pms.adsb.velocity(msg)
|
||||
pms.adsb.speed_heading(msg)
|
||||
pms.adsb.icao(msg)
|
||||
pms.adsb.typecode(msg)
|
||||
|
||||
# typecode 1-4
|
||||
pms.adsb.callsign(msg)
|
||||
|
||||
# typecode 5-8 (surface) and 9-18 (airborne)
|
||||
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)
|
||||
|
||||
# typecode: 19
|
||||
pms.adsb.velocity(msg) # handles both surface & airborne messages
|
||||
pms.adsb.speed_heading(msg) # handles both surface & airborne messages
|
||||
pms.adsb.surface_velocity(msg)
|
||||
pms.adsb.airborne_velocity(msg)
|
||||
|
||||
|
||||
Note: 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 ELS decoding:
|
||||
********************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.els.icao(msg) # ICAO address
|
||||
pms.els.df4alt(msg) # Altitude from any DF4 message
|
||||
pms.ehs.df5id(msg) # Squawk code from any DF5 message
|
||||
|
||||
**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)**
|
||||
|
||||
Core functions for EHS decoding:
|
||||
********************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.ehs.icao(msg) # icao address
|
||||
pms.ehs.BDS(msg) # Comm-B Data Selector Version
|
||||
pms.ehs.icao(msg) # ICAO address
|
||||
pms.ehs.df20alt(msg) # Altitude from any DF20 message
|
||||
pms.ehs.df21id(msg) # Squawk code from any DF21 message
|
||||
|
||||
# for BDS version 2,0
|
||||
pms.ehs.callsign(msg) # Aircraft callsign
|
||||
pms.ehs.BDS(msg) # Comm-B Data Selector Version
|
||||
|
||||
# for BDS version 4,0
|
||||
pms.ehs.alt_mcp(msg) # MCP/FCU selected altitude (ft)
|
||||
pms.ehs.alt_fms(msg) # FMS selected altitude (ft)
|
||||
pms.ehs.alt_pbaro(msg) # Barometric pressure (mb)
|
||||
# for BDS version 2,0
|
||||
pms.ehs.isBDS20(msg) # Check if message is BDS 2,0
|
||||
pms.ehs.callsign(msg) # Aircraft callsign
|
||||
|
||||
# for BDS version 5,0
|
||||
pms.ehs.roll(msg) # roll angle (deg)
|
||||
pms.ehs.track(msg) # track angle (deg)
|
||||
pms.ehs.gs(msg) # ground speed (kt)
|
||||
pms.ehs.rtrack(msg) # track angle rate (deg/sec)
|
||||
pms.ehs.tas(msg) # true airspeed (kt)
|
||||
# for BDS version 4,0
|
||||
pms.ehs.isBDS40(msg) # Check if message is BDS 4,0
|
||||
pms.ehs.alt40mcp(msg) # MCP/FCU selected altitude (ft)
|
||||
pms.ehs.alt40fms(msg) # FMS selected altitude (ft)
|
||||
pms.ehs.p40baro(msg) # Barometric pressure (mb)
|
||||
|
||||
# for BDS version 6,0
|
||||
pms.ehs.heading(msg) # heading (deg)
|
||||
pms.ehs.ias(msg) # indicated airspeed (kt)
|
||||
pms.ehs.mach(msg) # MACH number
|
||||
pms.ehs.baro_vr(msg) # barometric altitude rate (ft/min)
|
||||
pms.ehs.ins_vr(msg) # inertial vertical speed (ft/min)
|
||||
# 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 (%)
|
||||
|
||||
Some helper functions:
|
||||
# for BDS version 5,0
|
||||
pms.ehs.isBDS50(msg) # Check if message is BDS 5,0
|
||||
pms.ehs.roll50(msg) # roll angle (deg)
|
||||
pms.ehs.trk50(msg) # track angle (deg)
|
||||
pms.ehs.gs50(msg) # ground speed (kt)
|
||||
pms.ehs.rtrk50(msg) # track angle rate (deg/sec)
|
||||
pms.ehs.tas50(msg) # true airspeed (kt)
|
||||
|
||||
.. code:: python
|
||||
# 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)
|
||||
|
||||
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
|
||||
# for BDS version 6,0
|
||||
pms.ehs.isBDS60(msg) # Check if message is BDS 6,0
|
||||
pms.ehs.hdg60(msg) # heading (deg)
|
||||
pms.ehs.ias60(msg) # indicated airspeed (kt)
|
||||
pms.ehs.mach60(msg) # MACH number
|
||||
pms.ehs.vr60baro(msg) # barometric altitude rate (ft/min)
|
||||
pms.ehs.vr60ins(msg) # inertial vertical speed (ft/min)
|
||||
|
||||
Developement
|
||||
------------
|
||||
To perform unit tests. First install ``tox`` through pip, Then, run the following commands:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ tox
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-------------------
|
||||
@@ -1 +1 @@
|
||||
pyModeS==1.0.5
|
||||
pyModeS==1.1.0
|
||||
|
||||
@@ -3,3 +3,4 @@ from __future__ import absolute_import, print_function, division
|
||||
from .util import *
|
||||
from . import adsb
|
||||
from . import ehs
|
||||
from . import els
|
||||
|
||||
351
pyModeS/adsb.py
351
pyModeS/adsb.py
@@ -1,31 +1,33 @@
|
||||
# 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/>.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
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 +36,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 +53,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 +69,10 @@ def typecode(msg):
|
||||
# ---------------------------------------------
|
||||
def category(msg):
|
||||
"""Aircraft category number
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: category number
|
||||
"""
|
||||
@@ -77,8 +85,10 @@ def category(msg):
|
||||
|
||||
def callsign(msg):
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: callsign
|
||||
"""
|
||||
@@ -112,8 +122,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 +138,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 +154,10 @@ def cprlat(msg):
|
||||
|
||||
def cprlon(msg):
|
||||
"""CPR encoded longitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: encoded longitude
|
||||
"""
|
||||
@@ -152,9 +168,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 +198,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 +212,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 +239,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,23 +258,42 @@ 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:
|
||||
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
|
||||
"""
|
||||
if 5 <= typecode(msg) <= 8:
|
||||
return airborne_position_with_ref(msg, lat_ref, lon_ref)
|
||||
return surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
elif 9 <= typecode(msg) <= 18:
|
||||
return surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||
return airborne_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
|
||||
|
||||
def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode 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
|
||||
"""
|
||||
@@ -303,9 +427,9 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
ni = _cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 360.0 / ni
|
||||
d_lon = 90.0 / ni
|
||||
else:
|
||||
d_lon = 360.0
|
||||
d_lon = 90.0
|
||||
|
||||
m = util.floor(lon_ref / d_lon) \
|
||||
+ util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
@@ -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,14 +461,21 @@ def _cprNL(lat):
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
if typecode(msg) < 9 or typecode(msg) > 18:
|
||||
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
if typecode(msg) >=5 and typecode(msg) <=8:
|
||||
# surface position, altitude 0
|
||||
return 0
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
q = msgbin[47]
|
||||
if q:
|
||||
@@ -355,13 +488,15 @@ 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
|
||||
"""
|
||||
if typecode(msg) < 9 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a airborne position message" % msg)
|
||||
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
tc = typecode(msg)
|
||||
@@ -404,58 +539,158 @@ def nic(msg):
|
||||
|
||||
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), heading (degree),
|
||||
(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" % msg)
|
||||
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 = util.bin2int(msgbin[45])
|
||||
v_ew_sign = -1 if int(msgbin[45]) else 1
|
||||
v_ew = util.bin2int(msgbin[46:56]) - 1 # east-west velocity
|
||||
|
||||
v_ns_sign = util.bin2int(msgbin[56])
|
||||
v_ns_sign = -1 if int(msgbin[56]) else 1
|
||||
v_ns = util.bin2int(msgbin[57:67]) - 1 # north-south velocity
|
||||
|
||||
v_we = -1*v_ew if v_ew_sign else v_ew
|
||||
v_sn = -1*v_ns if v_ns_sign else v_ns
|
||||
|
||||
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
|
||||
|
||||
hdg = math.atan2(v_we, v_sn)
|
||||
hdg = math.degrees(hdg) # convert to degrees
|
||||
hdg = hdg if hdg >= 0 else hdg + 360 # no negative val
|
||||
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 = 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_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(hdg, 1), int(rocd), tag
|
||||
return int(spd), round(trk_or_hdg, 1), int(rocd), tag
|
||||
|
||||
|
||||
def speed_heading(msg):
|
||||
"""Get speed and heading only from the velocity message
|
||||
def surface_velocity(msg):
|
||||
"""Decode surface velocity from from a surface position message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(int, float): speed (kt), heading (degree)
|
||||
(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)
|
||||
"""
|
||||
spd, hdg, rocd, tag = velocity(msg)
|
||||
return spd, hdg
|
||||
|
||||
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.
|
||||
|
||||
799
pyModeS/ehs.py
799
pyModeS/ehs.py
File diff suppressed because it is too large
Load Diff
37
pyModeS/els.py
Normal file
37
pyModeS/els.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from . import util, modes_common
|
||||
|
||||
def icao(msg):
|
||||
return modes_common.icao(msg)
|
||||
|
||||
|
||||
def df4alt(msg):
|
||||
"""Computes the altitude from DF4 message, bit 20-32
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
"""
|
||||
|
||||
if util.df(msg) != 4:
|
||||
raise RuntimeError("Message must be Downlink Format 4.")
|
||||
|
||||
return modes_common.altcode(msg)
|
||||
|
||||
|
||||
def df5id(msg):
|
||||
"""Computes identity (squawk code) from DF5 message, bit 20-32
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: squawk code
|
||||
"""
|
||||
|
||||
if util.df(msg) != 5:
|
||||
raise RuntimeError("Message must be Downlink Format 5.")
|
||||
|
||||
return modes_common.idcode(msg)
|
||||
131
pyModeS/modes_common.py
Normal file
131
pyModeS/modes_common.py
Normal file
@@ -0,0 +1,131 @@
|
||||
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 = util.gray2int(gc500)
|
||||
|
||||
# in 100-ft step must be converted first
|
||||
gc100 = codestr[8:]
|
||||
n100 = util.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
|
||||
@@ -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,19 @@ 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))
|
||||
|
||||
|
||||
def gray2int(graystr):
|
||||
"""Convert greycode to binary (DF4, 20 altitude coding)"""
|
||||
num = bin2int(graystr)
|
||||
num ^= (num >> 8)
|
||||
num ^= (num >> 4)
|
||||
num ^= (num >> 2)
|
||||
num ^= (num >> 1)
|
||||
return num
|
||||
|
||||
9
setup.py
9
setup.py
@@ -3,6 +3,13 @@
|
||||
See:
|
||||
https://packaging.python.org/en/latest/distributing.html
|
||||
https://github.com/pypa/sampleproject
|
||||
|
||||
Steps for deploying a new verison:
|
||||
1. Increase the version number
|
||||
2. remove the old deployment under [dist] folder
|
||||
3. run: python setup.py sdist
|
||||
run: python setup.py bdist_wheel --universal
|
||||
4. twine upload dist/*
|
||||
"""
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
@@ -23,7 +30,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.2.2',
|
||||
|
||||
description='Python Mode-S Decoder',
|
||||
long_description=long_description,
|
||||
|
||||
@@ -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
|
||||
5000
tests/data/sample_data_ehs_df20.csv
Normal file
5000
tests/data/sample_data_ehs_df20.csv
Normal file
File diff suppressed because it is too large
Load Diff
5000
tests/data/sample_data_ehs_df21.csv
Normal file
5000
tests/data/sample_data_ehs_df21.csv
Normal file
File diff suppressed because it is too large
Load Diff
2000
tests/ehs.csv
2000
tests/ehs.csv
File diff suppressed because it is too large
Load Diff
202
tests/run.py
202
tests/run.py
@@ -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)
|
||||
41
tests/sample_run_adsb.py
Normal file
41
tests/sample_run_adsb.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import adsb, ehs, util
|
||||
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
def adsb_decode_all(n=None):
|
||||
print("===== Decode ADS-B sample data=====")
|
||||
import csv
|
||||
f = open('tests/data/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)
|
||||
|
||||
if __name__ == '__main__':
|
||||
adsb_decode_all(n=100)
|
||||
75
tests/sample_run_ehs.py
Normal file
75
tests/sample_run_ehs.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import adsb, ehs, util
|
||||
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
def bds_info(BDS, m):
|
||||
if BDS == "BDS17":
|
||||
info = ([i[-2:] for i in ehs.cap17(m)])
|
||||
|
||||
elif BDS == "BDS20":
|
||||
info = ehs.callsign(m)
|
||||
|
||||
elif BDS == "BDS40":
|
||||
info = (ehs.alt40mcp(m), ehs.alt40fms(m), ehs.p40baro(m))
|
||||
|
||||
elif BDS == "BDS44":
|
||||
info = (ehs.wind44(m), ehs.temp44(m), ehs.p44(m), ehs.hum44(m))
|
||||
|
||||
elif BDS == "BDS44REV":
|
||||
info = (ehs.wind44(m, rev=True), ehs.temp44(m, rev=True), ehs.p44(m, rev=True), ehs.hum44(m, rev=True))
|
||||
|
||||
elif BDS == "BDS50":
|
||||
info = (ehs.roll50(m), ehs.trk50(m), ehs.gs50(m), ehs.rtrk50(m), ehs.tas50(m))
|
||||
|
||||
elif BDS == "BDS53":
|
||||
info = (ehs.hdg53(m), ehs.ias53(m), ehs.mach53(m), ehs.tas53(m), ehs.vr53(m))
|
||||
|
||||
elif BDS == "BDS60":
|
||||
info = (ehs.hdg60(m), ehs.ias60(m), ehs.mach60(m), ehs.vr60baro(m), ehs.vr60ins(m))
|
||||
|
||||
else:
|
||||
info = None
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def ehs_decode_all(df, n=None):
|
||||
import csv
|
||||
|
||||
print("===== Decode EHS sample data (DF=%s)=====" % df)
|
||||
|
||||
f = open('tests/data/sample_data_ehs_df%s.csv' % df, 'rt')
|
||||
|
||||
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
if n and i > n:
|
||||
break
|
||||
|
||||
ts = r[0]
|
||||
m = r[2]
|
||||
|
||||
df = util.df(m)
|
||||
icao = ehs.icao(m)
|
||||
BDS = ehs.BDS(m)
|
||||
code = ehs.df20alt(m) if df==20 else ehs.df21id(m)
|
||||
|
||||
if not BDS:
|
||||
print(ts, m, icao, df, '%5s'%code, 'UNKNOWN')
|
||||
continue
|
||||
|
||||
if isinstance(BDS, list):
|
||||
print(ts, m, icao, df, '%5s'%code, end=' ')
|
||||
for i, bds in enumerate(BDS):
|
||||
if i == 0:
|
||||
print(bds, *bds_info(bds, m))
|
||||
else:
|
||||
print(' '*55, bds, *bds_info(bds, m))
|
||||
|
||||
else:
|
||||
print(ts, m, icao, df, '%5s'%code, BDS, *bds_info(BDS, m))
|
||||
|
||||
if __name__ == '__main__':
|
||||
ehs_decode_all(df=20, n=100)
|
||||
ehs_decode_all(df=21, n=100)
|
||||
79
tests/test_adsb.py
Normal file
79
tests/test_adsb.py
Normal file
@@ -0,0 +1,79 @@
|
||||
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_position_with_ref():
|
||||
pos = adsb.position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0)
|
||||
assert pos == (49.82410, 6.06785)
|
||||
pos = adsb.position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5)
|
||||
assert pos == (-43.48564, 172.53942)
|
||||
|
||||
|
||||
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("8FC8200A3AB8F5F893096B000000",
|
||||
-43.5, 172.5)
|
||||
assert pos == (-43.48564, 172.53942)
|
||||
|
||||
|
||||
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")
|
||||
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
|
||||
assert vgs == (159, 182.9, -832, 'GS')
|
||||
assert vas == (376, 244.0, -2304, 'AS')
|
||||
assert vgs_surface == (19.0, 42.2, 0 , 'GS')
|
||||
assert adsb.altitude_diff('8D485020994409940838175B284F') == 550
|
||||
|
||||
|
||||
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
|
||||
62
tests/test_ehs.py
Normal file
62
tests/test_ehs.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from pyModeS import ehs
|
||||
from pyModeS import modes_common
|
||||
|
||||
def test_ehs_icao():
|
||||
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
|
||||
assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
|
||||
assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
|
||||
|
||||
|
||||
def test_df20alt():
|
||||
assert ehs.df20alt("A02014B400000000000000F9D514") == 32300
|
||||
|
||||
|
||||
def test_ehs_BDS():
|
||||
assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
|
||||
assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
|
||||
# assert ehs.BDS("A000031DBAA9DD18622C441330E9") == 'BDS44'
|
||||
assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
|
||||
assert ehs.BDS("A00004128F39F91A7E27C46ADC21") == 'BDS60'
|
||||
|
||||
def test_ehs_BDS20_callsign():
|
||||
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
|
||||
assert ehs.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
|
||||
|
||||
|
||||
def test_ehs_BDS40_functions():
|
||||
assert ehs.alt40mcp("A000029C85E42F313000007047D3") == 3008
|
||||
assert ehs.alt40fms("A000029C85E42F313000007047D3") == 3008
|
||||
assert ehs.p40baro("A000029C85E42F313000007047D3") == 1020.0
|
||||
|
||||
def test_ehs_BDS50_functions():
|
||||
assert ehs.roll50("A000139381951536E024D4CCF6B5") == 2.1
|
||||
assert ehs.trk50("A000139381951536E024D4CCF6B5") == 114.3
|
||||
assert ehs.gs50("A000139381951536E024D4CCF6B5") == 438
|
||||
assert ehs.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
|
||||
assert ehs.tas50("A000139381951536E024D4CCF6B5") == 424
|
||||
# signed values
|
||||
assert ehs.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4
|
||||
|
||||
def test_ehs_BDS60_functions():
|
||||
assert ehs.hdg60("A00004128F39F91A7E27C46ADC21") == 42.7
|
||||
assert ehs.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
||||
assert ehs.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
||||
assert ehs.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
assert ehs.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
|
||||
def test_graycode_to_altitude():
|
||||
assert modes_common.gray2alt('00000000010') == -1000
|
||||
assert modes_common.gray2alt('00000001010') == -500
|
||||
assert modes_common.gray2alt('00000011011') == -100
|
||||
assert modes_common.gray2alt('00000011010') == 0
|
||||
assert modes_common.gray2alt('00000011110') == 100
|
||||
assert modes_common.gray2alt('00000010011') == 600
|
||||
assert modes_common.gray2alt('00000110010') == 1000
|
||||
assert modes_common.gray2alt('00001001001') == 5800
|
||||
assert modes_common.gray2alt('00011100100') == 10300
|
||||
assert modes_common.gray2alt('01100011010') == 32000
|
||||
assert modes_common.gray2alt('01110000100') == 46300
|
||||
assert modes_common.gray2alt('01010101100') == 50200
|
||||
assert modes_common.gray2alt('11011110100') == 73200
|
||||
assert modes_common.gray2alt('10000000011') == 126600
|
||||
assert modes_common.gray2alt('10000000001') == 126700
|
||||
15
tests/test_util.py
Normal file
15
tests/test_util.py
Normal 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
|
||||
Reference in New Issue
Block a user