2 Commits

Author SHA1 Message Date
Junzi Sun
59d21de725 Merge branch 'master' into adsb_encoder 2020-05-04 01:53:27 +02:00
Junzi Sun
aff0f75de2 BDS08 and BDS09 encoders 2020-05-03 23:29:10 +02:00
48 changed files with 757 additions and 1673 deletions

View File

@@ -1,29 +0,0 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: PyPI Publish
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

View File

@@ -8,18 +8,11 @@ ext:
python setup.py build_ext --inplace
test:
make clean
@echo ""
@echo "[Test with py_common]"
python -m pytest tests
@echo ""
@echo "[Test with c_common]"
python setup.py build_ext --inplace
python -m pytest tests
clean:
find pyModeS -type f -name '*.c' -delete
find pyModeS -type f -name '*.so' -delete
find pyModeS/decoder -type f -name '*.c' -delete
find pyModeS/decoder -type f -name '*.so' -delete
find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf
rm -rf *.egg-info
rm -rf .pytest_cache

View File

@@ -1,7 +1,7 @@
The Python ADS-B/Mode-S Decoder
===============================
PyModeS is a Python library designed to decode Mode-S (including ADS-B) messages. It can be imported to your python project or used as a standalone tool to view and save live traffic data.
PyModeS is a Python library designed to decode Mode-S (including ADS-B) message. It can be imported to your python project or used as a standalone tool to view and save live traffic data.
This is a project created by Junzi Sun, who works at `TU Delft <https://www.tudelft.nl/en/>`_, `Aerospace Engineering Faculty <https://www.tudelft.nl/en/ae/>`_, `CNS/ATM research group <http://cs.lr.tudelft.nl/atm/>`_. It is supported by many `contributors <https://github.com/junzis/pyModeS/graphs/contributors>`_ from different institutions.
@@ -72,35 +72,21 @@ Installation examples::
# stable version
pip install pyModeS
# conda (compiled) version
conda install -c conda-forge pymodes
# development version
pip install git+https://github.com/junzis/pyModeS
Dependencies ``numpy``, and ``pyzmq`` are installed automatically during previous installations processes.
If you need to connect pyModeS to a RTL-SDR receiver, ``pyrtlsdr`` need to be installed manually::
pip install pyrtlsdr
Dependencies ``numpy``, ``pyzmq`` and ``pyrtlsdr`` are installed automatically during previous installations processes.
Advanced installation (using c modules)
------------------------------------------
If you want to make use of the (faster) c module, install ``pyModeS`` as follows::
# conda (compiled) version
conda install -c conda-forge pymodes
# stable version (to be compiled on your side)
pip install pyModeS[fast]
# development version
git clone https://github.com/junzis/pyModeS
cd pyModeS
pip install .[fast]
make ext
make install
View live traffic (modeslive)
@@ -126,7 +112,7 @@ General usage::
Live with RTL-SDR
*******************
If you have an RTL-SDR receiver connected to your computer, you can use the ``rtlsdr`` source switch (require ``pyrtlsdr`` package), with command::
If you have an RTL-SDR receiver plugged to the computer, you can connect it with ``rtlsdr`` source switch, shown as follows::
$ modeslive --source rtlsdr
@@ -275,20 +261,8 @@ Mode-S Enhanced Surveillance (EHS)
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
Meteorological reports [Experimental]
**************************************
To identify BDS 4,4 and 4,5 codes, you must set ``mrar`` argument to ``True`` in the ``infer()`` function:
.. code:: python
pms.bds.infer(msg. mrar=True)
Once the correct MRAR and MHR messages are identified, decode them as follows:
Meteorological routine air report (MRAR)
+++++++++++++++++++++++++++++++++++++++++
Meteorological routine air report (MRAR) [Experimental]
********************************************************
.. code:: python
@@ -299,8 +273,8 @@ Meteorological routine air report (MRAR)
pms.commb.hum44(msg) # Humidity (%)
Meteorological hazard air report (MHR)
+++++++++++++++++++++++++++++++++++++++++
Meteorological hazard air report (MHR) [Experimental]
*******************************************************
.. code:: python

View File

@@ -5,14 +5,12 @@ try:
from . import c_common as common
from .c_common import *
except:
from . import py_common as common
from .py_common import *
from . import common
from .common import *
from .decoder import tell
from .decoder import adsb
from .decoder import commb
from .decoder import allcall
from .decoder import surv
from .decoder import bds
from .extra import aero
from .extra import tcpclient

View File

@@ -5,8 +5,7 @@ cdef unsigned char int_to_char(unsigned char i)
cpdef str hex2bin(str hexstr)
cpdef long bin2int(str binstr)
cpdef long hex2int(str hexstr)
cpdef str bin2hex(str binstr)
cpdef long hex2int(str binstr)
cpdef unsigned char df(str msg)
cpdef long crc(str msg, bint encode=*)
@@ -17,12 +16,8 @@ cpdef bint is_icao_assigned(str icao)
cpdef int typecode(str msg)
cpdef int cprNL(double lat)
cpdef str idcode(str msg)
cpdef str squawk(str binstr)
cpdef int altcode(str msg)
cpdef int altitude(str binstr)
cpdef str data(str msg)
cpdef bint allzeros(str msg)

View File

@@ -25,7 +25,7 @@ cdef unsigned char int_to_char(unsigned char i):
@cython.boundscheck(False)
@cython.overflowcheck(False)
cpdef str hex2bin(str hexstr):
"""Convert a hexadecimal string to binary string, with zero fillings."""
"""Convert a hexdecimal string to binary string, with zero fillings."""
# num_of_bits = len(hexstr) * 4
cdef hexbytes = bytes(hexstr.encode())
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
@@ -66,14 +66,9 @@ cpdef long hex2int(str hexstr):
cumul = 16*cumul + char_to_int(v_hexstr[i])
return cumul
@cython.boundscheck(False)
cpdef str bin2hex(str binstr):
return "{0:X}".format(int(binstr, 2))
@cython.boundscheck(False)
cpdef unsigned char df(str msg):
"""Decode Downlink Format value, bits 1 to 5."""
"""Decode Downlink Format vaule, bits 1 to 5."""
cdef str dfbin = hex2bin(msg[:2])
# return min(bin2int(dfbin[0:5]), 24)
cdef long df = bin2int(dfbin[0:5])
@@ -160,7 +155,17 @@ cpdef long floor(double x):
return <long> c_floor(x)
cpdef str icao(str msg):
"""Calculate the ICAO address from an Mode-S message."""
"""Calculate the ICAO address from an Mode-S message.
Applicable only with DF4, DF5, DF20, DF21 messages.
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
cdef unsigned char DF = df(msg)
cdef long c0, c1
@@ -207,7 +212,14 @@ cpdef bint is_icao_assigned(str icao):
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int typecode(str msg):
"""Type code of ADS-B message"""
"""Type code of ADS-B message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: type code number
"""
if df(msg) not in (17, 18):
return -1
# return None
@@ -228,7 +240,7 @@ cpdef int cprNL(double lat):
cdef int nz = 15
cdef double a = 1 - cos(pi / (2 * nz))
cdef double b = cos(pi / 180 * fabs(lat)) ** 2
cdef double b = cos(pi / 180.0 * fabs(lat)) ** 2
cdef double nl = 2 * pi / (acos(1 - a / b))
NL = floor(nl)
return NL
@@ -236,41 +248,45 @@ cpdef int cprNL(double lat):
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef str idcode(str msg):
"""Compute identity (squawk code)."""
"""Compute identity (squawk code).
Applicable only for DF5 or DF21 messages, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if df(msg) not in [5, 21]:
raise RuntimeError("Message must be Downlink Format 5 or 21.")
squawk_code = squawk(hex2bin(msg)[19:32])
return squawk_code
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef str squawk(str binstr):
"""Compute identity (squawk code)."""
if len(binstr) != 13 or set(binstr) != set('01'):
raise RuntimeError("Input must be 13 bits binary string")
cdef bytearray _mbin = bytearray(binstr.encode())
cdef bytearray _mbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] mbin = _mbin
cdef bytearray _idcode = bytearray(4)
cdef unsigned char[:] idcode = _idcode
cdef unsigned char C1 = mbin[0]
cdef unsigned char A1 = mbin[1]
cdef unsigned char C2 = mbin[2]
cdef unsigned char A2 = mbin[3]
cdef unsigned char C4 = mbin[4]
cdef unsigned char A4 = mbin[5]
# X = mbin[6]
cdef unsigned char B1 = mbin[7]
cdef unsigned char D1 = mbin[8]
cdef unsigned char B2 = mbin[9]
cdef unsigned char D2 = mbin[10]
cdef unsigned char B4 = mbin[11]
cdef unsigned char D4 = mbin[12]
cdef unsigned char C1 = mbin[19]
cdef unsigned char A1 = mbin[20]
cdef unsigned char C2 = mbin[21]
cdef unsigned char A2 = mbin[22]
cdef unsigned char C4 = mbin[23]
cdef unsigned char A4 = mbin[24]
# _ = mbin[25]
cdef unsigned char B1 = mbin[26]
cdef unsigned char D1 = mbin[27]
cdef unsigned char B2 = mbin[28]
cdef unsigned char D2 = mbin[29]
cdef unsigned char B4 = mbin[30]
cdef unsigned char 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)
idcode[0] = int_to_char((char_to_int(A4)*2 + char_to_int(A2))*2 + char_to_int(A1))
idcode[1] = int_to_char((char_to_int(B4)*2 + char_to_int(B2))*2 + char_to_int(B1))
@@ -279,68 +295,68 @@ cpdef str squawk(str binstr):
return _idcode.decode()
#return str(byte1) + str(byte2) + str(byte3) + str(byte4)
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int altcode(str msg):
"""Compute the altitude."""
"""Compute the altitude.
Applicable only for DF4 or DF20 message, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if df(msg) not in [0, 4, 16, 20]:
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
alt = altitude(hex2bin(msg)[19:32])
return alt
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int altitude(str binstr):
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
raise RuntimeError("Input must be 13 bits binary string")
cdef bytearray _mbin = bytearray(binstr.encode())
# Altitude code, bit 20-32
cdef bytearray _mbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] mbin = _mbin
cdef char Mbit = binstr[6]
cdef char Qbit = binstr[8]
cdef char mbit = mbin[25] # M bit: 26
cdef char qbit = mbin[27] # Q bit: 28
cdef int alt = 0
cdef bytearray vbin
cdef bytearray _graybytes = bytearray(11)
cdef unsigned char[:] graybytes = _graybytes
if bin2int(binstr) == 0:
# altitude unknown or invalid
alt = -9999
elif Mbit == 48: # unit in ft, "0" -> 48
if Qbit == 49: # 25ft interval, "1" -> 49
vbin = _mbin[:6] + _mbin[7:8] + _mbin[9:]
if mbit == 48: # unit in ft, "0" -> 48
if qbit == 49: # 25ft interval, "1" -> 49
vbin = _mbin[19:25] + _mbin[26:27] + _mbin[28:32]
alt = bin2int(vbin.decode()) * 25 - 1000
if Qbit == 48: # 100ft interval, above 50175ft, "0" -> 48
graybytes[8] = mbin[0]
graybytes[2] = mbin[1]
graybytes[9] = mbin[2]
graybytes[3] = mbin[3]
graybytes[10] = mbin[4]
graybytes[4] = mbin[5]
# M = mbin[6]
graybytes[5] = mbin[7]
# Q = mbin[8]
graybytes[6] = mbin[9]
graybytes[0] = mbin[10]
graybytes[7] = mbin[11]
graybytes[1] = mbin[12]
if qbit == 48: # 100ft interval, above 50175ft, "0" -> 48
graybytes[8] = mbin[19]
graybytes[2] = mbin[20]
graybytes[9] = mbin[21]
graybytes[3] = mbin[22]
graybytes[10] = mbin[23]
graybytes[4] = mbin[24]
# _ = mbin[25]
graybytes[5] = mbin[26]
# cdef char D1 = mbin[27] # always zero
graybytes[6] = mbin[28]
graybytes[0] = mbin[29]
graybytes[7] = mbin[30]
graybytes[1] = mbin[31]
# graybytes = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
alt = gray2alt(_graybytes.decode())
elif Mbit == 49: # unit in meter, "1" -> 49
vbin = _mbin[:6] + _mbin[7:]
if mbit == 49: # unit in meter, "1" -> 49
vbin = _mbin[19:25] + _mbin[26:31]
alt = int(bin2int(vbin.decode()) * 3.28084) # convert to ft
return alt
cpdef int gray2alt(str codestr):
cdef str gc500 = codestr[:8]
cdef int n500 = gray2int(gc500)
@@ -379,7 +395,15 @@ cpdef str data(str msg):
cpdef bint allzeros(str msg):
"""Check if the data bits are all zeros."""
"""Check if the data bits are all zeros.
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
d = hex2bin(data(msg))
if bin2int(d) > 0:

View File

@@ -1,46 +1,39 @@
from typing import Optional
import numpy as np
from textwrap import wrap
def hex2bin(hexstr: str) -> str:
"""Convert a hexadecimal string to binary string, with zero fillings."""
def hex2bin(hexstr):
"""Convert a hexdecimal string to binary string, with zero fillings."""
num_of_bits = len(hexstr) * 4
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
return binstr
def hex2int(hexstr: str) -> int:
"""Convert a hexadecimal string to integer."""
def hex2int(hexstr):
"""Convert a hexdecimal string to integer."""
return int(hexstr, 16)
def bin2int(binstr: str) -> int:
def bin2int(binstr):
"""Convert a binary string to integer."""
return int(binstr, 2)
def bin2hex(binstr: str) -> str:
"""Convert a binary string to hexadecimal string."""
return "{0:X}".format(int(binstr, 2))
def df(msg: str) -> int:
def df(msg):
"""Decode Downlink Format value, bits 1 to 5."""
dfbin = hex2bin(msg[:2])
return min(bin2int(dfbin[0:5]), 24)
def crc(msg: str, encode: bool = False) -> int:
def crc(msg, encode=False):
"""Mode-S Cyclic Redundancy Check.
Detect if bit error occurs in the Mode-S message. When encode option is on,
the checksum is generated.
Args:
msg: 28 bytes hexadecimal message string
encode: True to encode the date only and return the checksum
msg (string): 28 bytes hexadecimal message string
encode (bool): True to encode the date only and return the checksum
Returns:
int: message checksum, or partity bits (encoder)
@@ -77,7 +70,7 @@ def crc(msg: str, encode: bool = False) -> int:
return result
def crc_legacy(msg: str, encode: bool = False) -> int:
def crc_legacy(msg, encode=False):
"""Mode-S Cyclic Redundancy Check. (Legacy code, 2x slow)."""
# the polynominal generattor code for CRC [1111111111111010000001001]
generator = np.array(
@@ -105,7 +98,7 @@ def crc_legacy(msg: str, encode: bool = False) -> int:
return reminder
def floor(x: float) -> int:
def floor(x):
"""Mode-S floor function.
Defined as the greatest integer value k, such that k <= x
@@ -115,7 +108,7 @@ def floor(x: float) -> int:
return int(np.floor(x))
def icao(msg: str) -> Optional[str]:
def icao(msg):
"""Calculate the ICAO address from an Mode-S message.
Applicable only with DF4, DF5, DF20, DF21 messages.
@@ -127,7 +120,6 @@ def icao(msg: str) -> Optional[str]:
String: ICAO address in 6 bytes hexadecimal string
"""
addr: Optional[str]
DF = df(msg)
if DF in (11, 17, 18):
@@ -142,7 +134,7 @@ def icao(msg: str) -> Optional[str]:
return addr
def is_icao_assigned(icao: str) -> bool:
def is_icao_assigned(icao):
"""Check whether the ICAO address is assigned (Annex 10, Vol 3)."""
if (icao is None) or (not isinstance(icao, str)) or (len(icao) != 6):
return False
@@ -171,7 +163,7 @@ def is_icao_assigned(icao: str) -> bool:
return True
def typecode(msg: str) -> Optional[int]:
def typecode(msg):
"""Type code of ADS-B message
Args:
@@ -187,7 +179,7 @@ def typecode(msg: str) -> Optional[int]:
return bin2int(tcbin[0:5])
def cprNL(lat: float) -> int:
def cprNL(lat):
"""NL() function in CPR decoding."""
if np.isclose(lat, 0):
@@ -199,14 +191,17 @@ def cprNL(lat: float) -> int:
nz = 15
a = 1 - np.cos(np.pi / (2 * nz))
b = np.cos(np.pi / 180 * abs(lat)) ** 2
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
nl = 2 * np.pi / (np.arccos(1 - a / b))
NL = floor(nl)
return NL
def idcode(msg: str) -> str:
"""Compute identity code (squawk) encoded in DF5 or DF21 message.
def idcode(msg):
"""Compute identity (squawk code).
Applicable only for DF5 or DF21 messages, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
@@ -219,37 +214,20 @@ def idcode(msg: str) -> str:
raise RuntimeError("Message must be Downlink Format 5 or 21.")
mbin = hex2bin(msg)
idcodebin = mbin[19:32]
return squawk(idcodebin)
def squawk(binstr: str) -> str:
"""Decode 13 bits identity (squawk) code.
Args:
binstr (String): 13 bits binary string
Returns:
int: altitude in ft
"""
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
raise RuntimeError("Input must be 13 bits binary string")
C1 = binstr[0]
A1 = binstr[1]
C2 = binstr[2]
A2 = binstr[3]
C4 = binstr[4]
A4 = binstr[5]
# X = binstr[6]
B1 = binstr[7]
D1 = binstr[8]
B2 = binstr[9]
D2 = binstr[10]
B4 = binstr[11]
D4 = binstr[12]
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)
@@ -259,8 +237,11 @@ def squawk(binstr: str) -> str:
return str(byte1) + str(byte2) + str(byte3) + str(byte4)
def altcode(msg: str) -> Optional[int]:
"""Compute altitude encoded in DF4 or DF20 message.
def altcode(msg):
"""Compute the altitude.
Applicable only for DF4 or DF20 message, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
@@ -269,78 +250,50 @@ def altcode(msg: str) -> Optional[int]:
int: altitude in ft
"""
alt: Optional[int]
if df(msg) not in [0, 4, 16, 20]:
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
# Altitude code, bit 20-32
mbin = hex2bin(msg)
altitude_code = mbin[19:32]
mbit = mbin[25] # M bit: 26
qbit = mbin[27] # Q bit: 28
alt = altitude(altitude_code)
return alt
def altitude(binstr: str) -> Optional[int]:
"""Decode 13 bits altitude code.
Args:
binstr (String): 13 bits binary string
Returns:
int: altitude in ft
"""
alt: Optional[int]
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
raise RuntimeError("Input must be 13 bits binary string")
Mbit = binstr[6]
Qbit = binstr[8]
if bin2int(binstr) == 0:
# altitude unknown or invalid
alt = None
elif Mbit == "0": # unit in ft
if Qbit == "1": # 25ft interval
vbin = binstr[:6] + binstr[7] + binstr[9:]
if mbit == "0": # unit in ft
if qbit == "1": # 25ft interval
vbin = mbin[19:25] + mbin[26] + mbin[28:32]
alt = bin2int(vbin) * 25 - 1000
if Qbit == "0": # 100ft interval, above 50187.5ft
C1 = binstr[0]
A1 = binstr[1]
C2 = binstr[2]
A2 = binstr[3]
C4 = binstr[4]
A4 = binstr[5]
# M = binstr[6]
B1 = binstr[7]
# Q = binstr[8]
B2 = binstr[9]
D2 = binstr[10]
B4 = binstr[11]
D4 = binstr[12]
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 = binstr[:6] + binstr[7:]
if mbit == "1": # unit in meter
vbin = mbin[19:25] + mbin[26:31]
alt = int(bin2int(vbin) * 3.28084) # convert to ft
return alt
def gray2alt(binstr: str) -> Optional[int]:
gc500 = binstr[:8]
def gray2alt(codestr):
gc500 = codestr[:8]
n500 = gray2int(gc500)
# in 100-ft step must be converted first
gc100 = binstr[8:]
gc100 = codestr[8:]
n100 = gray2int(gc100)
if n100 in [0, 5, 6]:
@@ -356,9 +309,9 @@ def gray2alt(binstr: str) -> Optional[int]:
return alt
def gray2int(binstr: str) -> int:
def gray2int(graystr):
"""Convert greycode to binary."""
num = bin2int(binstr)
num = bin2int(graystr)
num ^= num >> 8
num ^= num >> 4
num ^= num >> 2
@@ -366,12 +319,12 @@ def gray2int(binstr: str) -> int:
return num
def data(msg: str) -> str:
def data(msg):
"""Return the data frame in the message, bytes 9 to 22."""
return msg[8:-6]
def allzeros(msg: str) -> bool:
def allzeros(msg):
"""Check if the data bits are all zeros.
Args:
@@ -389,7 +342,7 @@ def allzeros(msg: str) -> bool:
return True
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool:
def wrongstatus(data, sb, msb, lsb):
"""Check if the status bit and field bits are consistency.
This Function is used for checking BDS code versions.
@@ -404,85 +357,3 @@ def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool:
return True
return False
def fs(msg):
"""Decode flight status for DF 4, 5, 20, and 21.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: flight status, description
"""
msgbin = hex2bin(msg)
fs = bin2int(msgbin[5:8])
text = None
if fs == 0:
text = "no alert, no SPI, aircraft is airborne"
elif fs == 1:
text = "no alert, no SPI, aircraft is on-ground"
elif fs == 2:
text = "alert, no SPI, aircraft is airborne"
elif fs == 3:
text = "alert, no SPI, aircraft is on-ground"
elif fs == 4:
text = "alert, SPI, aircraft is airborne or on-ground"
elif fs == 5:
text = "no alert, SPI, aircraft is airborne or on-ground"
return fs, text
def dr(msg):
"""Decode downlink request for DF 4, 5, 20, and 21.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: downlink request, description
"""
msgbin = hex2bin(msg)
dr = bin2int(msgbin[8:13])
text = None
if dr == 0:
text = "no downlink request"
elif dr == 1:
text = "request to send Comm-B message"
elif dr == 4:
text = "Comm-B broadcast 1 available"
elif dr == 5:
text = "Comm-B broadcast 2 available"
elif dr >= 16:
text = "ELM downlink segments available: {}".format(dr - 15)
return dr, text
def um(msg):
"""Decode utility message for DF 4, 5, 20, and 21.
Utility message contains interrogator identifier and reservation type.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: interrogator identifier code that triggered the reply, and
reservation type made by the interrogator
"""
msgbin = hex2bin(msg)
iis = bin2int(msgbin[13:17])
ids = bin2int(msgbin[17:19])
if ids == 0:
ids_text = None
if ids == 1:
ids_text = "Comm-B interrogator identifier code"
if ids == 2:
ids_text = "Comm-C interrogator identifier code"
if ids == 3:
ids_text = "Comm-D interrogator identifier code"
return iis, ids, ids_text

View File

@@ -1,4 +1,4 @@
def tell(msg: str) -> None:
def tell(msg):
from pyModeS import common, adsb, commb, bds
def _print(label, value, unit=None):
@@ -22,7 +22,7 @@ def tell(msg: str) -> None:
tc = common.typecode(msg)
if 1 <= tc <= 4: # callsign
callsign = adsb.callsign(msg)
_print("Type", "Identification and category")
_print("Type", "Identitification and category")
_print("Callsign:", callsign)
if 5 <= tc <= 8: # surface position
@@ -71,68 +71,6 @@ def tell(msg: str) -> None:
_print("CPR Longitude", cprlon)
_print("Altitude", alt, "feet")
if tc == 29: # target state and status
_print("Type", "Target State and Status")
subtype = common.bin2int((common.hex2bin(msg)[32:])[5:7])
_print("Subtype", subtype)
tcas_operational = adsb.tcas_operational(msg)
types = {0: "Not Engaged", 1: "Engaged"}
tcas_operational_types = {0: "Not Operational", 1: "Operational"}
if subtype == 0:
emergency_types = {
0: "No emergency",
1: "General emergency",
2: "Lifeguard/medical emergency",
3: "Minimum fuel",
4: "No communications",
5: "Unlawful interference",
6: "Downed aircraft",
7: "Reserved"
}
vertical_horizontal_types = {
1: "Acquiring mode",
2: "Capturing/Maintaining mode"
}
tcas_ra_types = {0: "Not active", 1: "Active"}
alt, alt_source, alt_ref = adsb.target_altitude(msg)
angle, angle_type, angle_source = adsb.target_angle(msg)
vertical_mode = adsb.vertical_mode(msg)
horizontal_mode = adsb.horizontal_mode(msg)
tcas_ra = adsb.tcas_ra(msg)
emergency_status = adsb.emergency_status(msg)
_print("Target altitude", alt, "feet")
_print("Altitude source", alt_source)
_print("Altitude reference", alt_ref)
_print("Angle", angle, "°")
_print("Angle Type", angle_type)
_print("Angle Source", angle_source)
_print("Vertical mode", vertical_horizontal_types[vertical_mode])
_print("Horizontal mode", vertical_horizontal_types[horizontal_mode])
_print("TCAS/ACAS", tcas_operational_types[tcas_operational])
_print("TCAS/ACAS RA", tcas_ra_types[tcas_ra])
_print("Emergency status", emergency_types[emergency_status])
else:
alt, alt_source = adsb.selected_altitude(msg)
baro = adsb.baro_pressure_setting(msg)
hdg = adsb.selected_heading(msg)
autopilot = adsb.autopilot(msg)
vnav = adsb.vnav_mode(msg)
alt_hold = adsb.altitude_hold_mode(msg)
app = adsb.approach_mode(msg)
lnav = adsb.lnav_mode(msg)
_print("Selected altitude", alt, "feet")
_print("Altitude source", alt_source)
_print("Barometric pressure setting", baro, "millibars")
_print("Selected Heading", hdg, "°")
if not(common.bin2int((common.hex2bin(msg)[32:])[46]) == 0):
_print("Autopilot", types[autopilot])
_print("VNAV mode", types[vnav])
_print("Altitude hold mode", types[alt_hold])
_print("Approach mode", types[app])
_print("TCAS/ACAS", tcas_operational_types[tcas_operational])
_print("LNAV mode", types[lnav])
if df == 20:
_print("Protocol", "Mode-S Comm-B altitude reply")
_print("Altitude", common.altcode(msg), "feet")

View File

@@ -28,24 +28,6 @@ from pyModeS.decoder.bds.bds06 import (
)
from pyModeS.decoder.bds.bds08 import category, callsign
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
from pyModeS.decoder.bds.bds61 import is_emergency, emergency_state, emergency_squawk
from pyModeS.decoder.bds.bds62 import (
selected_altitude,
selected_heading,
target_altitude,
target_angle,
tcas_operational,
tcas_ra,
baro_pressure_setting,
vertical_mode,
horizontal_mode,
vnav_mode,
lnav_mode,
autopilot,
altitude_hold_mode,
approach_mode,
emergency_status
)
def df(msg):
@@ -109,7 +91,7 @@ def position_with_ref(msg, lat_ref, lon_ref):
A reference position is required, which can be previously
calculated location, ground station, or airport location.
The function works with both airborne and surface position messages.
The reference position shall be within 180NM (airborne) or 45NM (surface)
The reference position shall be with in 180NM (airborne) or 45NM (surface)
of the true position.
Args:
@@ -163,15 +145,15 @@ def velocity(msg, source=False):
Args:
msg (str): 28 hexdigits string
source (boolean): Include direction and vertical rate sources in return. Default to False.
If set to True, the function will return six values instead of four.
If set to True, the function will return six value instead of four.
Returns:
(int, float, int, string, [string], [string]): Four or six parameters, including:
int, float, int, string, [string], [string]: Four or six parameters, including:
- Speed (kt)
- Angle (degree), either ground track or heading
- Vertical rate (ft/min)
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
- [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
- [Optional] Vertical rate source ('BARO' or 'GNSS')
For surface messages, vertical rate and its respective sources are set to None.
@@ -341,7 +323,7 @@ def nic_v2(msg, NICa, NICbc):
Args:
msg (str): 28 hexdigits string
NICa (int or string): NIC supplement - A
NICbc (int or string): NIC supplement - B or C
NICbc (int or srting): NIC supplement - B or C
Returns:
int or string: Horizontal Radius of Containment

View File

@@ -1,82 +1,5 @@
"""
Decode all-call reply messages, with downlink format 11
Decoding all call replies DF=11
[To be implemented]
"""
from pyModeS import common
def _checkdf(func):
"""Ensure downlink format is 11."""
def wrapper(msg):
df = common.df(msg)
if df != 11:
raise RuntimeError(
"Incorrect downlink format, expect 11, got {}".format(df)
)
return func(msg)
return wrapper
@_checkdf
def icao(msg):
"""Decode transponder code (ICAO address).
Args:
msg (str): 14 hexdigits string
Returns:
string: ICAO address
"""
return common.icao(msg)
@_checkdf
def interrogator(msg):
"""Decode interrogator identifier code.
Args:
msg (str): 14 hexdigits string
Returns:
int: interrogator identifier code
"""
# the CRC remainder contains the CL and IC field. top three bits are CL field and last four bits are IC field.
remainder = common.crc(msg)
if remainder > 79:
IC = "corrupt IC"
elif remainder < 16:
IC="II"+str(remainder)
else:
IC="SI"+str(remainder-16)
return IC
@_checkdf
def capability(msg):
"""Decode transponder capability.
Args:
msg (str): 14 hexdigits string
Returns:
int, str: transponder capability, description
"""
msgbin = common.hex2bin(msg)
ca = common.bin2int(msgbin[5:8])
if ca == 0:
text = "level 1 transponder"
elif ca == 4:
text = "level 2 transponder, ability to set CA to 7, on ground"
elif ca == 5:
text = "level 2 transponder, ability to set CA to 7, airborne"
elif ca == 6:
text = "evel 2 transponder, ability to set CA to 7, either airborne or ground"
elif ca == 7:
text = "Downlink Request value is 0,or the Flight Status is 2, 3, 4 or 5, either airborne or on the ground"
else:
text = None
return ca, text

View File

@@ -38,7 +38,6 @@ from pyModeS.decoder.bds import (
bds50,
bds53,
bds60,
bds62
)
@@ -61,34 +60,25 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
vy = v * np.cos(np.radians(angle))
return vx, vy
# message must be both BDS 50 and 60 before processing
if not (bds50.is50(msg) and bds60.is60(msg)):
return None
# --- assuming BDS60 ---
h50 = bds50.trk50(msg)
v50 = bds50.gs50(msg)
if h50 is None or v50 is None:
return "BDS50,BDS60"
h60 = bds60.hdg60(msg)
m60 = bds60.mach60(msg)
i60 = bds60.ias60(msg)
# additional check now knowing the altitude
if (m60 is not None) and (i60 is not None):
ias_ = aero.mach2cas(m60, alt_ref * aero.ft) / aero.kts
if abs(i60 - ias_) > 20:
return "BDS50"
if h60 is None or (m60 is None and i60 is None):
return "BDS50,BDS60"
m60 = np.nan if m60 is None else m60
i60 = np.nan if i60 is None else i60
# --- assuming BDS50 ---
h50 = bds50.trk50(msg)
v50 = bds50.gs50(msg)
if h50 is None or v50 is None:
return "BDS50,BDS60"
XY5 = vxy(v50 * aero.kts, h50)
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)

View File

@@ -34,13 +34,13 @@ def airborne_position(msg0, msg1, t0, t1):
raise RuntimeError("Both even and odd CPR frames are required.")
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(mb0[22:39]) / 131072
cprlon_even = common.bin2int(mb0[39:56]) / 131072
cprlat_odd = common.bin2int(mb1[22:39]) / 131072
cprlon_odd = common.bin2int(mb1[39:56]) / 131072
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
air_d_lat_even = 360 / 60
air_d_lat_odd = 360 / 59
air_d_lat_even = 360.0 / 60
air_d_lat_odd = 360.0 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
@@ -64,13 +64,13 @@ def airborne_position(msg0, msg1, t0, t1):
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360 / ni) * (m % ni + cprlon_even)
lon = (360.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 1, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360 / ni) * (m % ni + cprlon_odd)
lon = (360.0 / ni) * (m % ni + cprlon_odd)
if lon > 180:
lon = lon - 360
@@ -82,7 +82,7 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
"""Decode airborne position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be within 180NM of the true position.
be with in 180NM of the true position.
Args:
msg (str): even message (28 hexdigits)
@@ -95,11 +95,11 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072
cprlon = common.bin2int(mb[39:56]) / 131072
cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0
i = int(mb[21])
d_lat = 360 / 59 if i else 360 / 60
d_lat = 360.0 / 59 if i else 360.0 / 60
j = common.floor(lat_ref / d_lat) + common.floor(
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
@@ -110,9 +110,9 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
ni = common.cprNL(lat) - i
if ni > 0:
d_lon = 360 / ni
d_lon = 360.0 / ni
else:
d_lon = 360
d_lon = 360.0
m = common.floor(lon_ref / d_lon) + common.floor(
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
@@ -139,12 +139,17 @@ def altitude(msg):
raise RuntimeError("%s: Not a airborn position message" % msg)
mb = common.hex2bin(msg)[32:]
altbin = mb[8:20]
if tc < 19:
altcode = altbin[0:6] + "0" + altbin[6:]
alt = common.altitude(altcode)
# barometric altitude
q = mb[15]
if q:
n = common.bin2int(mb[8:15] + mb[16:20])
alt = n * 25 - 1000
else:
alt = None
else:
alt = common.bin2int(altbin) * 3.28084
# GNSS altitude, meters -> feet
alt = common.bin2int(mb[8:20]) * 3.28084
return alt

View File

@@ -1,7 +1,7 @@
# ------------------------------------------
# BDS 0,6
# ADS-B TC=5-8
# Surface movement
# Surface movment
# ------------------------------------------
from pyModeS import common
@@ -27,13 +27,13 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
msgbin1 = common.hex2bin(msg1)
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
air_d_lat_even = 90 / 60
air_d_lat_odd = 90 / 59
air_d_lat_even = 90.0 / 60
air_d_lat_odd = 90.0 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
@@ -43,8 +43,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
# solution for north hemisphere
lat_even_s = lat_even_n - 90
lat_odd_s = lat_odd_n - 90
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
@@ -60,16 +60,16 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
nl = common.cprNL(lat_even)
ni = max(common.cprNL(lat_even) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90 / ni) * (m % ni + cprlon_even)
lon = (90.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat_odd)
ni = max(common.cprNL(lat_odd) - 1, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90 / ni) * (m % ni + cprlon_odd)
lon = (90.0 / ni) * (m % ni + cprlon_odd)
# four possible longitude solutions
lons = [lon, lon + 90, lon + 180, lon + 270]
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
# make sure lons are between -180 and 180
lons = [(l + 180) % 360 - 180 for l in lons]
@@ -86,7 +86,7 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
"""Decode surface position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be within 45NM of the true position.
be with in 45NM of the true position.
Args:
msg (str): even message (28 hexdigits)
@@ -99,11 +99,11 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072
cprlon = common.bin2int(mb[39:56]) / 131072
cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0
i = int(mb[21])
d_lat = 90 / 59 if i else 90 / 60
d_lat = 90.0 / 59 if i else 90.0 / 60
j = common.floor(lat_ref / d_lat) + common.floor(
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
@@ -114,9 +114,9 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
ni = common.cprNL(lat) - i
if ni > 0:
d_lon = 90 / ni
d_lon = 90.0 / ni
else:
d_lon = 90
d_lon = 90.0
m = common.floor(lon_ref / d_lon) + common.floor(
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
@@ -133,7 +133,7 @@ def surface_velocity(msg, source=False):
Args:
msg (str): 28 hexdigits string
source (boolean): Include direction and vertical rate sources in return. Default to False.
If set to True, the function will return six values instead of four.
If set to True, the function will return six value instead of four.
Returns:
int, float, int, string, [string], [string]: Four or six parameters, including:
@@ -153,7 +153,7 @@ def surface_velocity(msg, source=False):
# ground track
trk_status = int(mb[12])
if trk_status == 1:
trk = common.bin2int(mb[13:20]) * 360 / 128
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
trk = round(trk, 1)
else:
trk = None
@@ -168,11 +168,13 @@ def surface_velocity(msg, source=False):
elif mov == 124:
spd = 175
else:
mov_lb = [2, 9, 13, 39, 94, 109, 124]
kts_lb = [0.125, 1, 2, 15, 70, 100, 175]
step = [0.125, 0.25, 0.5, 1, 2, 5]
i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov)
spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1]
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)
if source:
return spd, trk, 0, "GS", "TRUE_NORTH", None
else:

View File

@@ -1,7 +1,7 @@
# ------------------------------------------
# BDS 0,8
# ADS-B TC=1-4
# Aircraft identification and category
# Aircraft identitification and category
# ------------------------------------------
from pyModeS import common

View File

@@ -16,7 +16,7 @@ def airborne_velocity(msg, source=False):
Args:
msg (str): 28 hexdigits string
source (boolean): Include direction and vertical rate sources in return. Default to False.
If set to True, the function will return six values instead of four.
If set to True, the function will return six value instead of four.
Returns:
int, float, int, string, [string], [string]: Four or six parameters, including:
@@ -24,7 +24,7 @@ def airborne_velocity(msg, source=False):
- Angle (degree), either ground track or heading
- Vertical rate (ft/min)
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
- [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
- [Optional] Vertical rate source ('BARO' or 'GNSS')
"""
@@ -35,54 +35,45 @@ def airborne_velocity(msg, source=False):
subtype = common.bin2int(mb[5:8])
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
return None
if subtype in (1, 2):
v_ew_sign = -1 if mb[13] == "1" else 1
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
if subtype == 2: # Supersonic
v_ew *= 4
v_ew = common.bin2int(mb[14:24])
v_ns = common.bin2int(mb[25:35])
v_ns_sign = -1 if mb[24] == "1" else 1
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
if subtype == 2: # Supersonic
v_ns *= 4
if v_ew == 0 or v_ns == 0:
spd = None
trk_or_hdg = None
vs = None
else:
v_ew_sign = -1 if mb[13] == "1" else 1
v_ew = v_ew - 1 # east-west velocity
if subtype == 2: # Supersonic
v_ew *= 4
v_we = v_ew_sign * v_ew
v_sn = v_ns_sign * v_ns
v_ns_sign = -1 if mb[24] == "1" else 1
v_ns = v_ns - 1 # north-south velocity
if subtype == 2: # Supersonic
v_ns *= 4
spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
spd = int(spd)
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
spd = int(spd)
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
trk_or_hdg = round(trk, 2)
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
spd_type = "GS"
trk_or_hdg = round(trk, 2)
dir_type = "TRUE_NORTH"
else:
if mb[13] == "0":
hdg = None
else:
hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
hdg = round(hdg, 2)
trk_or_hdg = hdg
spd = common.bin2int(mb[25:35])
spd = None if spd == 0 else spd - 1
if subtype == 4: # Supersonic
spd *= 4
@@ -91,7 +82,7 @@ def airborne_velocity(msg, source=False):
else:
spd_type = "TAS"
dir_type = "MAGNETIC_NORTH"
dir_type = "MAGENTIC_NORTH"
vr_source = "GNSS" if mb[35] == "0" else "BARO"
vr_sign = -1 if mb[36] == "1" else 1
@@ -105,7 +96,7 @@ def airborne_velocity(msg, source=False):
def altitude_diff(msg):
"""Decode the difference between GNSS and barometric altitude.
"""Decode the differece between GNSS and barometric altitude.
Args:
msg (str): 28 hexdigits string, TC=19

View File

@@ -21,7 +21,7 @@ def is17(msg):
d = common.hex2bin(common.data(msg))
if common.bin2int(d[24:56]) != 0:
if common.bin2int(d[28:56]) != 0:
return False
caps = cap17(msg)
@@ -45,7 +45,7 @@ def cap17(msg):
msg (str): 28 hexdigits string
Returns:
list: list of supported BDS codes
list: list of support BDS codes
"""
allbds = [
"05",
@@ -72,10 +72,14 @@ def cap17(msg):
"56",
"5F",
"60",
"NA",
"NA",
"E1",
"E2",
]
d = common.hex2bin(common.data(msg))
idx = [i for i, v in enumerate(d[:24]) if v == "1"]
capacity = ["BDS" + allbds[i] for i in idx]
idx = [i for i, v in enumerate(d[:28]) if v == "1"]
capacity = ["BDS" + allbds[i] for i in idx if allbds[i] is not "NA"]
return capacity

View File

@@ -24,11 +24,9 @@ def is20(msg):
if d[0:8] != "00100000":
return False
# allow empty callsign
if common.bin2int(d[8:56]) == 0
return True
cs = cs20(msg)
if "#" in cs20(msg):
if "#" in cs:
return False
return True

View File

@@ -117,4 +117,4 @@ def alt40fms(msg):
"alt40fms() has been renamed to selalt40fms(). It will be removed in the future.",
DeprecationWarning,
)
return selalt40fms(msg)
return selalt40mcp(msg)

View File

@@ -68,7 +68,7 @@ def wind44(msg):
return None, None
speed = common.bin2int(d[5:14]) # knots
direction = common.bin2int(d[14:23]) * 180 / 256 # degree
direction = common.bin2int(d[14:23]) * 180.0 / 256.0 # degree
return round(speed, 0), round(direction, 1)
@@ -136,13 +136,13 @@ def hum44(msg):
if d[49] == "0":
return None
hm = common.bin2int(d[50:56]) * 100 / 64 # %
hm = common.bin2int(d[50:56]) * 100.0 / 64 # %
return round(hm, 1)
def turb44(msg):
"""Turbulence.
"""Turblence.
Args:
msg (str): 28 hexdigits string

View File

@@ -40,7 +40,7 @@ def is50(msg):
return False
roll = roll50(msg)
if (roll is not None) and abs(roll) > 50:
if (roll is not None) and abs(roll) > 60:
return False
gs = gs50(msg)
@@ -78,7 +78,7 @@ def roll50(msg):
if sign:
value = value - 512
angle = value * 45 / 256 # degree
angle = value * 45.0 / 256.0 # degree
return round(angle, 1)
@@ -102,7 +102,7 @@ def trk50(msg):
if sign:
value = value - 1024
trk = value * 90 / 512.0
trk = value * 90.0 / 512.0
# convert from [-180, 180] to [0, 360]
if trk < 0:
@@ -151,7 +151,7 @@ def rtrk50(msg):
if sign:
value = value - 512
angle = value * 8 / 256 # degree / sec
angle = value * 8.0 / 256.0 # degree / sec
return round(angle, 3)

View File

@@ -78,7 +78,7 @@ def hdg53(msg):
if sign:
value = value - 1024
hdg = value * 90 / 512 # degree
hdg = value * 90.0 / 512.0 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:

View File

@@ -4,7 +4,6 @@
# ------------------------------------------
from pyModeS import common
from pyModeS.extra import aero
def is60(msg):
@@ -55,14 +54,6 @@ def is60(msg):
if vr_ins is not None and abs(vr_ins) > 6000:
return False
# additional check knowing altitude
if (mach is not None) and (ias is not None) and (common.df(msg) == 20):
alt = common.altcode(msg)
if alt is not None:
ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts
if abs(ias - ias_) > 20:
return False
return True
@@ -86,7 +77,7 @@ def hdg60(msg):
if sign:
value = value - 1024
hdg = value * 90 / 512 # degree
hdg = value * 90 / 512.0 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
@@ -158,7 +149,7 @@ def vr60baro(msg):
def vr60ins(msg):
"""Vertical rate measured by onboard equipment (IRS, AHRS)
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
Args:
msg (str): 28 hexdigits string

View File

@@ -1,83 +0,0 @@
# ------------------------------------------
# BDS 6,1
# ADS-B TC=28
# Aircraft Airborne status
# ------------------------------------------
from pyModeS import common
def is_emergency(msg: str) -> bool:
"""Check if the aircraft is reporting an emergency.
Non-emergencies are either a subtype of zero (no information) or
subtype of one and a value of zero (no emergency).
Subtype = 2 indicates an ACAS RA broadcast, look in BDS 3,0
:param msg: 28 bytes hexadecimal message string
:return: if the aircraft has declared an emergency
"""
if common.typecode(msg) != 28:
raise RuntimeError("%s: Not an airborne status message, expecting TC=28" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:8])
if subtype == 2:
raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented")
emergency_state = common.bin2int(mb[8:11])
if subtype == 1 and emergency_state == 1:
return True
else:
return False
def emergency_state(msg: str) -> int:
"""Decode aircraft emergency state.
Value Meaning
----- -----------------------
0 No emergency
1 General emergency
2 Lifeguard/Medical
3 Minimum fuel
4 No communications
5 Unlawful communications
6-7 Reserved
:param msg: 28 bytes hexadecimal message string
:return: emergency state
"""
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:8])
if subtype == 2:
raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented")
emergency_state = common.bin2int(mb[8:11])
return emergency_state
def emergency_squawk(msg: str) -> str:
"""Decode squawk code.
Emergency value 1: squawk 7700.
Emergency value 4: squawk 7600.
Emergency value 5: squawk 7500.
:param msg: 28 bytes hexadecimal message string
:return: aircraft squawk code
"""
if common.typecode(msg) != 28:
raise RuntimeError("%s: Not an airborne status message, expecting TC=28" % msg)
msgbin = common.hex2bin(msg)
# construct the 13 bits Mode A ID code
idcode = msgbin[43:56]
squawk = common.squawk(idcode)
return squawk

View File

@@ -1,474 +0,0 @@
# ------------------------------------------
# BDS 6,2
# ADS-B TC=29
# Target State and Status
# ------------------------------------------
from pyModeS import common
def selected_altitude(msg):
"""Decode selected altitude.
Args:
msg (str): 28 hexdigits string
Returns:
int: Selected altitude (ft)
string: Source ('MCP/FCU' or 'FMS')
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain selected altitude, use target altitude instead" % msg)
alt = common.bin2int(mb[9:20])
alt = None if alt == 0 else (alt - 1) * 32
alt_source = "MCP/FCU" if int(mb[8]) == 0 else "FMS"
return alt, alt_source
def target_altitude(msg):
"""Decode target altitude.
Args:
msg (str): 28 hexdigits string
Returns:
int: Target altitude (ft)
string: Source ('MCP/FCU', 'Holding mode' or 'FMS/RNAV')
string: Altitude reference, either pressure altitude or barometric corrected altitude ('FL' or 'MSL')
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain target altitude, use selected altitude instead" % msg)
alt_avail = common.bin2int(mb[7:9])
if alt_avail == 0:
return None
elif alt_avail == 1:
alt_source = "MCP/FCU"
elif alt_avail == 2:
alt_source = "Holding mode"
else:
alt_source = "FMS/RNAV"
alt_ref = "FL" if int(mb[9]) == 0 else "MSL"
alt = -1000 + common.bin2int(mb[15:25]) * 100
return alt, alt_source, alt_ref
def vertical_mode(msg):
"""Decode vertical mode.
Value Meaning
----- -----------------------
1 "Acquiring" mode
2 "Capturing" or "Maintaining" mode
3 Reserved
Args:
msg (str): 28 hexdigits string
Returns:
int: Vertical mode
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain vertical mode, use vnav mode instead" % msg)
vertical_mode = common.bin2int(mb[13:15])
if vertical_mode == 0:
return None
return vertical_mode
def horizontal_mode(msg):
"""Decode horizontal mode.
Value Meaning
----- -----------------------
1 "Acquiring" mode
2 "Capturing" or "Maintaining" mode
3 Reserved
Args:
msg (str): 28 hexdigits string
Returns:
int: Horizontal mode
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain horizontal mode, use lnav mode instead" % msg)
horizontal_mode = common.bin2int(mb[25:27])
if horizontal_mode == 0:
return None
return horizontal_mode
def selected_heading(msg):
"""Decode selected heading.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
float: Selected heading (degree)
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain selected heading, use target angle instead" % msg)
if int(mb[29]) == 0:
hdg = None
else:
hdg_sign = int(mb[30])
hdg = (hdg_sign+1) * common.bin2int(mb[31:39]) * (180/256)
hdg = round(hdg, 2)
return hdg
def target_angle(msg):
"""Decode target heading/track angle.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
int: Target angle (degree)
string: Angle type ('Heading' or 'Track')
string: Source ('MCP/FCU', 'Autopilot Mode' or 'FMS/RNAV')
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain target angle, use selected heading instead" % msg)
angle_avail = common.bin2int(mb[25:27])
if angle_avail == 0:
angle = None
else:
angle = common.bin2int(mb[27:36])
if angle_avail == 1:
angle_source = "MCP/FCU"
elif angle_avail == 2:
angle_source = "Autopilot mode"
else:
angle_source = "FMS/RNAV"
angle_type = "Heading" if int(mb[36]) else "Track"
return angle, angle_type, angle_source
def baro_pressure_setting(msg):
"""Decode barometric pressure setting.
Args:
msg (str): 28 hexdigits string
Returns:
float: Barometric pressure setting (millibars)
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain barometric pressure setting" % msg)
baro = common.bin2int(mb[20:29])
baro = None if baro == 0 else 800 + (baro - 1) * 0.8
baro = round(baro, 1)
return baro
def autopilot(msg) -> bool:
"""Decode autopilot engagement.
Args:
msg (str): 28 hexdigits string
Returns:
bool: Autopilot engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain autopilot engagement" % msg)
if int(mb[46]) == 0:
return None
autopilot = True if int(mb[47]) == 1 else False
return autopilot
def vnav_mode(msg) -> bool:
"""Decode VNAV mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: VNAV mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain vnav mode, use vertical mode instead" % msg)
if int(mb[46]) == 0:
return None
vnav_mode = True if int(mb[48]) == 1 else False
return vnav_mode
def altitude_hold_mode(msg) -> bool:
"""Decode altitude hold mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: Altitude hold mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain altitude hold mode" % msg)
if int(mb[46]) == 0:
return None
alt_hold_mode = True if int(mb[49]) == 1 else False
return alt_hold_mode
def approach_mode(msg) -> bool:
"""Decode approach mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: Approach mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain approach mode" % msg)
if int(mb[46]) == 0:
return None
app_mode = True if int(mb[51]) == 1 else False
return app_mode
def lnav_mode(msg) -> bool:
"""Decode LNAV mode.
Args:
msg (str): 28 hexdigits string
Returns:
bool: LNAV mode engaged
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain lnav mode, use horizontal mode instead" % msg)
if int(mb[46]) == 0:
return None
lnav_mode = True if int(mb[53]) == 1 else False
return lnav_mode
def tcas_operational(msg) -> bool:
"""Decode TCAS/ACAS operational.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
bool: TCAS/ACAS operational
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 0:
tcas = True if int(mb[51]) == 0 else False
else:
tcas = True if int(mb[52]) == 1 else False
return tcas
def tcas_ra(msg) -> bool:
"""Decode TCAS/ACAS Resolution advisory.
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
bool: TCAS/ACAS Resolution advisory active
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain TCAS/ACAS RA" % msg)
tcas_ra = True if int(mb[52]) == 1 else False
return tcas_ra
def emergency_status(msg) -> int:
"""Decode aircraft emergency status.
Value Meaning
----- -----------------------
0 No emergency
1 General emergency
2 Lifeguard/medical emergency
3 Minimum fuel
4 No communications
5 Unlawful interference
6 Downed aircraft
7 Reserved
Args:
msg (str): 28 bytes hexadecimal message string
Returns:
int: Emergency status
"""
if common.typecode(msg) != 29:
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:7])
if subtype == 1:
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain emergency status" % msg)
return common.bin2int(mb[53:56])

View File

@@ -36,5 +36,3 @@ from pyModeS.decoder.bds.bds60 import *
# MRAR and MHR
from pyModeS.decoder.bds.bds44 import *
from pyModeS.decoder.bds.bds45 import *
from pyModeS.py_common import fs, dr, um

View File

@@ -1,48 +1,5 @@
"""
Decode short roll call surveillance replies, with downlink format 4 or 5
Warpper for short roll call surveillance replies DF=4/5
[To be implemented]
"""
from pyModeS import common
from pyModeS.py_common import fs, dr, um
def _checkdf(func):
"""Ensure downlink format is 4 or 5."""
def wrapper(msg):
df = common.df(msg)
if df not in [4, 5]:
raise RuntimeError(
"Incorrect downlink format, expect 4 or 5, got {}".format(df)
)
return func(msg)
return wrapper
@_checkdf
def altitude(msg):
"""Decode altitude.
Args:
msg (String): 14 hexdigits string
Returns:
int: altitude in ft
"""
return common.altcode(msg)
@_checkdf
def identity(msg):
"""Decode squawk code.
Args:
msg (String): 14 hexdigits string
Returns:
string: squawk code
"""
return common.idcode(msg)

View File

@@ -1,5 +1,4 @@
from pyModeS import common
from textwrap import wrap
def uplink_icao(msg):
@@ -24,200 +23,3 @@ def uf(msg):
"""Decode Uplink Format value, bits 1 to 5."""
ufbin = common.hex2bin(msg[:2])
return min(common.bin2int(ufbin[0:5]), 24)
def bds(msg):
"""Decode requested BDS register from selective (Roll Call) interrogation."""
UF = uf(msg)
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
if uf(msg) in {4, 5, 20, 21}:
di = mbytes[1] & 0x7 # DI - Designator Identification
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16
if di == 7:
RRS = mbytes[2] & 0x0F
BDS2 = RRS
elif di == 3:
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
BDS2 = RRS
else:
BDS2 = 0 # for other values of DI, the BDS2 is assumed 0 (as per ICAO Annex 10 Vol IV)
return str(format(BDS1,"X")) + str(format(BDS2,"X"))
else:
return None
else:
return None
def pr(msg):
"""Decode PR (probability of reply) field from All Call interrogation.
Interpretation:
0 signifies reply with probability of 1
1 signifies reply with probability of 1/2
2 signifies reply with probability of 1/4
3 signifies reply with probability of 1/8
4 signifies reply with probability of 1/16
5, 6, 7 not assigned
8 signifies disregard lockout, reply with probability of 1
9 signifies disregard lockout, reply with probability of 1/2
10 signifies disregard lockout, reply with probability of 1/4
11 signifies disregard lockout, reply with probability of 1/8
12 signifies disregard lockout, reply with probability of 1/16
13, 14, 15 not assigned.
"""
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
if uf(msg) == 11:
return ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7)
else:
return None
def ic(msg):
"""Decode IC (interrogator code) from a ground-based interrogation."""
UF = uf(msg)
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
IC = None
BDS2 = ""
if uf(msg) == 11:
codeLabel = mbytes[1] & 0x7
icField = (mbytes[1] >> 3) & 0xF
# Store the Interogator Code
ic_switcher = {
0: "II" + str(icField),
1: "SI" + str(icField),
2: "SI" + str(icField + 16),
3: "SI" + str(icField + 32),
4: "SI" + str(icField + 48),
}
IC = ic_switcher.get(codeLabel, "")
if uf(msg) in {4, 5, 20, 21}:
di = mbytes[1] & 0x7
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16
if di == 0 or di == 1 or di == 7:
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
elif di == 3:
# SI
SI = (mbytes[2] >> 2) & 0x3F
IC = "SI" + str(SI)
return IC
def lockout(msg):
"""Decode the lockout command from selective (Roll Call) interrogation."""
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
if uf(msg) in {4, 5, 20, 21}:
lockout = False
di = mbytes[1] & 0x7
if di == 7:
# LOS
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
elif di == 3:
# LSS
if ((mbytes[2] & 0x2) >> 1) == 1:
lockout = True
return lockout
else:
return None
def uplink_fields(msg):
"""Decode individual fields of a ground-based interrogation."""
msgbin = common.hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(common.bin2int, msgbin_split))
PR = ""
LOS = ""
IC = ""
lockout = False
di = ""
RR = ""
RRS = ""
BDS = ""
if uf(msg) == 11:
# Probability of Reply decoding
PR = ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7)
# Get cl and ic bit fields from the data
# Decode the SI or II interrogator code
codeLabel = mbytes[1] & 0x7
icField = (mbytes[1] >> 3) & 0xF
# Store the Interogator Code
ic_switcher = {
0: "II" + str(icField),
1: "SI" + str(icField),
2: "SI" + str(icField + 16),
3: "SI" + str(icField + 32),
4: "SI" + str(icField + 48),
}
IC = ic_switcher.get(codeLabel, "")
if uf(msg) in {4, 5, 20, 21}:
# Decode the DI and get the lockout information conveniently (LSS or LOS)
# DI - Designator Identification
di = mbytes[1] & 0x7
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16
BDS2 = 0
if di == 0 or di == 1:
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
elif di == 7:
# LOS
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
RRS = mbytes[2] & 0x0F
BDS2 = RRS
elif di == 3:
# LSS
if ((mbytes[2] & 0x2) >> 1) == 1:
lockout = True
# SI
SI = (mbytes[2] >> 2) & 0x3F
IC = "SI" + str(SI)
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
BDS2 = RRS
if RR > 15:
BDS = str(format(BDS1,"X")) + str(format(BDS2,"X"))
return {
"DI": di,
"IC": IC,
"LOS": lockout,
"PR": PR,
"RR": RR,
"RRS": RRS,
"BDS": BDS,
}

View File

@@ -0,0 +1,68 @@
from .bds.bds08 import me08
from .bds.bds09 import me09
from pyModeS import common
def encode_adsb(**kwargs):
"""Encode ADS-B message.
Args:
icao (string): Transponder ICAO address (6 hexdigits)
capability (int): Transponder capability, between 0 and 7
typecode (int): Typecode, less than 32
callsign (string): Callsign (6 hexdigits)
category (int): Aircraft category, between 0 and 7, Default to 0.
speed (int): Speed in knots.
angle (float): Track angle or heading angle in degrees.
vertical_rate (int): vertical rate in feet/minute
intent_change (int): Intent change flag, 0 or 1. Default to 0.
ifr_capability (int): IFR capability flag, 0 or 1. Default to 1.
navigation_quality (int): NUC (ver 0) or NACv (ver 1, 2), between 0 and 7.
Default to 0.
supersonic (bool): Is this a supersonic flight? Default to False.
speed_type (str): Speed type: GS, IAS, or TAS. Default to GS.
vertical_rate_source (str): GNSS or BARO. Default to BARO.
gnss_baro_alt_diff (int): Different between GNSS and barometric altitude in feet.
Negative value indicates GNSS altitude below barometric altitude. Default to 0
Returns:
string: 28 hexdigits raw message
"""
tc = kwargs.get("typecode")
if 1 <= tc <= 4:
me = me08(**kwargs)
elif tc == 19:
me = me09(**kwargs)
msg = _constuct(**dict(kwargs, me=me))
return msg
def _constuct(**kwargs):
icao = kwargs.get("icao")
me = kwargs.get("me")
capability = kwargs.get("capability", 6)
if icao is None or len(icao) != 6:
raise Exception("Transponder address must be 6 hexadecimal characters.")
if me is None or len(me) != 14:
raise Exception("Message be 14 hexadecimal characters.")
if capability > 6:
raise Exception("Transponder capability must be smaller than 7.")
header_bin = "10001" + "{0:03b}".format(capability)
header_hex = "{0:02X}".format(int(header_bin, 2))
msg = header_hex + icao + me + "000000"
pi = common.crc(msg, encode=True)
pi_hex = "{0:06X}".format(pi)
msg = msg[:-6] + pi_hex
return msg

View File

@@ -0,0 +1,5 @@
# ------------------------------------------
# BDS 0,5
# ADS-B TC=9-18
# Airborn position
# ------------------------------------------

View File

@@ -0,0 +1,5 @@
# ------------------------------------------
# BDS 0,6
# ADS-B TC=5-8
# Surface position
# ------------------------------------------

View File

@@ -0,0 +1,40 @@
# ------------------------------------------
# BDS 0,8
# ADS-B TC=1-4
# Aircraft identitification and category
# ------------------------------------------
from pyModeS import common
charmap = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######"
def me08(callsign, **kwargs):
cs = callsign
tc = kwargs.get("typecode")
cat = kwargs.get("category", 0)
if len(cs) > 8:
raise Exception("callsign must contain less than 9 characters")
if tc > 4:
raise Exception("typecode must be less 5")
if cat > 7:
raise Exception("category must be less 8")
if not cs.isalnum():
raise Exception("callsign must only contain alphanumeric characters")
cs = "{:<8}".format(cs.upper())
idx = [charmap.index(c) for c in cs]
me_bin = (
"{0:05b}".format(tc)
+ "{0:03b}".format(cat)
+ "".join("{0:06b}".format(i) for i in idx)
)
me_hex = "{0:04X}".format(int(me_bin, 2))
return me_hex

View File

@@ -0,0 +1,119 @@
# ------------------------------------------
# BDS 0,9
# ADS-B TC=19
# Aircraft Airborn velocity
# ------------------------------------------
import numpy as np
def me09(speed, angle, vertical_rate, **kwargs):
spd = speed
agl = angle
vr = vertical_rate
tc = kwargs.get("typecode")
intent = kwargs.get("intent_change", 0)
ifr = kwargs.get("ifr_capability", 1)
navq = kwargs.get("navigation_quality", 0)
supersonic = kwargs.get("supersonic", False)
spd_type = kwargs.get("speed_type", "gs").lower()
vr_source = kwargs.get("vertical_rate_source", "baro").lower()
alt_diff = kwargs.get("gnss_baro_alt_diff", 0)
if tc != 19:
raise Exception("Typecode must be 19.")
if intent not in (0, 1):
raise Exception("Intent change flag must be 0 or 1.")
if ifr not in (0, 1):
raise Exception("IFR capability flag must be 0 or 1.")
if type(supersonic) != bool:
raise Exception("Subsonic flag must be True or False.")
if navq > 7:
raise Exception("Navigation quality indicator must be smaller than 8.")
if spd_type not in ["gs", "tas"]:
raise Exception("Speed type must be 'gs', 'ias', or 'tas'.")
if vr_source not in ["baro", "gnss"]:
raise Exception("Vertical rate source must be 'baro' or 'gnss'.")
me_bin = ""
# typecode
me_bin += "{0:05b}".format(tc)
# sub-type
if supersonic:
if spd_type == "gs":
me_bin += "010"
else:
me_bin += "100"
else:
if spd_type == "gs":
me_bin += "001"
else:
me_bin += "011"
# intent, ifr, navigation quality
me_bin += str(intent) + str(ifr) + "{0:03b}".format(navq)
# speed and angle part
if spd_type == "gs":
vx = spd * np.sin(np.radians(agl))
vy = spd * np.cos(np.radians(agl))
if supersonic:
vx /= 4
vy /= 4
vx = int(round(vx))
vy = int(round(vy))
sew = "0" if vx >= 0 else "1"
sns = "0" if vy >= 0 else "1"
vew = "{0:010b}".format(min(abs(vx), 1023) + 1)
vns = "{0:010b}".format(min(abs(vy), 1023) + 1)
me_bin += sew + vew + sns + vns
elif spd_type == "ias" or spd_type == "tas":
hdg = int(round(agl * 1024 / 360))
hdg = min(hdg, 1023)
air_type = "1" if spd_type == "tas" else "0"
if supersonic:
spd /= 4
spd = min(int(round(spd)), 1023)
me_bin += "1" + "{0:010b}".format(hdg) + air_type + "{0:010b}".format(spd)
# vertical rate source
me_bin += "1" if vr_source == "baro" else "0"
# vertical rate
me_bin += "0" if vr > 0 else "1"
vr = int(round((abs(vr) / 64 + 1)))
vr = min(vr, 511)
me_bin += "{0:09b}".format(vr)
# reserved
me_bin += "00"
# altitude difference
me_bin += "1" if alt_diff < 0 else "0"
alt_diff = int(round(abs(alt_diff) / 25 + 1))
alt_diff = min(alt_diff, 127)
me_bin += "{0:07b}".format(alt_diff)
print(me_bin)
# convert to hexdigits
me_hex = "{0:04X}".format(int(me_bin, 2))
return me_hex

View File

@@ -35,18 +35,18 @@ ft = 0.3048 # ft -> m
fpm = 0.00508 # ft/min -> m/s
inch = 0.0254 # inch -> m
sqft = 0.09290304 # 1 square foot
nm = 1852 # nautical mile -> m
nm = 1852.0 # nautical mile -> m
lbs = 0.453592 # pound -> kg
g0 = 9.80665 # m/s2, Sea level gravity constant
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
p0 = 101325 # Pa, air pressure, sea level ISA
p0 = 101325.0 # Pa, air pressure, sea level ISA
rho0 = 1.225 # kg/m3, air density, sea level ISA
T0 = 288.15 # K, temperature, sea level ISA
gamma = 1.40 # cp/cv for air
gamma1 = 0.2 # (gamma-1)/2 for air
gamma2 = 3.5 # gamma/(gamma-1) for air
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
r_earth = 6371000 # m, average earth radius
r_earth = 6371000.0 # m, average earth radius
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
@@ -94,8 +94,8 @@ def distance(lat1, lon1, lat2, lon2, H=0):
"""
# phi = 90 - latitude
phi1 = np.radians(90 - lat1)
phi2 = np.radians(90 - lat2)
phi1 = np.radians(90.0 - lat1)
phi2 = np.radians(90.0 - lat2)
# theta = longitude
theta1 = np.radians(lon1)
@@ -158,16 +158,16 @@ def tas2eas(Vtas, H):
def cas2tas(Vcas, H):
"""Calibrated Airspeed to True Airspeed"""
p, rho, T = atmos(H)
qdyn = p0 * ((1 + rho0 * Vcas * Vcas / (7 * p0)) ** 3.5 - 1.0)
Vtas = np.sqrt(7 * p / rho * ((1 + qdyn / p) ** (2 / 7.0) - 1.0))
qdyn = p0 * ((1.0 + rho0 * Vcas * Vcas / (7.0 * p0)) ** 3.5 - 1.0)
Vtas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
return Vtas
def tas2cas(Vtas, H):
"""True Airspeed to Calibrated Airspeed"""
p, rho, T = atmos(H)
qdyn = p * ((1 + rho * Vtas * Vtas / (7 * p)) ** 3.5 - 1.0)
Vcas = np.sqrt(7 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2 / 7.0) - 1.0))
qdyn = p * ((1.0 + rho * Vtas * Vtas / (7.0 * p)) ** 3.5 - 1.0)
Vcas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
return Vcas

View File

@@ -1,14 +1,8 @@
import time
import traceback
import numpy as np
import pyModeS as pms
try:
import rtlsdr
except:
print("------------------------------------------------------------------------")
print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !")
print("------------------------------------------------------------------------")
from rtlsdr import RtlSdr
import time
sampling_rate = 2e6
smaples_per_microsec = 2
@@ -27,7 +21,7 @@ class RtlReader(object):
def __init__(self, **kwargs):
super(RtlReader, self).__init__()
self.signal_buffer = [] # amplitude of the sample only
self.sdr = rtlsdr.RtlSdr()
self.sdr = RtlSdr()
self.sdr.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency
self.sdr.gain = "auto"
@@ -37,8 +31,6 @@ class RtlReader(object):
self.stop_flag = False
self.noise_floor = 1e6
self.exception_queue = None
def _calc_noise(self):
"""Calculate noise floor"""
window = smaples_per_microsec * 100
@@ -170,7 +162,6 @@ class RtlReader(object):
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self.raw_pipe_in = raw_pipe_in
self.exception_queue = exception_queue
self.stop_flag = stop_flag
try:
@@ -182,8 +173,8 @@ class RtlReader(object):
except Exception as e:
tb = traceback.format_exc()
if self.exception_queue is not None:
self.exception_queue.put(tb)
if exception_queue is not None:
exception_queue.put(tb)
raise e

View File

@@ -7,6 +7,11 @@ import pyModeS as pms
import traceback
import zmq
if sys.version_info > (3, 0):
PY_VERSION = 3
else:
PY_VERSION = 2
class TcpClient(object):
def __init__(self, host, port, datatype):
@@ -23,8 +28,6 @@ class TcpClient(object):
self.raw_pipe_in = None
self.stop_flag = False
self.exception_queue = None
def connect(self):
self.socket = zmq.Context().socket(zmq.STREAM)
self.socket.setsockopt(zmq.LINGER, 0)
@@ -32,7 +35,7 @@ class TcpClient(object):
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
def stop(self):
self.socket.close()
self.socket.disconnect()
def read_raw_buffer(self):
""" Read raw ADS-B data type.
@@ -252,7 +255,6 @@ class TcpClient(object):
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self.raw_pipe_in = raw_pipe_in
self.exception_queue = exception_queue
self.stop_flag = stop_flag
self.connect()
@@ -260,6 +262,9 @@ class TcpClient(object):
try:
received = [i for i in self.socket.recv(4096)]
if PY_VERSION == 2:
received = [ord(i) for i in received]
self.buffer.extend(received)
# print(''.join(x.encode('hex') for x in self.buffer))
@@ -277,12 +282,9 @@ class TcpClient(object):
# raise RuntimeError("test exception")
except zmq.error.Again:
continue
except Exception as e:
tb = traceback.format_exc()
if self.exception_queue is not None:
self.exception_queue.put(tb)
exception_queue.put(tb)
raise e
@@ -292,7 +294,4 @@ if __name__ == "__main__":
port = int(sys.argv[2])
datatype = sys.argv[3]
client = TcpClient(host=host, port=port, datatype=datatype)
try:
client.run()
finally:
client.stop()
client.run()

View File

@@ -12,6 +12,9 @@ from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, "w")
support_rawtypes = ["raw", "beast", "skysense"]
parser = argparse.ArgumentParser()
@@ -23,9 +26,8 @@ parser.add_argument(
)
parser.add_argument(
"--connect",
help="Define server, port and data type. Supported data types are: {}".format(
support_rawtypes
),
help="Define server, port and data type. Supported data types are: %s"
% support_rawtypes,
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
@@ -84,10 +86,6 @@ if DUMPTO is not None:
sys.exit(1)
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, "w")
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
exception_queue = multiprocessing.Queue()

View File

@@ -1,3 +0,0 @@
# https://github.com/embray/setup.cfg
[metadata]
license_file = LICENSE

View File

@@ -8,12 +8,20 @@ Steps for deploying a new version:
1. Increase the version number
2. remove the old deployment under [dist] and [build] folder
3. run: python setup.py sdist
run: python setup.py bdist_wheel --universal
4. twine upload dist/*
"""
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# Compile some parts
from setuptools.extension import Extension
from Cython.Build import cythonize
extensions = [Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"])]
# To use a consistent encoding
from codecs import open
from os import path
@@ -24,38 +32,78 @@ here = path.abspath(path.dirname(__file__))
with open(path.join(here, "README.rst"), encoding="utf-8") as f:
long_description = f.read()
details = dict(
setup(
name="pyModeS",
version="2.10",
# 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="2.5",
description="Python Mode-S and ADS-B Decoder",
long_description=long_description,
# The project's main homepage.
url="https://github.com/junzis/pyModeS",
# Author details
author="Junzi Sun",
author_email="j.sun-1@tudelft.nl",
# Choose your license
license="GNU GPL v3",
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 4 - Beta",
# Indicate who your project is intended for
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
# Pick your license as you wish (should match "license" above)
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
# "Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
],
ext_modules=cythonize(extensions),
# What does your project relate to?
keywords="Mode-S ADS-B EHS ELS Comm-B",
# You can just specify the packages manually here if your project is
# simple. Or you can use find_packages().
packages=find_packages(exclude=["contrib", "docs", "tests"]),
install_requires=["numpy", "pyzmq"],
extras_require={"fast": ["Cython"]},
package_data={"pyModeS": ["*.pyx", "*.pxd", "py.typed"]},
# Alternatively, if you want to distribute just a my_module.py, uncomment
# this:
# py_modules=["my_module"],
# List run-time dependencies here. These will be installed by pip when
# your project is installed. For an analysis of "install_requires" vs pip's
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=["numpy", "pyzmq", "pyrtlsdr"],
# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,
# for example:
# $ pip install -e .[dev,test]
# extras_require={
# 'dev': ['check-manifest'],
# 'test': ['coverage'],
# },
# If there are data files included in your packages that need to be
# installed, specify them here. If using Python 2.6 or less, then these
# have to be included in MANIFEST.in as well.
# package_data={
# 'sample': ['package_data.dat'],
# },
# Although 'package_data' is the preferred approach, in some case you may
# need to place data files outside of your packages. See:
# http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
# data_files=[('my_data', ['data/data_file'])],
# To provide executable scripts, use entry points in preference to the
# "scripts" keyword. Entry points provide cross-platform support and allow
# pip to create the appropriate form of executable for the target platform.
# entry_points={
# 'console_scripts': [
# 'sample=sample:main',
# ],
# },
scripts=["pyModeS/streamer/modeslive"],
)
try:
from setuptools.extension import Extension
from Cython.Build import cythonize
extensions = [Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"])]
setup(**dict(details, ext_modules=cythonize(extensions)))
except:
setup(**details)

View File

@@ -46,7 +46,7 @@ def bds_info(BDS, m):
)
else:
info = []
info = None
return info
@@ -87,5 +87,5 @@ def commb_decode_all(df, n=None):
if __name__ == "__main__":
commb_decode_all(df=20, n=500)
commb_decode_all(df=21, n=500)
commb_decode_all(df=20, n=100)
commb_decode_all(df=21, n=100)

View File

@@ -76,30 +76,10 @@ def test_adsb_velocity():
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, 182.88, -832, "GS")
assert vas == (375, 243.98, -2304, "TAS")
assert vgs_surface == (19, 42.2, 0, "GS")
assert vgs_surface == (19.0, 42.2, 0, "GS")
assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
def test_adsb_emergency():
assert not adsb.is_emergency("8DA2C1B6E112B600000000760759")
assert adsb.emergency_state("8DA2C1B6E112B600000000760759") == 0
assert adsb.emergency_squawk("8DA2C1B6E112B600000000760759") == "6615"
def test_adsb_target_state_status():
sel_alt = adsb.selected_altitude("8DA05629EA21485CBF3F8CADAEEB")
assert sel_alt == (16992, "MCP/FCU")
assert adsb.baro_pressure_setting("8DA05629EA21485CBF3F8CADAEEB") == 1012.8
assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB")== 66.8
assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") == True
assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") == True
assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") == False
assert adsb.approach_mode("8DA05629EA21485CBF3F8CADAEEB") == False
assert adsb.tcas_operational("8DA05629EA21485CBF3F8CADAEEB") == True
assert adsb.lnav_mode("8DA05629EA21485CBF3F8CADAEEB") == True
# def test_nic():
# assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
# assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1

View File

@@ -1,13 +0,0 @@
from pyModeS import allcall
def test_icao():
assert allcall.icao("5D484FDEA248F5") == "484FDE"
def test_interrogator():
assert allcall.interrogator("5D484FDEA248F5") == "SI6"
def test_capability():
assert allcall.capability("5D484FDEA248F5")[0] == 5

View File

@@ -17,8 +17,8 @@ def test_bds_infer():
def test_bds_is50or60():
assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
assert bds.is50or60("A8001EBCFFFB23286004A73F6A5B", 320, 250, 14000) == "BDS50"
assert bds.is50or60("A8001EBCFE1B29287FDCA807BCFC", 320, 250, 14000) == "BDS50"
assert bds.is50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == "BDS50"
assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == "BDS60"
def test_surface_position():

View File

@@ -1,59 +1,57 @@
try:
from pyModeS import c_common
from pyModeS.decoder import c_common as common
def test_conversions():
assert c_common.hex2bin("6E") == "01101110"
assert c_common.bin2hex("01101110") == "6E"
assert c_common.bin2hex("1101110") == "6E"
assert common.hex2bin("6E406B") == "011011100100000001101011"
def test_crc_decode():
assert c_common.crc("8D406B902015A678D4D220AA4BDA") == 0
assert c_common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
assert c_common.crc("8d45cab390c39509496ca9a32912") == 0
assert c_common.crc("8d74802958c904e6ef4ba0184d5c") == 0
assert c_common.crc("8d4400cd9b0000b4f87000e71a10") == 0
assert c_common.crc("8d4065de58a1054a7ef0218e226a") == 0
assert common.crc("8D406B902015A678D4D220AA4BDA") == 0
assert common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
assert common.crc("8d45cab390c39509496ca9a32912") == 0
assert common.crc("8d74802958c904e6ef4ba0184d5c") == 0
assert common.crc("8d4400cd9b0000b4f87000e71a10") == 0
assert common.crc("8d4065de58a1054a7ef0218e226a") == 0
assert c_common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
assert c_common.crc("a800089d8094e33a6004e4b8a522") == 4805588
assert c_common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
assert c_common.crc("a0000410bc900010a40000f5f477") == 11727682
assert c_common.crc("8d4ca251204994b1c36e60a5343d") == 16
assert c_common.crc("b0001718c65632b0a82040715b65") == 353333
assert common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
assert common.crc("a800089d8094e33a6004e4b8a522") == 4805588
assert common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
assert common.crc("a0000410bc900010a40000f5f477") == 11727682
assert common.crc("8d4ca251204994b1c36e60a5343d") == 16
assert common.crc("b0001718c65632b0a82040715b65") == 353333
def test_crc_encode():
parity = c_common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert parity == 11160538
def test_icao():
assert c_common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
assert c_common.icao("A0001839CA3800315800007448D9") == "400940"
assert c_common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2"
assert c_common.icao("A000029CFFBAA11E2004727281F1") == "4243D0"
assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
assert common.icao("A0001839CA3800315800007448D9") == "400940"
assert common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2"
assert common.icao("A000029CFFBAA11E2004727281F1") == "4243D0"
def test_modes_altcode():
assert c_common.altcode("A02014B400000000000000F9D514") == 32300
assert common.altcode("A02014B400000000000000F9D514") == 32300
def test_modes_idcode():
assert c_common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346"
assert common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346"
def test_graycode_to_altitude():
assert c_common.gray2alt("00000000010") == -1000
assert c_common.gray2alt("00000001010") == -500
assert c_common.gray2alt("00000011011") == -100
assert c_common.gray2alt("00000011010") == 0
assert c_common.gray2alt("00000011110") == 100
assert c_common.gray2alt("00000010011") == 600
assert c_common.gray2alt("00000110010") == 1000
assert c_common.gray2alt("00001001001") == 5800
assert c_common.gray2alt("00011100100") == 10300
assert c_common.gray2alt("01100011010") == 32000
assert c_common.gray2alt("01110000100") == 46300
assert c_common.gray2alt("01010101100") == 50200
assert c_common.gray2alt("11011110100") == 73200
assert c_common.gray2alt("10000000011") == 126600
assert c_common.gray2alt("10000000001") == 126700
assert common.gray2alt("00000000010") == -1000
assert common.gray2alt("00000001010") == -500
assert common.gray2alt("00000011011") == -100
assert common.gray2alt("00000011010") == 0
assert common.gray2alt("00000011110") == 100
assert common.gray2alt("00000010011") == 600
assert common.gray2alt("00000110010") == 1000
assert common.gray2alt("00001001001") == 5800
assert common.gray2alt("00011100100") == 10300
assert common.gray2alt("01100011010") == 32000
assert common.gray2alt("01110000100") == 46300
assert common.gray2alt("01010101100") == 50200
assert common.gray2alt("11011110100") == 73200
assert common.gray2alt("10000000011") == 126600
assert common.gray2alt("10000000001") == 126700
except:

62
tests/test_common.py Normal file
View File

@@ -0,0 +1,62 @@
from pyModeS import common
def test_conversions():
assert common.hex2bin("6E406B") == "011011100100000001101011"
def test_crc_decode():
assert common.crc_legacy("8D406B902015A678D4D220AA4BDA") == 0
assert common.crc("8D406B902015A678D4D220AA4BDA") == 0
assert common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
assert common.crc("8d45cab390c39509496ca9a32912") == 0
assert common.crc("8d49d3d4e1089d00000000744c3b") == 0
assert common.crc("8d74802958c904e6ef4ba0184d5c") == 0
assert common.crc("8d4400cd9b0000b4f87000e71a10") == 0
assert common.crc("8d4065de58a1054a7ef0218e226a") == 0
assert common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
assert common.crc("a800089d8094e33a6004e4b8a522") == 4805588
assert common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
assert common.crc("a0000410bc900010a40000f5f477") == 11727682
assert common.crc("8d4ca251204994b1c36e60a5343d") == 16
assert common.crc("b0001718c65632b0a82040715b65") == 353333
def test_crc_encode():
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert parity == 11160538
def test_icao():
assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
assert common.icao("A0001839CA3800315800007448D9") == "400940"
assert common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2"
assert common.icao("A000029CFFBAA11E2004727281F1") == "4243D0"
def test_modes_altcode():
assert common.altcode("A02014B400000000000000F9D514") == 32300
def test_modes_idcode():
assert common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346"
def test_graycode_to_altitude():
assert common.gray2alt("00000000010") == -1000
assert common.gray2alt("00000001010") == -500
assert common.gray2alt("00000011011") == -100
assert common.gray2alt("00000011010") == 0
assert common.gray2alt("00000011110") == 100
assert common.gray2alt("00000010011") == 600
assert common.gray2alt("00000110010") == 1000
assert common.gray2alt("00001001001") == 5800
assert common.gray2alt("00011100100") == 10300
assert common.gray2alt("01100011010") == 32000
assert common.gray2alt("01110000100") == 46300
assert common.gray2alt("01010101100") == 50200
assert common.gray2alt("11011110100") == 73200
assert common.gray2alt("10000000011") == 126600
assert common.gray2alt("10000000001") == 126700

23
tests/test_encoder.py Normal file
View File

@@ -0,0 +1,23 @@
from pyModeS import encoder
def test_identification():
msg = encoder.encode_adsb(
icao="406B90", typecode=4, capability=5, category=0, callsign="EZY85MH"
)
assert msg == "8D406B902015A678D4D220AA4BDA"
def test_speed():
msg = encoder.encode_adsb(
icao="485020",
typecode=19,
capability=5,
speed_type="gs",
speed=159,
angle=182.88,
vertical_rate=-832,
vertical_rate_source="gnss",
gnss_baro_alt_diff=550,
)
assert msg == "8D485020994409940838175B284F"

View File

@@ -1,64 +0,0 @@
from pyModeS import py_common
def test_conversions():
assert py_common.hex2bin("6E") == "01101110"
assert py_common.bin2hex("01101110") == "6E"
assert py_common.bin2hex("1101110") == "6E"
def test_crc_decode():
assert py_common.crc_legacy("8D406B902015A678D4D220AA4BDA") == 0
assert py_common.crc("8D406B902015A678D4D220AA4BDA") == 0
assert py_common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
assert py_common.crc("8d45cab390c39509496ca9a32912") == 0
assert py_common.crc("8d49d3d4e1089d00000000744c3b") == 0
assert py_common.crc("8d74802958c904e6ef4ba0184d5c") == 0
assert py_common.crc("8d4400cd9b0000b4f87000e71a10") == 0
assert py_common.crc("8d4065de58a1054a7ef0218e226a") == 0
assert py_common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
assert py_common.crc("a800089d8094e33a6004e4b8a522") == 4805588
assert py_common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
assert py_common.crc("a0000410bc900010a40000f5f477") == 11727682
assert py_common.crc("8d4ca251204994b1c36e60a5343d") == 16
assert py_common.crc("b0001718c65632b0a82040715b65") == 353333
def test_crc_encode():
parity = py_common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert parity == 11160538
def test_icao():
assert py_common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
assert py_common.icao("A0001839CA3800315800007448D9") == "400940"
assert py_common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2"
assert py_common.icao("A000029CFFBAA11E2004727281F1") == "4243D0"
def test_modes_altcode():
assert py_common.altcode("A02014B400000000000000F9D514") == 32300
def test_modes_idcode():
assert py_common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346"
def test_graycode_to_altitude():
assert py_common.gray2alt("00000000010") == -1000
assert py_common.gray2alt("00000001010") == -500
assert py_common.gray2alt("00000011011") == -100
assert py_common.gray2alt("00000011010") == 0
assert py_common.gray2alt("00000011110") == 100
assert py_common.gray2alt("00000010011") == 600
assert py_common.gray2alt("00000110010") == 1000
assert py_common.gray2alt("00001001001") == 5800
assert py_common.gray2alt("00011100100") == 10300
assert py_common.gray2alt("01100011010") == 32000
assert py_common.gray2alt("01110000100") == 46300
assert py_common.gray2alt("01010101100") == 50200
assert py_common.gray2alt("11011110100") == 73200
assert py_common.gray2alt("10000000011") == 126600
assert py_common.gray2alt("10000000001") == 126700

View File

@@ -1,22 +0,0 @@
from pyModeS import surv
def test_fs():
assert surv.fs("2A00516D492B80")[0] == 2
def test_dr():
assert surv.dr("2A00516D492B80")[0] == 0
def test_um():
assert surv.um("200CBE4ED80137")[0] == 9
assert surv.um("200CBE4ED80137")[1] == 1
def test_identity():
assert surv.identity("2A00516D492B80") == "0356"
def test_altitude():
assert surv.altitude("20001718029FCD") == 36000