merge main
This commit is contained in:
parent
67ced74c0a
commit
f9225bf375
12
.coveragerc
Normal file
12
.coveragerc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[run]
|
||||||
|
branch = True
|
||||||
|
include = */pyModeS/*
|
||||||
|
omit = *tests*
|
||||||
|
|
||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
coverage: ignore
|
||||||
|
raise NotImplementedError
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
ignore_errors = True
|
29
.github/workflows/pypi-publish.yml
vendored
Normal file
29
.github/workflows/pypi-publish.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 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/*
|
56
.github/workflows/run-tests.yml
vendored
Normal file
56
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request_target:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install -U pip numpy cython mypy
|
||||||
|
pip install -U pytest codecov pytest-cov
|
||||||
|
pip install .
|
||||||
|
|
||||||
|
- name: Type checking
|
||||||
|
if: ${{ env.PYTHON_VERSION != '3.7' }}
|
||||||
|
run: |
|
||||||
|
mypy pyModeS
|
||||||
|
|
||||||
|
- name: Run tests (without Cython)
|
||||||
|
run: |
|
||||||
|
pytest tests --cov --cov-report term-missing
|
||||||
|
|
||||||
|
- name: Install with Cython
|
||||||
|
run: |
|
||||||
|
pip install -U cython
|
||||||
|
pip uninstall -y pymodes
|
||||||
|
pip install .
|
||||||
|
|
||||||
|
- name: Run tests (with Cython)
|
||||||
|
run: |
|
||||||
|
pytest tests
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
if: ${{ github.event_name != 'pull_request_target' && env.PYTHON_VERSION == '3.10' }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
with:
|
||||||
|
env_vars: PYTHON_VERSION
|
44
README.rst
44
README.rst
@ -1,7 +1,7 @@
|
|||||||
The Python ADS-B/Mode-S Decoder
|
The Python ADS-B/Mode-S Decoder
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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,21 +72,35 @@ Installation examples::
|
|||||||
# stable version
|
# stable version
|
||||||
pip install pyModeS
|
pip install pyModeS
|
||||||
|
|
||||||
|
# conda (compiled) version
|
||||||
|
conda install -c conda-forge pymodes
|
||||||
|
|
||||||
# development version
|
# development version
|
||||||
pip install git+https://github.com/junzis/pyModeS
|
pip install git+https://github.com/junzis/pyModeS
|
||||||
|
|
||||||
|
|
||||||
Dependencies ``numpy``, ``pyzmq`` and ``pyrtlsdr`` are installed automatically during previous installations processes.
|
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
|
||||||
|
|
||||||
|
|
||||||
Advanced installation (using c modules)
|
Advanced installation (using c modules)
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
If you want to make use of the (faster) c module, install ``pyModeS`` as follows::
|
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
|
git clone https://github.com/junzis/pyModeS
|
||||||
cd pyModeS
|
cd pyModeS
|
||||||
make ext
|
pip install .[fast]
|
||||||
make install
|
|
||||||
|
|
||||||
|
|
||||||
View live traffic (modeslive)
|
View live traffic (modeslive)
|
||||||
@ -112,7 +126,7 @@ General usage::
|
|||||||
Live with RTL-SDR
|
Live with RTL-SDR
|
||||||
*******************
|
*******************
|
||||||
|
|
||||||
If you have an RTL-SDR receiver plugged to the computer, you can connect it with ``rtlsdr`` source switch, shown as follows::
|
If you have an RTL-SDR receiver connected to your computer, you can use the ``rtlsdr`` source switch (require ``pyrtlsdr`` package), with command::
|
||||||
|
|
||||||
$ modeslive --source rtlsdr
|
$ modeslive --source rtlsdr
|
||||||
|
|
||||||
@ -261,8 +275,20 @@ Mode-S Enhanced Surveillance (EHS)
|
|||||||
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
|
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
|
||||||
|
|
||||||
|
|
||||||
Meteorological routine air report (MRAR) [Experimental]
|
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)
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
@ -273,8 +299,8 @@ Meteorological routine air report (MRAR) [Experimental]
|
|||||||
pms.commb.hum44(msg) # Humidity (%)
|
pms.commb.hum44(msg) # Humidity (%)
|
||||||
|
|
||||||
|
|
||||||
Meteorological hazard air report (MHR) [Experimental]
|
Meteorological hazard air report (MHR)
|
||||||
*******************************************************
|
+++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -4,18 +4,32 @@ import warnings
|
|||||||
try:
|
try:
|
||||||
from . import c_common as common
|
from . import c_common as common
|
||||||
from .c_common import *
|
from .c_common import *
|
||||||
except:
|
except Exception:
|
||||||
from . import py_common as common
|
from . import py_common as common # type: ignore
|
||||||
from .py_common import *
|
from .py_common import * # type: ignore
|
||||||
|
|
||||||
from .decoder import tell
|
from .decoder import tell
|
||||||
from .decoder import adsb
|
from .decoder import adsb
|
||||||
from .decoder import acas
|
from .decoder import acas
|
||||||
from .decoder import commb
|
from .decoder import commb
|
||||||
|
from .decoder import allcall
|
||||||
|
from .decoder import surv
|
||||||
from .decoder import bds
|
from .decoder import bds
|
||||||
from .extra import aero
|
from .extra import aero
|
||||||
from .extra import tcpclient
|
from .extra import tcpclient
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"common",
|
||||||
|
"tell",
|
||||||
|
"adsb",
|
||||||
|
"commb",
|
||||||
|
"allcall",
|
||||||
|
"surv",
|
||||||
|
"bds",
|
||||||
|
"aero",
|
||||||
|
"tcpclient",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
warnings.simplefilter("once", DeprecationWarning)
|
warnings.simplefilter("once", DeprecationWarning)
|
||||||
|
|
||||||
|
18
pyModeS/c_common.pyi
Normal file
18
pyModeS/c_common.pyi
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
def hex2bin(hexstr: str) -> str: ...
|
||||||
|
def bin2int(binstr: str) -> int: ...
|
||||||
|
def hex2int(hexstr: str) -> int: ...
|
||||||
|
def bin2hex(binstr: str) -> str: ...
|
||||||
|
def df(msg: str) -> int: ...
|
||||||
|
def crc(msg: str, encode: bool = False) -> int: ...
|
||||||
|
def floor(x: float) -> float: ...
|
||||||
|
def icao(msg: str) -> str: ...
|
||||||
|
def is_icao_assigned(icao: str) -> bool: ...
|
||||||
|
def typecode(msg: str) -> int: ...
|
||||||
|
def cprNL(lat: float) -> int: ...
|
||||||
|
def idcode(msg: str) -> str: ...
|
||||||
|
def squawk(binstr: str) -> str: ...
|
||||||
|
def altcode(msg: str) -> int: ...
|
||||||
|
def altitude(binstr: str) -> int: ...
|
||||||
|
def data(msg: str) -> str: ...
|
||||||
|
def allzeros(msg: str) -> bool: ...
|
||||||
|
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ...
|
@ -25,7 +25,7 @@ cdef unsigned char int_to_char(unsigned char i):
|
|||||||
@cython.boundscheck(False)
|
@cython.boundscheck(False)
|
||||||
@cython.overflowcheck(False)
|
@cython.overflowcheck(False)
|
||||||
cpdef str hex2bin(str hexstr):
|
cpdef str hex2bin(str hexstr):
|
||||||
"""Convert a hexdecimal string to binary string, with zero fillings."""
|
"""Convert a hexadecimal string to binary string, with zero fillings."""
|
||||||
# num_of_bits = len(hexstr) * 4
|
# num_of_bits = len(hexstr) * 4
|
||||||
cdef hexbytes = bytes(hexstr.encode())
|
cdef hexbytes = bytes(hexstr.encode())
|
||||||
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
|
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
|
||||||
@ -73,7 +73,7 @@ cpdef str bin2hex(str binstr):
|
|||||||
|
|
||||||
@cython.boundscheck(False)
|
@cython.boundscheck(False)
|
||||||
cpdef unsigned char df(str msg):
|
cpdef unsigned char df(str msg):
|
||||||
"""Decode Downlink Format vaule, bits 1 to 5."""
|
"""Decode Downlink Format value, bits 1 to 5."""
|
||||||
cdef str dfbin = hex2bin(msg[:2])
|
cdef str dfbin = hex2bin(msg[:2])
|
||||||
# return min(bin2int(dfbin[0:5]), 24)
|
# return min(bin2int(dfbin[0:5]), 24)
|
||||||
cdef long df = bin2int(dfbin[0:5])
|
cdef long df = bin2int(dfbin[0:5])
|
||||||
@ -228,7 +228,7 @@ cpdef int cprNL(double lat):
|
|||||||
|
|
||||||
cdef int nz = 15
|
cdef int nz = 15
|
||||||
cdef double a = 1 - cos(pi / (2 * nz))
|
cdef double a = 1 - cos(pi / (2 * nz))
|
||||||
cdef double b = cos(pi / 180.0 * fabs(lat)) ** 2
|
cdef double b = cos(pi / 180 * fabs(lat)) ** 2
|
||||||
cdef double nl = 2 * pi / (acos(1 - a / b))
|
cdef double nl = 2 * pi / (acos(1 - a / b))
|
||||||
NL = floor(nl)
|
NL = floor(nl)
|
||||||
return NL
|
return NL
|
||||||
@ -295,7 +295,7 @@ cpdef int altcode(str msg):
|
|||||||
@cython.wraparound(False)
|
@cython.wraparound(False)
|
||||||
cpdef int altitude(str binstr):
|
cpdef int altitude(str binstr):
|
||||||
|
|
||||||
if len(binstr) != 13 or set(binstr) != set('01'):
|
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
|
||||||
raise RuntimeError("Input must be 13 bits binary string")
|
raise RuntimeError("Input must be 13 bits binary string")
|
||||||
|
|
||||||
cdef bytearray _mbin = bytearray(binstr.encode())
|
cdef bytearray _mbin = bytearray(binstr.encode())
|
||||||
|
22
pyModeS/common.pyi
Normal file
22
pyModeS/common.pyi
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def hex2bin(hexstr: str) -> str: ...
|
||||||
|
def bin2int(binstr: str) -> int: ...
|
||||||
|
def hex2int(hexstr: str) -> int: ...
|
||||||
|
def bin2hex(binstr: str) -> str: ...
|
||||||
|
def df(msg: str) -> int: ...
|
||||||
|
def crc(msg: str, encode: bool = False) -> int: ...
|
||||||
|
def floor(x: float) -> float: ...
|
||||||
|
def icao(msg: str) -> Optional[str]: ...
|
||||||
|
def is_icao_assigned(icao: str) -> bool: ...
|
||||||
|
def typecode(msg: str) -> Optional[int]: ...
|
||||||
|
def cprNL(lat: float) -> int: ...
|
||||||
|
def idcode(msg: str) -> str: ...
|
||||||
|
def squawk(binstr: str) -> str: ...
|
||||||
|
def altcode(msg: str) -> Optional[int]: ...
|
||||||
|
def altitude(binstr: str) -> Optional[int]: ...
|
||||||
|
def gray2alt(binstr: str) -> Optional[int]: ...
|
||||||
|
def gray2int(binstr: str) -> int: ...
|
||||||
|
def data(msg: str) -> str: ...
|
||||||
|
def allzeros(msg: str) -> bool: ...
|
||||||
|
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ...
|
@ -1,8 +1,8 @@
|
|||||||
def tell(msg: str) -> None:
|
def tell(msg: str) -> None:
|
||||||
from pyModeS import common, adsb, commb, bds
|
from .. import common, adsb, commb, bds
|
||||||
|
|
||||||
def _print(label, value, unit=None):
|
def _print(label, value, unit=None):
|
||||||
print("%20s: " % label, end="")
|
print("%28s: " % label, end="")
|
||||||
print("%s " % value, end="")
|
print("%s " % value, end="")
|
||||||
if unit:
|
if unit:
|
||||||
print(unit)
|
print(unit)
|
||||||
@ -20,9 +20,14 @@ def tell(msg: str) -> None:
|
|||||||
_print("Protocol", "Mode-S Extended Squitter (ADS-B)")
|
_print("Protocol", "Mode-S Extended Squitter (ADS-B)")
|
||||||
|
|
||||||
tc = common.typecode(msg)
|
tc = common.typecode(msg)
|
||||||
|
|
||||||
|
if tc is None:
|
||||||
|
_print("ERROR", "Unknown typecode")
|
||||||
|
return
|
||||||
|
|
||||||
if 1 <= tc <= 4: # callsign
|
if 1 <= tc <= 4: # callsign
|
||||||
callsign = adsb.callsign(msg)
|
callsign = adsb.callsign(msg)
|
||||||
_print("Type", "Identitification and category")
|
_print("Type", "Identification and category")
|
||||||
_print("Callsign:", callsign)
|
_print("Callsign:", callsign)
|
||||||
|
|
||||||
if 5 <= tc <= 8: # surface position
|
if 5 <= tc <= 8: # surface position
|
||||||
@ -52,7 +57,9 @@ def tell(msg: str) -> None:
|
|||||||
|
|
||||||
if tc == 19:
|
if tc == 19:
|
||||||
_print("Type", "Airborne velocity")
|
_print("Type", "Airborne velocity")
|
||||||
spd, trk, vr, t = adsb.velocity(msg)
|
velocity = adsb.velocity(msg)
|
||||||
|
if velocity is not None:
|
||||||
|
spd, trk, vr, t = velocity
|
||||||
types = {"GS": "Ground speed", "TAS": "True airspeed"}
|
types = {"GS": "Ground speed", "TAS": "True airspeed"}
|
||||||
_print("Speed", spd, "knots")
|
_print("Speed", spd, "knots")
|
||||||
_print("Track", trk, "degrees")
|
_print("Track", trk, "degrees")
|
||||||
@ -71,6 +78,94 @@ def tell(msg: str) -> None:
|
|||||||
_print("CPR Longitude", cprlon)
|
_print("CPR Longitude", cprlon)
|
||||||
_print("Altitude", alt, "feet")
|
_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_29 = {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)
|
||||||
|
if vertical_mode is not None:
|
||||||
|
_print(
|
||||||
|
"Vertical mode",
|
||||||
|
vertical_horizontal_types[vertical_mode],
|
||||||
|
)
|
||||||
|
if horizontal_mode is not None:
|
||||||
|
_print(
|
||||||
|
"Horizontal mode",
|
||||||
|
vertical_horizontal_types[horizontal_mode],
|
||||||
|
)
|
||||||
|
_print(
|
||||||
|
"TCAS/ACAS",
|
||||||
|
tcas_operational_types[tcas_operational]
|
||||||
|
if tcas_operational
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
_print("TCAS/ACAS RA", tcas_ra_types[tcas_ra])
|
||||||
|
_print("Emergency status", emergency_types[emergency_status])
|
||||||
|
else:
|
||||||
|
alt, alt_source = adsb.selected_altitude(msg) # type: ignore
|
||||||
|
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,
|
||||||
|
"" if baro is None else "millibars",
|
||||||
|
)
|
||||||
|
_print("Selected Heading", hdg, "°")
|
||||||
|
if not (common.bin2int((common.hex2bin(msg)[32:])[46]) == 0):
|
||||||
|
_print(
|
||||||
|
"Autopilot", types_29[autopilot] if autopilot else None
|
||||||
|
)
|
||||||
|
_print("VNAV mode", types_29[vnav] if vnav else None)
|
||||||
|
_print(
|
||||||
|
"Altitude hold mode",
|
||||||
|
types_29[alt_hold] if alt_hold else None,
|
||||||
|
)
|
||||||
|
_print("Approach mode", types_29[app] if app else None)
|
||||||
|
_print(
|
||||||
|
"TCAS/ACAS",
|
||||||
|
tcas_operational_types[tcas_operational]
|
||||||
|
if tcas_operational
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
_print("LNAV mode", types_29[lnav] if lnav else None)
|
||||||
|
|
||||||
if df == 20:
|
if df == 20:
|
||||||
_print("Protocol", "Mode-S Comm-B altitude reply")
|
_print("Protocol", "Mode-S Comm-B altitude reply")
|
||||||
_print("Altitude", common.altcode(msg), "feet")
|
_print("Altitude", common.altcode(msg), "feet")
|
||||||
@ -94,7 +189,7 @@ def tell(msg: str) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
BDS = bds.infer(msg, mrar=True)
|
BDS = bds.infer(msg, mrar=True)
|
||||||
if BDS in labels.keys():
|
if BDS is not None and BDS in labels.keys():
|
||||||
_print("BDS", "%s (%s)" % (BDS, labels[BDS]))
|
_print("BDS", "%s (%s)" % (BDS, labels[BDS]))
|
||||||
else:
|
else:
|
||||||
_print("BDS", BDS)
|
_print("BDS", BDS)
|
||||||
|
@ -2,52 +2,120 @@
|
|||||||
|
|
||||||
The ADS-B module also imports functions from the following modules:
|
The ADS-B module also imports functions from the following modules:
|
||||||
|
|
||||||
- pyModeS.decoder.bds.bds05: ``airborne_position()``, ``airborne_position_with_ref()``, ``altitude()``
|
- bds05: ``airborne_position()``, ``airborne_position_with_ref()``,
|
||||||
- pyModeS.decoder.bds.bds06: ``surface_position()``, ``surface_position_with_ref()``, ``surface_velocity()``
|
``altitude()``
|
||||||
- pyModeS.decoder.bds.bds08: ``category()``, ``callsign()``
|
- bds06: ``surface_position()``, ``surface_position_with_ref()``,
|
||||||
- pyModeS.decoder.bds.bds09: ``airborne_velocity()``, ``altitude_diff()``
|
``surface_velocity()``
|
||||||
|
- bds08: ``category()``, ``callsign()``
|
||||||
|
- bds09: ``airborne_velocity()``, ``altitude_diff()``
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pyModeS as pms
|
from __future__ import annotations
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pyModeS import common
|
from .. import common
|
||||||
|
from . import uncertainty
|
||||||
from pyModeS.decoder import uncertainty
|
from .bds.bds05 import airborne_position, airborne_position_with_ref
|
||||||
|
from .bds.bds05 import altitude as altitude05
|
||||||
# from pyModeS.decoder.bds import bds05, bds06, bds09
|
from .bds.bds06 import (
|
||||||
from pyModeS.decoder.bds.bds05 import (
|
|
||||||
airborne_position,
|
|
||||||
airborne_position_with_ref,
|
|
||||||
altitude as altitude05,
|
|
||||||
)
|
|
||||||
from pyModeS.decoder.bds.bds06 import (
|
|
||||||
surface_position,
|
surface_position,
|
||||||
surface_position_with_ref,
|
surface_position_with_ref,
|
||||||
surface_velocity,
|
surface_velocity,
|
||||||
)
|
)
|
||||||
from pyModeS.decoder.bds.bds08 import category, callsign
|
from .bds.bds08 import callsign, category
|
||||||
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
|
from .bds.bds09 import airborne_velocity, altitude_diff
|
||||||
from pyModeS.decoder.bds.bds61_st1 import (
|
from .bds.bds61_st1 import emergency_squawk, emergency_state, is_emergency
|
||||||
is_emergency,
|
from .bds.bds62 import (
|
||||||
emergency_state,
|
altitude_hold_mode,
|
||||||
emergency_squawk,
|
approach_mode,
|
||||||
|
autopilot,
|
||||||
|
baro_pressure_setting,
|
||||||
|
emergency_status,
|
||||||
|
horizontal_mode,
|
||||||
|
lnav_mode,
|
||||||
|
selected_altitude,
|
||||||
|
selected_heading,
|
||||||
|
target_altitude,
|
||||||
|
target_angle,
|
||||||
|
tcas_operational,
|
||||||
|
tcas_ra,
|
||||||
|
vertical_mode,
|
||||||
|
vnav_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"airborne_position",
|
||||||
|
"airborne_position_with_ref",
|
||||||
|
"altitude05",
|
||||||
|
"surface_position",
|
||||||
|
"surface_position_with_ref",
|
||||||
|
"surface_velocity",
|
||||||
|
"callsign",
|
||||||
|
"category",
|
||||||
|
"airborne_velocity",
|
||||||
|
"altitude_diff",
|
||||||
|
"emergency_squawk",
|
||||||
|
"emergency_state",
|
||||||
|
"is_emergency",
|
||||||
|
"df",
|
||||||
|
"icao",
|
||||||
|
"typecode",
|
||||||
|
"position",
|
||||||
|
"position_with_ref",
|
||||||
|
"altitude",
|
||||||
|
"velocity",
|
||||||
|
"speed_heading",
|
||||||
|
"oe_flag",
|
||||||
|
"version",
|
||||||
|
"nuc_p",
|
||||||
|
"nuc_v",
|
||||||
|
"nic_v1",
|
||||||
|
"nic_v2",
|
||||||
|
"nic_s",
|
||||||
|
"nic_a_c",
|
||||||
|
"nic_b",
|
||||||
|
"nac_p",
|
||||||
|
"nac_v",
|
||||||
|
"sil",
|
||||||
|
"selected_altitude",
|
||||||
|
"target_altitude",
|
||||||
|
"vertical_mode",
|
||||||
|
"horizontal_mode",
|
||||||
|
"selected_heading",
|
||||||
|
"target_angle",
|
||||||
|
"baro_pressure_setting",
|
||||||
|
"autopilot",
|
||||||
|
"vnav_mode",
|
||||||
|
"altitude_hold_mode",
|
||||||
|
"approach_mode",
|
||||||
|
"lnav_mode",
|
||||||
|
"tcas_operational",
|
||||||
|
"tcas_ra",
|
||||||
|
"emergency_status",
|
||||||
|
]
|
||||||
|
|
||||||
def df(msg):
|
|
||||||
|
def df(msg: str) -> int:
|
||||||
return common.df(msg)
|
return common.df(msg)
|
||||||
|
|
||||||
|
|
||||||
def icao(msg):
|
def icao(msg: str) -> None | str:
|
||||||
return common.icao(msg)
|
return common.icao(msg)
|
||||||
|
|
||||||
|
|
||||||
def typecode(msg):
|
def typecode(msg: str) -> None | int:
|
||||||
return common.typecode(msg)
|
return common.typecode(msg)
|
||||||
|
|
||||||
|
|
||||||
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
def position(
|
||||||
|
msg0: str,
|
||||||
|
msg1: str,
|
||||||
|
t0: int | datetime,
|
||||||
|
t1: int | datetime,
|
||||||
|
lat_ref: None | float = None,
|
||||||
|
lon_ref: None | float = None,
|
||||||
|
) -> None | tuple[float, float]:
|
||||||
"""Decode surface or airborne position from a pair of even and odd
|
"""Decode surface or airborne position from a pair of even and odd
|
||||||
position messages.
|
position messages.
|
||||||
|
|
||||||
@ -69,6 +137,9 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
|||||||
tc0 = typecode(msg0)
|
tc0 = typecode(msg0)
|
||||||
tc1 = typecode(msg1)
|
tc1 = typecode(msg1)
|
||||||
|
|
||||||
|
if tc0 is None or tc1 is None:
|
||||||
|
raise RuntimeError("Incorrect or inconsistent message types")
|
||||||
|
|
||||||
if 5 <= tc0 <= 8 and 5 <= tc1 <= 8:
|
if 5 <= tc0 <= 8 and 5 <= tc1 <= 8:
|
||||||
if lat_ref is None or lon_ref is None:
|
if lat_ref is None or lon_ref is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@ -90,13 +161,13 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
|||||||
raise RuntimeError("Incorrect or inconsistent message types")
|
raise RuntimeError("Incorrect or inconsistent message types")
|
||||||
|
|
||||||
|
|
||||||
def position_with_ref(msg, lat_ref, lon_ref):
|
def position_with_ref(msg: str, lat_ref: float, lon_ref: float) -> tuple[float, float]:
|
||||||
"""Decode position with only one message.
|
"""Decode position with only one message.
|
||||||
|
|
||||||
A reference position is required, which can be previously
|
A reference position is required, which can be previously
|
||||||
calculated location, ground station, or airport location.
|
calculated location, ground station, or airport location.
|
||||||
The function works with both airborne and surface position messages.
|
The function works with both airborne and surface position messages.
|
||||||
The reference position shall be with in 180NM (airborne) or 45NM (surface)
|
The reference position shall be within 180NM (airborne) or 45NM (surface)
|
||||||
of the true position.
|
of the true position.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -110,6 +181,9 @@ def position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
|
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
|
||||||
|
if tc is None:
|
||||||
|
raise RuntimeError("incorrect or inconsistent message types")
|
||||||
|
|
||||||
if 5 <= tc <= 8:
|
if 5 <= tc <= 8:
|
||||||
return surface_position_with_ref(msg, lat_ref, lon_ref)
|
return surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||||
|
|
||||||
@ -120,7 +194,7 @@ def position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
raise RuntimeError("incorrect or inconsistent message types")
|
raise RuntimeError("incorrect or inconsistent message types")
|
||||||
|
|
||||||
|
|
||||||
def altitude(msg):
|
def altitude(msg: str) -> None | float:
|
||||||
"""Decode aircraft altitude.
|
"""Decode aircraft altitude.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -132,7 +206,7 @@ def altitude(msg):
|
|||||||
"""
|
"""
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
|
||||||
if tc < 5 or tc == 19 or tc > 22:
|
if tc is None or tc < 5 or tc == 19 or tc > 22:
|
||||||
raise RuntimeError("%s: Not a position message" % msg)
|
raise RuntimeError("%s: Not a position message" % msg)
|
||||||
|
|
||||||
elif tc >= 5 and tc <= 8:
|
elif tc >= 5 and tc <= 8:
|
||||||
@ -144,39 +218,47 @@ def altitude(msg):
|
|||||||
return altitude05(msg)
|
return altitude05(msg)
|
||||||
|
|
||||||
|
|
||||||
def velocity(msg, source=False):
|
def velocity(
|
||||||
"""Calculate the speed, heading, and vertical rate (handles both airborne or surface message).
|
msg: str, source: bool = False
|
||||||
|
) -> None | tuple[None | float, None | float, None | int, str]:
|
||||||
|
"""Calculate the speed, heading, and vertical rate
|
||||||
|
(handles both airborne or surface message).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
source (boolean): Include direction and vertical rate sources in return. Default to False.
|
source (boolean): Include direction and vertical rate sources in return.
|
||||||
|
Default to False.
|
||||||
If set to True, the function will return six value instead of four.
|
If set to True, the function will return six value instead of four.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int, float, int, string, [string], [string]: Four or six parameters, including:
|
int, float, int, string, [string], [string]:
|
||||||
- Speed (kt)
|
- Speed (kt)
|
||||||
- Angle (degree), either ground track or heading
|
- Angle (degree), either ground track or heading
|
||||||
- Vertical rate (ft/min)
|
- Vertical rate (ft/min)
|
||||||
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
||||||
- [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
|
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
|
||||||
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
||||||
|
|
||||||
For surface messages, vertical rate and its respective sources are set to None.
|
For surface messages, vertical rate and its respective sources are set
|
||||||
|
to None.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if 5 <= typecode(msg) <= 8:
|
tc = typecode(msg)
|
||||||
|
error = "incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
|
||||||
|
if tc is None:
|
||||||
|
raise RuntimeError(error)
|
||||||
|
|
||||||
|
if 5 <= tc <= 8:
|
||||||
return surface_velocity(msg, source)
|
return surface_velocity(msg, source)
|
||||||
|
|
||||||
elif typecode(msg) == 19:
|
elif tc == 19:
|
||||||
return airborne_velocity(msg, source)
|
return airborne_velocity(msg, source)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(error)
|
||||||
"incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def speed_heading(msg):
|
def speed_heading(msg: str) -> None | tuple[None | float, None | float]:
|
||||||
"""Get speed and ground track (or heading) from the velocity message
|
"""Get speed and ground track (or heading) from the velocity message
|
||||||
(handles both airborne or surface message)
|
(handles both airborne or surface message)
|
||||||
|
|
||||||
@ -186,11 +268,14 @@ def speed_heading(msg):
|
|||||||
Returns:
|
Returns:
|
||||||
(int, float): speed (kt), ground track or heading (degree)
|
(int, float): speed (kt), ground track or heading (degree)
|
||||||
"""
|
"""
|
||||||
spd, trk_or_hdg, rocd, tag = velocity(msg)
|
decoded = velocity(msg)
|
||||||
|
if decoded is None:
|
||||||
|
return None
|
||||||
|
spd, trk_or_hdg, rocd, tag = decoded
|
||||||
return spd, trk_or_hdg
|
return spd, trk_or_hdg
|
||||||
|
|
||||||
|
|
||||||
def oe_flag(msg):
|
def oe_flag(msg: str) -> int:
|
||||||
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
@ -201,7 +286,7 @@ def oe_flag(msg):
|
|||||||
return int(msgbin[53])
|
return int(msgbin[53])
|
||||||
|
|
||||||
|
|
||||||
def version(msg):
|
def version(msg: str) -> int:
|
||||||
"""ADS-B Version
|
"""ADS-B Version
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -223,13 +308,15 @@ def version(msg):
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
def nuc_p(msg):
|
def nuc_p(msg: str) -> tuple[int, None | float, None | int, None | int]:
|
||||||
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
|
"""Calculate NUCp, Navigation Uncertainty Category - Position
|
||||||
|
(ADS-B version 1)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string,
|
msg (str): 28 hexdigits string,
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
int: NUCp, Navigation Uncertainty Category (position)
|
||||||
int: Horizontal Protection Limit
|
int: Horizontal Protection Limit
|
||||||
int: 95% Containment Radius - Horizontal (meters)
|
int: 95% Containment Radius - Horizontal (meters)
|
||||||
int: 95% Containment Radius - Vertical (meters)
|
int: 95% Containment Radius - Vertical (meters)
|
||||||
@ -237,7 +324,7 @@ def nuc_p(msg):
|
|||||||
"""
|
"""
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
|
||||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
if tc is None or tc < 5 or tc is None or tc > 22:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s: Not a surface position message (5<TC<8), \
|
"%s: Not a surface position message (5<TC<8), \
|
||||||
airborne position message (8<TC<19), \
|
airborne position message (8<TC<19), \
|
||||||
@ -245,27 +332,36 @@ def nuc_p(msg):
|
|||||||
% msg
|
% msg
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
NUCp = uncertainty.TC_NUCp_lookup[tc]
|
NUCp = uncertainty.TC_NUCp_lookup[tc]
|
||||||
HPL = uncertainty.NUCp[NUCp]["HPL"]
|
index = uncertainty.NUCp.get(NUCp, None)
|
||||||
RCu = uncertainty.NUCp[NUCp]["RCu"]
|
|
||||||
RCv = uncertainty.NUCp[NUCp]["RCv"]
|
if index is not None:
|
||||||
except KeyError:
|
HPL = index["HPL"]
|
||||||
|
RCu = index["RCu"]
|
||||||
|
RCv = index["RCv"]
|
||||||
|
else:
|
||||||
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
|
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
|
||||||
|
|
||||||
if tc in [20, 21]:
|
|
||||||
RCv = uncertainty.NA
|
RCv = uncertainty.NA
|
||||||
|
|
||||||
return HPL, RCu, RCv
|
# RCv only available for GNSS height
|
||||||
|
if tc == 20:
|
||||||
|
RCv = 4
|
||||||
|
elif tc == 21:
|
||||||
|
RCv = 15
|
||||||
|
|
||||||
|
return NUCp, HPL, RCu, RCv
|
||||||
|
|
||||||
|
|
||||||
def nuc_v(msg):
|
def nuc_v(msg: str) -> tuple[int, None | float, None | float]:
|
||||||
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
|
"""Calculate NUCv, Navigation Uncertainty Category - Velocity
|
||||||
|
(ADS-B version 1)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string,
|
msg (str): 28 hexdigits string,
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
int: NUCv, Navigation Uncertainty Category (velocity)
|
||||||
int or string: 95% Horizontal Velocity Error
|
int or string: 95% Horizontal Velocity Error
|
||||||
int or string: 95% Vertical Velocity Error
|
int or string: 95% Vertical Velocity Error
|
||||||
"""
|
"""
|
||||||
@ -278,17 +374,18 @@ def nuc_v(msg):
|
|||||||
|
|
||||||
msgbin = common.hex2bin(msg)
|
msgbin = common.hex2bin(msg)
|
||||||
NUCv = common.bin2int(msgbin[42:45])
|
NUCv = common.bin2int(msgbin[42:45])
|
||||||
|
index = uncertainty.NUCv.get(NUCv, None)
|
||||||
|
|
||||||
try:
|
if index is not None:
|
||||||
HVE = uncertainty.NUCv[NUCv]["HVE"]
|
HVE = index["HVE"]
|
||||||
VVE = uncertainty.NUCv[NUCv]["VVE"]
|
VVE = index["VVE"]
|
||||||
except KeyError:
|
else:
|
||||||
HVE, VVE = uncertainty.NA, uncertainty.NA
|
HVE, VVE = uncertainty.NA, uncertainty.NA
|
||||||
|
|
||||||
return HVE, VVE
|
return NUCv, HVE, VVE
|
||||||
|
|
||||||
|
|
||||||
def nic_v1(msg, NICs):
|
def nic_v1(msg: str, NICs: int) -> tuple[int, None | float, None | float]:
|
||||||
"""Calculate NIC, navigation integrity category, for ADS-B version 1
|
"""Calculate NIC, navigation integrity category, for ADS-B version 1
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -296,10 +393,12 @@ def nic_v1(msg, NICs):
|
|||||||
NICs (int or string): NIC supplement
|
NICs (int or string): NIC supplement
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
int: NIC, Navigation Integrity Category
|
||||||
int or string: Horizontal Radius of Containment
|
int or string: Horizontal Radius of Containment
|
||||||
int or string: Vertical Protection Limit
|
int or string: Vertical Protection Limit
|
||||||
"""
|
"""
|
||||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
tc = typecode(msg)
|
||||||
|
if tc is None or tc < 5 or tc > 22:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s: Not a surface position message (5<TC<8), \
|
"%s: Not a surface position message (5<TC<8), \
|
||||||
airborne position message (8<TC<19), \
|
airborne position message (8<TC<19), \
|
||||||
@ -307,33 +406,37 @@ def nic_v1(msg, NICs):
|
|||||||
% msg
|
% msg
|
||||||
)
|
)
|
||||||
|
|
||||||
tc = typecode(msg)
|
|
||||||
NIC = uncertainty.TC_NICv1_lookup[tc]
|
NIC = uncertainty.TC_NICv1_lookup[tc]
|
||||||
|
|
||||||
if isinstance(NIC, dict):
|
if isinstance(NIC, dict):
|
||||||
NIC = NIC[NICs]
|
NIC = NIC[NICs]
|
||||||
|
|
||||||
try:
|
d_index = uncertainty.NICv1.get(NIC, None)
|
||||||
Rc = uncertainty.NICv1[NIC][NICs]["Rc"]
|
|
||||||
VPL = uncertainty.NICv1[NIC][NICs]["VPL"]
|
|
||||||
except KeyError:
|
|
||||||
Rc, VPL = uncertainty.NA, uncertainty.NA
|
Rc, VPL = uncertainty.NA, uncertainty.NA
|
||||||
|
|
||||||
return Rc, VPL
|
if d_index is not None:
|
||||||
|
index = d_index.get(NICs, None)
|
||||||
|
if index is not None:
|
||||||
|
Rc = index["Rc"]
|
||||||
|
VPL = index["VPL"]
|
||||||
|
|
||||||
|
return NIC, Rc, VPL
|
||||||
|
|
||||||
|
|
||||||
def nic_v2(msg, NICa, NICbc):
|
def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int, int]:
|
||||||
"""Calculate NIC, navigation integrity category, for ADS-B version 2
|
"""Calculate NIC, navigation integrity category, for ADS-B version 2
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
NICa (int or string): NIC supplement - A
|
NICa (int or string): NIC supplement - A
|
||||||
NICbc (int or srting): NIC supplement - B or C
|
NICbc (int or string): NIC supplement - B or C
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
int: NIC, Navigation Integrity Category
|
||||||
int or string: Horizontal Radius of Containment
|
int or string: Horizontal Radius of Containment
|
||||||
"""
|
"""
|
||||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
tc = typecode(msg)
|
||||||
|
if tc is None or tc < 5 or tc > 22:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s: Not a surface position message (5<TC<8), \
|
"%s: Not a surface position message (5<TC<8), \
|
||||||
airborne position message (8<TC<19), \
|
airborne position message (8<TC<19), \
|
||||||
@ -341,7 +444,6 @@ def nic_v2(msg, NICa, NICbc):
|
|||||||
% msg
|
% msg
|
||||||
)
|
)
|
||||||
|
|
||||||
tc = typecode(msg)
|
|
||||||
NIC = uncertainty.TC_NICv2_lookup[tc]
|
NIC = uncertainty.TC_NICv2_lookup[tc]
|
||||||
|
|
||||||
if 20 <= tc <= 22:
|
if 20 <= tc <= 22:
|
||||||
@ -357,10 +459,10 @@ def nic_v2(msg, NICa, NICbc):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
Rc = uncertainty.NA
|
Rc = uncertainty.NA
|
||||||
|
|
||||||
return Rc
|
return NIC, Rc # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def nic_s(msg):
|
def nic_s(msg: str) -> int:
|
||||||
"""Obtain NIC supplement bit, TC=31 message
|
"""Obtain NIC supplement bit, TC=31 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -382,7 +484,7 @@ def nic_s(msg):
|
|||||||
return nic_s
|
return nic_s
|
||||||
|
|
||||||
|
|
||||||
def nic_a_c(msg):
|
def nic_a_c(msg: str) -> tuple[int, int]:
|
||||||
"""Obtain NICa/c, navigation integrity category supplements a and c
|
"""Obtain NICa/c, navigation integrity category supplements a and c
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -405,7 +507,7 @@ def nic_a_c(msg):
|
|||||||
return nic_a, nic_c
|
return nic_a, nic_c
|
||||||
|
|
||||||
|
|
||||||
def nic_b(msg):
|
def nic_b(msg: str) -> int:
|
||||||
"""Obtain NICb, navigation integrity category supplement-b
|
"""Obtain NICb, navigation integrity category supplement-b
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -416,7 +518,7 @@ def nic_b(msg):
|
|||||||
"""
|
"""
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
|
||||||
if tc < 9 or tc > 18:
|
if tc is None or tc < 9 or tc > 18:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s: Not a airborne position message, expecting 8<TC<19" % msg
|
"%s: Not a airborne position message, expecting 8<TC<19" % msg
|
||||||
)
|
)
|
||||||
@ -427,15 +529,18 @@ def nic_b(msg):
|
|||||||
return nic_b
|
return nic_b
|
||||||
|
|
||||||
|
|
||||||
def nac_p(msg):
|
def nac_p(msg: str) -> tuple[int, int | None, int | None]:
|
||||||
"""Calculate NACp, Navigation Accuracy Category - Position
|
"""Calculate NACp, Navigation Accuracy Category - Position
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string, TC = 29 or 31
|
msg (str): 28 hexdigits string, TC = 29 or 31
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
|
int: NACp, Navigation Accuracy Category (position)
|
||||||
int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
|
int or string: 95% horizontal accuracy bounds,
|
||||||
|
Estimated Position Uncertainty
|
||||||
|
int or string: 95% vertical accuracy bounds,
|
||||||
|
Vertical Estimated Position Uncertainty
|
||||||
"""
|
"""
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
|
||||||
@ -459,18 +564,21 @@ def nac_p(msg):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
EPU, VEPU = uncertainty.NA, uncertainty.NA
|
EPU, VEPU = uncertainty.NA, uncertainty.NA
|
||||||
|
|
||||||
return EPU, VEPU
|
return NACp, EPU, VEPU
|
||||||
|
|
||||||
|
|
||||||
def nac_v(msg):
|
def nac_v(msg: str) -> tuple[int, float | None, float | None]:
|
||||||
"""Calculate NACv, Navigation Accuracy Category - Velocity
|
"""Calculate NACv, Navigation Accuracy Category - Velocity
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string, TC = 19
|
msg (str): 28 hexdigits string, TC = 19
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
|
int: NACv, Navigation Accuracy Category (velocity)
|
||||||
int or string: 95% vertical accuracy bounds for velocity, Vertical Figure of Merit
|
int or string: 95% horizontal accuracy bounds for velocity,
|
||||||
|
Horizontal Figure of Merit
|
||||||
|
int or string: 95% vertical accuracy bounds for velocity,
|
||||||
|
Vertical Figure of Merit
|
||||||
"""
|
"""
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
|
||||||
@ -488,18 +596,23 @@ def nac_v(msg):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
|
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
|
||||||
|
|
||||||
return HFOMr, VFOMr
|
return NACv, HFOMr, VFOMr
|
||||||
|
|
||||||
|
|
||||||
def sil(msg, version):
|
def sil(
|
||||||
|
msg: str,
|
||||||
|
version: None | int,
|
||||||
|
) -> tuple[float | None, float | None, str]:
|
||||||
"""Calculate SIL, Surveillance Integrity Level
|
"""Calculate SIL, Surveillance Integrity Level
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string with TC = 29, 31
|
msg (str): 28 hexdigits string with TC = 29, 31
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int or string: Probability of exceeding Horizontal Radius of Containment RCu
|
int or string:
|
||||||
int or string: Probability of exceeding Vertical Integrity Containment Region VPL
|
Probability of exceeding Horizontal Radius of Containment RCu
|
||||||
|
int or string:
|
||||||
|
Probability of exceeding Vertical Integrity Containment Region VPL
|
||||||
string: SIL supplement based on per "hour" or "sample", or 'unknown'
|
string: SIL supplement based on per "hour" or "sample", or 'unknown'
|
||||||
"""
|
"""
|
||||||
tc = typecode(msg)
|
tc = typecode(msg)
|
||||||
|
@ -1,5 +1,98 @@
|
|||||||
"""
|
"""
|
||||||
Decoding all call replies DF=11
|
Decode all-call reply messages, with downlink format 11
|
||||||
|
"""
|
||||||
|
|
||||||
[To be implemented]
|
|
||||||
"""
|
from __future__ import annotations
|
||||||
|
from typing import Callable, TypeVar
|
||||||
|
|
||||||
|
from .. import common
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
F = Callable[[str], T]
|
||||||
|
|
||||||
|
|
||||||
|
def _checkdf(func: F[T]) -> F[T]:
|
||||||
|
|
||||||
|
"""Ensure downlink format is 11."""
|
||||||
|
|
||||||
|
def wrapper(msg: str) -> T:
|
||||||
|
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: str) -> None | str:
|
||||||
|
"""Decode transponder code (ICAO address).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (str): 14 hexdigits string
|
||||||
|
Returns:
|
||||||
|
string: ICAO address
|
||||||
|
|
||||||
|
"""
|
||||||
|
return common.icao(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@_checkdf
|
||||||
|
def interrogator(msg: str) -> str:
|
||||||
|
"""Decode interrogator identifier code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (str): 14 hexdigits string
|
||||||
|
Returns:
|
||||||
|
int: interrogator identifier code
|
||||||
|
|
||||||
|
"""
|
||||||
|
# the CRC remainder contains the CL and IC field.
|
||||||
|
# the 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: str) -> tuple[int, None | str]:
|
||||||
|
"""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
|
||||||
|
@ -18,16 +18,13 @@
|
|||||||
Common functions for Mode-S decoding
|
Common functions for Mode-S decoding
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from pyModeS.extra import aero
|
from ... import common
|
||||||
from pyModeS import common
|
from ...extra import aero
|
||||||
|
from . import ( # noqa: F401
|
||||||
from pyModeS.decoder.bds import (
|
|
||||||
bds05,
|
|
||||||
bds06,
|
|
||||||
bds08,
|
|
||||||
bds09,
|
|
||||||
bds10,
|
bds10,
|
||||||
bds17,
|
bds17,
|
||||||
bds20,
|
bds20,
|
||||||
@ -36,12 +33,13 @@ from pyModeS.decoder.bds import (
|
|||||||
bds44,
|
bds44,
|
||||||
bds45,
|
bds45,
|
||||||
bds50,
|
bds50,
|
||||||
bds53,
|
|
||||||
bds60,
|
bds60,
|
||||||
|
bds61_st1,
|
||||||
|
bds62,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
def is50or60(msg: str, spd_ref: float, trk_ref: float, alt_ref: float) -> Optional[str]:
|
||||||
"""Use reference ground speed and trk to determine BDS50 and DBS60.
|
"""Use reference ground speed and trk to determine BDS50 and DBS60.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -51,7 +49,8 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
|||||||
alt_ref (float): reference altitude (ADS-B altitude), ft
|
alt_ref (float): reference altitude (ADS-B altitude), ft
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
String or None: BDS version, or possible versions, or None if nothing matches.
|
String or None: BDS version, or possible versions,
|
||||||
|
or None if nothing matches.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -60,25 +59,34 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
|||||||
vy = v * np.cos(np.radians(angle))
|
vy = v * np.cos(np.radians(angle))
|
||||||
return vx, vy
|
return vx, vy
|
||||||
|
|
||||||
|
# message must be both BDS 50 and 60 before processing
|
||||||
if not (bds50.is50(msg) and bds60.is60(msg)):
|
if not (bds50.is50(msg) and bds60.is60(msg)):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
h50 = bds50.trk50(msg)
|
# --- assuming BDS60 ---
|
||||||
v50 = bds50.gs50(msg)
|
|
||||||
|
|
||||||
if h50 is None or v50 is None:
|
|
||||||
return "BDS50,BDS60"
|
|
||||||
|
|
||||||
h60 = bds60.hdg60(msg)
|
h60 = bds60.hdg60(msg)
|
||||||
m60 = bds60.mach60(msg)
|
m60 = bds60.mach60(msg)
|
||||||
i60 = bds60.ias60(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):
|
if h60 is None or (m60 is None and i60 is None):
|
||||||
return "BDS50,BDS60"
|
return "BDS50,BDS60"
|
||||||
|
|
||||||
m60 = np.nan if m60 is None else m60
|
m60 = np.nan if m60 is None else m60
|
||||||
i60 = np.nan if i60 is None else i60
|
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)
|
XY5 = vxy(v50 * aero.kts, h50)
|
||||||
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
|
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
|
||||||
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
|
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
|
||||||
@ -104,15 +112,17 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
|||||||
return BDS
|
return BDS
|
||||||
|
|
||||||
|
|
||||||
def infer(msg, mrar=False):
|
def infer(msg: str, mrar: bool = False) -> Optional[str]:
|
||||||
"""Estimate the most likely BDS code of an message.
|
"""Estimate the most likely BDS code of an message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45). Defaults to False.
|
mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45).
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
String or None: BDS version, or possible versions, or None if nothing matches.
|
String or None: BDS version, or possible versions,
|
||||||
|
or None if nothing matches.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
df = common.df(msg)
|
df = common.df(msg)
|
||||||
@ -123,6 +133,8 @@ def infer(msg, mrar=False):
|
|||||||
# For ADS-B / Mode-S extended squitter
|
# For ADS-B / Mode-S extended squitter
|
||||||
if df == 17:
|
if df == 17:
|
||||||
tc = common.typecode(msg)
|
tc = common.typecode(msg)
|
||||||
|
if tc is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if 1 <= tc <= 4:
|
if 1 <= tc <= 4:
|
||||||
return "BDS08" # identification and category
|
return "BDS08" # identification and category
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# BDS 0,5
|
# BDS 0,5
|
||||||
# ADS-B TC=9-18
|
# ADS-B TC=9-18
|
||||||
# Airborn position
|
# Airborne position
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def airborne_position(msg0, msg1, t0, t1):
|
def airborne_position(
|
||||||
"""Decode airborn position from a pair of even and odd position message
|
msg0: str, msg1: str, t0: int | datetime, t1: int | datetime
|
||||||
|
) -> None | tuple[float, float]:
|
||||||
|
"""Decode airborne position from a pair of even and odd position message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg0 (string): even message (28 hexdigits)
|
msg0 (string): even message (28 hexdigits)
|
||||||
@ -34,13 +40,13 @@ def airborne_position(msg0, msg1, t0, t1):
|
|||||||
raise RuntimeError("Both even and odd CPR frames are required.")
|
raise RuntimeError("Both even and odd CPR frames are required.")
|
||||||
|
|
||||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||||
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
|
cprlat_even = common.bin2int(mb0[22:39]) / 131072
|
||||||
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
|
cprlon_even = common.bin2int(mb0[39:56]) / 131072
|
||||||
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
|
cprlat_odd = common.bin2int(mb1[22:39]) / 131072
|
||||||
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
|
cprlon_odd = common.bin2int(mb1[39:56]) / 131072
|
||||||
|
|
||||||
air_d_lat_even = 360.0 / 60
|
air_d_lat_even = 360 / 60
|
||||||
air_d_lat_odd = 360.0 / 59
|
air_d_lat_odd = 360 / 59
|
||||||
|
|
||||||
# compute latitude index 'j'
|
# compute latitude index 'j'
|
||||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||||
@ -59,18 +65,19 @@ def airborne_position(msg0, msg1, t0, t1):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# compute ni, longitude index m, and longitude
|
# compute ni, longitude index m, and longitude
|
||||||
if t0 > t1:
|
# (people pass int+int or datetime+datetime)
|
||||||
|
if t0 > t1: # type: ignore
|
||||||
lat = lat_even
|
lat = lat_even
|
||||||
nl = common.cprNL(lat)
|
nl = common.cprNL(lat)
|
||||||
ni = max(common.cprNL(lat) - 0, 1)
|
ni = max(common.cprNL(lat) - 0, 1)
|
||||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||||
lon = (360.0 / ni) * (m % ni + cprlon_even)
|
lon = (360 / ni) * (m % ni + cprlon_even)
|
||||||
else:
|
else:
|
||||||
lat = lat_odd
|
lat = lat_odd
|
||||||
nl = common.cprNL(lat)
|
nl = common.cprNL(lat)
|
||||||
ni = max(common.cprNL(lat) - 1, 1)
|
ni = max(common.cprNL(lat) - 1, 1)
|
||||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||||
lon = (360.0 / ni) * (m % ni + cprlon_odd)
|
lon = (360 / ni) * (m % ni + cprlon_odd)
|
||||||
|
|
||||||
if lon > 180:
|
if lon > 180:
|
||||||
lon = lon - 360
|
lon = lon - 360
|
||||||
@ -78,11 +85,13 @@ def airborne_position(msg0, msg1, t0, t1):
|
|||||||
return round(lat, 5), round(lon, 5)
|
return round(lat, 5), round(lon, 5)
|
||||||
|
|
||||||
|
|
||||||
def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
def airborne_position_with_ref(
|
||||||
|
msg: str, lat_ref: float, lon_ref: float
|
||||||
|
) -> tuple[float, float]:
|
||||||
"""Decode airborne position with only one message,
|
"""Decode airborne position with only one message,
|
||||||
knowing reference nearby location, such as previously calculated location,
|
knowing reference nearby location, such as previously calculated location,
|
||||||
ground station, or airport location, etc. The reference position shall
|
ground station, or airport location, etc. The reference position shall
|
||||||
be with in 180NM of the true position.
|
be within 180NM of the true position.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): even message (28 hexdigits)
|
msg (str): even message (28 hexdigits)
|
||||||
@ -95,11 +104,11 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
|
|
||||||
mb = common.hex2bin(msg)[32:]
|
mb = common.hex2bin(msg)[32:]
|
||||||
|
|
||||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
cprlat = common.bin2int(mb[22:39]) / 131072
|
||||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
cprlon = common.bin2int(mb[39:56]) / 131072
|
||||||
|
|
||||||
i = int(mb[21])
|
i = int(mb[21])
|
||||||
d_lat = 360.0 / 59 if i else 360.0 / 60
|
d_lat = 360 / 59 if i else 360 / 60
|
||||||
|
|
||||||
j = common.floor(lat_ref / d_lat) + common.floor(
|
j = common.floor(lat_ref / d_lat) + common.floor(
|
||||||
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
||||||
@ -110,9 +119,9 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
ni = common.cprNL(lat) - i
|
ni = common.cprNL(lat) - i
|
||||||
|
|
||||||
if ni > 0:
|
if ni > 0:
|
||||||
d_lon = 360.0 / ni
|
d_lon = 360 / ni
|
||||||
else:
|
else:
|
||||||
d_lon = 360.0
|
d_lon = 360
|
||||||
|
|
||||||
m = common.floor(lon_ref / d_lon) + common.floor(
|
m = common.floor(lon_ref / d_lon) + common.floor(
|
||||||
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
||||||
@ -123,7 +132,7 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
return round(lat, 5), round(lon, 5)
|
return round(lat, 5), round(lon, 5)
|
||||||
|
|
||||||
|
|
||||||
def altitude(msg):
|
def altitude(msg: str) -> None | int:
|
||||||
"""Decode aircraft altitude
|
"""Decode aircraft altitude
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -135,17 +144,14 @@ def altitude(msg):
|
|||||||
|
|
||||||
tc = common.typecode(msg)
|
tc = common.typecode(msg)
|
||||||
|
|
||||||
if tc < 9 or tc == 19 or tc > 22:
|
if tc is None or tc < 9 or tc == 19 or tc > 22:
|
||||||
raise RuntimeError("%s: Not a airborn position message" % msg)
|
raise RuntimeError("%s: Not an airborne position message" % msg)
|
||||||
|
|
||||||
mb = common.hex2bin(msg)[32:]
|
mb = common.hex2bin(msg)[32:]
|
||||||
altbin = mb[8:20]
|
altbin = mb[8:20]
|
||||||
|
|
||||||
if tc < 19:
|
if tc < 19:
|
||||||
altcode = altbin[0:6] + "0" + altbin[6:]
|
altcode = altbin[0:6] + "0" + altbin[6:]
|
||||||
|
return common.altitude(altcode)
|
||||||
else:
|
else:
|
||||||
altcode = altbin[0:6] + "0" + altbin[6:]
|
return common.bin2int(altbin) * 3.28084 # type: ignore
|
||||||
|
|
||||||
alt = common.altitude(altcode)
|
|
||||||
|
|
||||||
return alt
|
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# BDS 0,6
|
# BDS 0,6
|
||||||
# ADS-B TC=5-8
|
# ADS-B TC=5-8
|
||||||
# Surface movment
|
# Surface movement
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
def surface_position(
|
||||||
|
msg0: str,
|
||||||
|
msg1: str,
|
||||||
|
t0: int | datetime,
|
||||||
|
t1: int | datetime,
|
||||||
|
lat_ref: float,
|
||||||
|
lon_ref: float,
|
||||||
|
) -> None | tuple[float, float]:
|
||||||
|
|
||||||
"""Decode surface position from a pair of even and odd position message,
|
"""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.
|
the lat/lon of receiver must be provided to yield the correct solution.
|
||||||
|
|
||||||
@ -27,13 +39,13 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
|||||||
msgbin1 = common.hex2bin(msg1)
|
msgbin1 = common.hex2bin(msg1)
|
||||||
|
|
||||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||||
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
|
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072
|
||||||
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
|
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072
|
||||||
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
|
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072
|
||||||
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
|
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072
|
||||||
|
|
||||||
air_d_lat_even = 90.0 / 60
|
air_d_lat_even = 90 / 60
|
||||||
air_d_lat_odd = 90.0 / 59
|
air_d_lat_odd = 90 / 59
|
||||||
|
|
||||||
# compute latitude index 'j'
|
# compute latitude index 'j'
|
||||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||||
@ -43,8 +55,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
|||||||
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||||
|
|
||||||
# solution for north hemisphere
|
# solution for north hemisphere
|
||||||
lat_even_s = lat_even_n - 90.0
|
lat_even_s = lat_even_n - 90
|
||||||
lat_odd_s = lat_odd_n - 90.0
|
lat_odd_s = lat_odd_n - 90
|
||||||
|
|
||||||
# chose which solution corrispondes to receiver location
|
# chose which solution corrispondes to receiver location
|
||||||
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
|
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
|
||||||
@ -55,38 +67,41 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# compute ni, longitude index m, and longitude
|
# compute ni, longitude index m, and longitude
|
||||||
if t0 > t1:
|
# (people pass int+int or datetime+datetime)
|
||||||
|
if t0 > t1: # type: ignore
|
||||||
lat = lat_even
|
lat = lat_even
|
||||||
nl = common.cprNL(lat_even)
|
nl = common.cprNL(lat_even)
|
||||||
ni = max(common.cprNL(lat_even) - 0, 1)
|
ni = max(common.cprNL(lat_even) - 0, 1)
|
||||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||||
lon = (90.0 / ni) * (m % ni + cprlon_even)
|
lon = (90 / ni) * (m % ni + cprlon_even)
|
||||||
else:
|
else:
|
||||||
lat = lat_odd
|
lat = lat_odd
|
||||||
nl = common.cprNL(lat_odd)
|
nl = common.cprNL(lat_odd)
|
||||||
ni = max(common.cprNL(lat_odd) - 1, 1)
|
ni = max(common.cprNL(lat_odd) - 1, 1)
|
||||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||||
lon = (90.0 / ni) * (m % ni + cprlon_odd)
|
lon = (90 / ni) * (m % ni + cprlon_odd)
|
||||||
|
|
||||||
# four possible longitude solutions
|
# four possible longitude solutions
|
||||||
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
|
lons = [lon, lon + 90, lon + 180, lon + 270]
|
||||||
|
|
||||||
# make sure lons are between -180 and 180
|
# make sure lons are between -180 and 180
|
||||||
lons = [(l + 180) % 360 - 180 for l in lons]
|
lons = [(lon + 180) % 360 - 180 for lon in lons]
|
||||||
|
|
||||||
# the closest solution to receiver is the correct one
|
# the closest solution to receiver is the correct one
|
||||||
dls = [abs(lon_ref - l) for l in lons]
|
dls = [abs(lon_ref - lon) for lon in lons]
|
||||||
imin = min(range(4), key=dls.__getitem__)
|
imin = min(range(4), key=dls.__getitem__)
|
||||||
lon = lons[imin]
|
lon = lons[imin]
|
||||||
|
|
||||||
return round(lat, 5), round(lon, 5)
|
return round(lat, 5), round(lon, 5)
|
||||||
|
|
||||||
|
|
||||||
def surface_position_with_ref(msg, lat_ref, lon_ref):
|
def surface_position_with_ref(
|
||||||
|
msg: str, lat_ref: float, lon_ref: float
|
||||||
|
) -> tuple[float, float]:
|
||||||
"""Decode surface position with only one message,
|
"""Decode surface position with only one message,
|
||||||
knowing reference nearby location, such as previously calculated location,
|
knowing reference nearby location, such as previously calculated location,
|
||||||
ground station, or airport location, etc. The reference position shall
|
ground station, or airport location, etc. The reference position shall
|
||||||
be with in 45NM of the true position.
|
be within 45NM of the true position.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): even message (28 hexdigits)
|
msg (str): even message (28 hexdigits)
|
||||||
@ -99,11 +114,11 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
|
|
||||||
mb = common.hex2bin(msg)[32:]
|
mb = common.hex2bin(msg)[32:]
|
||||||
|
|
||||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
cprlat = common.bin2int(mb[22:39]) / 131072
|
||||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
cprlon = common.bin2int(mb[39:56]) / 131072
|
||||||
|
|
||||||
i = int(mb[21])
|
i = int(mb[21])
|
||||||
d_lat = 90.0 / 59 if i else 90.0 / 60
|
d_lat = 90 / 59 if i else 90 / 60
|
||||||
|
|
||||||
j = common.floor(lat_ref / d_lat) + common.floor(
|
j = common.floor(lat_ref / d_lat) + common.floor(
|
||||||
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
||||||
@ -114,9 +129,9 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
ni = common.cprNL(lat) - i
|
ni = common.cprNL(lat) - i
|
||||||
|
|
||||||
if ni > 0:
|
if ni > 0:
|
||||||
d_lon = 90.0 / ni
|
d_lon = 90 / ni
|
||||||
else:
|
else:
|
||||||
d_lon = 90.0
|
d_lon = 90
|
||||||
|
|
||||||
m = common.floor(lon_ref / d_lon) + common.floor(
|
m = common.floor(lon_ref / d_lon) + common.floor(
|
||||||
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
||||||
@ -127,16 +142,19 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
|||||||
return round(lat, 5), round(lon, 5)
|
return round(lat, 5), round(lon, 5)
|
||||||
|
|
||||||
|
|
||||||
def surface_velocity(msg, source=False):
|
def surface_velocity(
|
||||||
|
msg: str, source: bool = False
|
||||||
|
) -> tuple[None | float, float, int, str]:
|
||||||
"""Decode surface velocity from a surface position message
|
"""Decode surface velocity from a surface position message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
source (boolean): Include direction and vertical rate sources in return. Default to False.
|
source (boolean): Include direction and vertical rate sources in return.
|
||||||
|
Default to False.
|
||||||
If set to True, the function will return six value instead of four.
|
If set to True, the function will return six value instead of four.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int, float, int, string, [string], [string]: Four or six parameters, including:
|
int, float, int, string, [string], [string]:
|
||||||
- Speed (kt)
|
- Speed (kt)
|
||||||
- Angle (degree), ground track
|
- Angle (degree), ground track
|
||||||
- Vertical rate, always 0
|
- Vertical rate, always 0
|
||||||
@ -145,7 +163,8 @@ def surface_velocity(msg, source=False):
|
|||||||
- [Optional] Vertical rate source (None)
|
- [Optional] Vertical rate source (None)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if common.typecode(msg) < 5 or common.typecode(msg) > 8:
|
tc = common.typecode(msg)
|
||||||
|
if tc is None or tc < 5 or tc > 8:
|
||||||
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
|
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
|
||||||
|
|
||||||
mb = common.hex2bin(msg)[32:]
|
mb = common.hex2bin(msg)[32:]
|
||||||
@ -153,7 +172,7 @@ def surface_velocity(msg, source=False):
|
|||||||
# ground track
|
# ground track
|
||||||
trk_status = int(mb[12])
|
trk_status = int(mb[12])
|
||||||
if trk_status == 1:
|
if trk_status == 1:
|
||||||
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
|
trk = common.bin2int(mb[13:20]) * 360 / 128
|
||||||
trk = round(trk, 1)
|
trk = round(trk, 1)
|
||||||
else:
|
else:
|
||||||
trk = None
|
trk = None
|
||||||
@ -164,18 +183,17 @@ def surface_velocity(msg, source=False):
|
|||||||
if mov == 0 or mov > 124:
|
if mov == 0 or mov > 124:
|
||||||
spd = None
|
spd = None
|
||||||
elif mov == 1:
|
elif mov == 1:
|
||||||
spd = 0
|
spd = 0.0
|
||||||
elif mov == 124:
|
elif mov == 124:
|
||||||
spd = 175
|
spd = 175.0
|
||||||
else:
|
else:
|
||||||
movs = [2, 9, 13, 39, 94, 109, 124]
|
mov_lb = [2, 9, 13, 39, 94, 109, 124]
|
||||||
kts = [0.125, 1, 2, 15, 70, 100, 175]
|
kts_lb: list[float] = [0.125, 1, 2, 15, 70, 100, 175]
|
||||||
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
|
step: list[float] = [0.125, 0.25, 0.5, 1, 2, 5]
|
||||||
step = (kts[i] - kts[i - 1]) * 1.0 / (movs[i] - movs[i - 1])
|
i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov)
|
||||||
spd = kts[i - 1] + (mov - movs[i - 1]) * step
|
spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1]
|
||||||
spd = round(spd, 2)
|
|
||||||
|
|
||||||
if source:
|
if source:
|
||||||
return spd, trk, 0, "GS", "TRUE_NORTH", None
|
return spd, trk, 0, "GS", "TRUE_NORTH", None # type: ignore
|
||||||
else:
|
else:
|
||||||
return spd, trk, 0, "GS"
|
return spd, trk, 0, "GS"
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# BDS 0,8
|
# BDS 0,8
|
||||||
# ADS-B TC=1-4
|
# ADS-B TC=1-4
|
||||||
# Aircraft identitification and category
|
# Aircraft identification and category
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def category(msg):
|
def category(msg: str) -> int:
|
||||||
"""Aircraft category number
|
"""Aircraft category number
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -17,7 +17,8 @@ def category(msg):
|
|||||||
int: category number
|
int: category number
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
tc = common.typecode(msg)
|
||||||
|
if tc is None or tc < 1 or tc > 4:
|
||||||
raise RuntimeError("%s: Not a identification message" % msg)
|
raise RuntimeError("%s: Not a identification message" % msg)
|
||||||
|
|
||||||
msgbin = common.hex2bin(msg)
|
msgbin = common.hex2bin(msg)
|
||||||
@ -25,7 +26,7 @@ def category(msg):
|
|||||||
return common.bin2int(mebin[5:8])
|
return common.bin2int(mebin[5:8])
|
||||||
|
|
||||||
|
|
||||||
def callsign(msg):
|
def callsign(msg: str) -> str:
|
||||||
"""Aircraft callsign
|
"""Aircraft callsign
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -34,8 +35,9 @@ def callsign(msg):
|
|||||||
Returns:
|
Returns:
|
||||||
string: callsign
|
string: callsign
|
||||||
"""
|
"""
|
||||||
|
tc = common.typecode(msg)
|
||||||
|
|
||||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
if tc is None or tc < 1 or tc > 4:
|
||||||
raise RuntimeError("%s: Not a identification message" % msg)
|
raise RuntimeError("%s: Not a identification message" % msg)
|
||||||
|
|
||||||
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
||||||
|
@ -1,35 +1,41 @@
|
|||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# BDS 0,9
|
# BDS 0,9
|
||||||
# ADS-B TC=19
|
# ADS-B TC=19
|
||||||
# Aircraft Airborn velocity
|
# Aircraft Airborne velocity
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
def airborne_velocity(msg, source=False):
|
|
||||||
|
def airborne_velocity(
|
||||||
|
msg: str, source: bool = False
|
||||||
|
) -> None | tuple[None | int, None | float, None | int, str]:
|
||||||
"""Decode airborne velocity.
|
"""Decode airborne velocity.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
source (boolean): Include direction and vertical rate sources in return. Default to False.
|
source (boolean): Include direction and vertical rate sources in return.
|
||||||
|
Default to False.
|
||||||
If set to True, the function will return six value instead of four.
|
If set to True, the function will return six value instead of four.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int, float, int, string, [string], [string]: Four or six parameters, including:
|
int, float, int, string, [string], [string]:
|
||||||
- Speed (kt)
|
- Speed (kt)
|
||||||
- Angle (degree), either ground track or heading
|
- Angle (degree), either ground track or heading
|
||||||
- Vertical rate (ft/min)
|
- Vertical rate (ft/min)
|
||||||
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
||||||
- [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
|
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
|
||||||
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if common.typecode(msg) != 19:
|
if common.typecode(msg) != 19:
|
||||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
raise RuntimeError(
|
||||||
|
"%s: Not a airborne velocity message, expecting TC=19" % msg
|
||||||
|
)
|
||||||
|
|
||||||
mb = common.hex2bin(msg)[32:]
|
mb = common.hex2bin(msg)[32:]
|
||||||
|
|
||||||
@ -38,14 +44,26 @@ def airborne_velocity(msg, source=False):
|
|||||||
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
|
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
trk_or_hdg: None | float
|
||||||
|
spd: None | float
|
||||||
|
|
||||||
if subtype in (1, 2):
|
if subtype in (1, 2):
|
||||||
|
|
||||||
|
v_ew = common.bin2int(mb[14:24])
|
||||||
|
v_ns = common.bin2int(mb[25:35])
|
||||||
|
|
||||||
|
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_sign = -1 if mb[13] == "1" else 1
|
||||||
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
|
v_ew = v_ew - 1 # east-west velocity
|
||||||
if subtype == 2: # Supersonic
|
if subtype == 2: # Supersonic
|
||||||
v_ew *= 4
|
v_ew *= 4
|
||||||
|
|
||||||
v_ns_sign = -1 if mb[24] == "1" else 1
|
v_ns_sign = -1 if mb[24] == "1" else 1
|
||||||
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
|
v_ns = v_ns - 1 # north-south velocity
|
||||||
if subtype == 2: # Supersonic
|
if subtype == 2: # Supersonic
|
||||||
v_ns *= 4
|
v_ns *= 4
|
||||||
|
|
||||||
@ -59,22 +77,23 @@ def airborne_velocity(msg, source=False):
|
|||||||
trk = math.degrees(trk) # convert to degrees
|
trk = math.degrees(trk) # convert to degrees
|
||||||
trk = trk if trk >= 0 else trk + 360 # no negative val
|
trk = trk if trk >= 0 else trk + 360 # no negative val
|
||||||
|
|
||||||
spd_type = "GS"
|
|
||||||
trk_or_hdg = round(trk, 2)
|
trk_or_hdg = round(trk, 2)
|
||||||
|
|
||||||
|
spd_type = "GS"
|
||||||
dir_type = "TRUE_NORTH"
|
dir_type = "TRUE_NORTH"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if mb[13] == "0":
|
if mb[13] == "0":
|
||||||
hdg = None
|
hdg = None
|
||||||
else:
|
else:
|
||||||
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
|
hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
|
||||||
hdg = round(hdg, 2)
|
hdg = round(hdg, 2)
|
||||||
|
|
||||||
trk_or_hdg = hdg
|
trk_or_hdg = hdg
|
||||||
|
|
||||||
spd = common.bin2int(mb[25:35])
|
spd = common.bin2int(mb[25:35])
|
||||||
spd = None if spd == 0 else spd - 1
|
spd = None if spd == 0 else spd - 1
|
||||||
if subtype == 4: # Supersonic
|
if subtype == 4 and spd is not None: # Supersonic
|
||||||
spd *= 4
|
spd *= 4
|
||||||
|
|
||||||
if mb[24] == "0":
|
if mb[24] == "0":
|
||||||
@ -82,7 +101,7 @@ def airborne_velocity(msg, source=False):
|
|||||||
else:
|
else:
|
||||||
spd_type = "TAS"
|
spd_type = "TAS"
|
||||||
|
|
||||||
dir_type = "MAGENTIC_NORTH"
|
dir_type = "MAGNETIC_NORTH"
|
||||||
|
|
||||||
vr_source = "GNSS" if mb[35] == "0" else "BARO"
|
vr_source = "GNSS" if mb[35] == "0" else "BARO"
|
||||||
vr_sign = -1 if mb[36] == "1" else 1
|
vr_sign = -1 if mb[36] == "1" else 1
|
||||||
@ -90,12 +109,19 @@ def airborne_velocity(msg, source=False):
|
|||||||
vs = None if vr == 0 else int(vr_sign * (vr - 1) * 64)
|
vs = None if vr == 0 else int(vr_sign * (vr - 1) * 64)
|
||||||
|
|
||||||
if source:
|
if source:
|
||||||
return spd, trk_or_hdg, vs, spd_type, dir_type, vr_source
|
return ( # type: ignore
|
||||||
|
spd,
|
||||||
|
trk_or_hdg,
|
||||||
|
vs,
|
||||||
|
spd_type,
|
||||||
|
dir_type,
|
||||||
|
vr_source,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return spd, trk_or_hdg, vs, spd_type
|
return spd, trk_or_hdg, vs, spd_type
|
||||||
|
|
||||||
|
|
||||||
def altitude_diff(msg):
|
def altitude_diff(msg: str) -> None | float:
|
||||||
"""Decode the differece between GNSS and barometric altitude.
|
"""Decode the differece between GNSS and barometric altitude.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -108,8 +134,10 @@ def altitude_diff(msg):
|
|||||||
"""
|
"""
|
||||||
tc = common.typecode(msg)
|
tc = common.typecode(msg)
|
||||||
|
|
||||||
if tc != 19:
|
if tc is None or tc != 19:
|
||||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
raise RuntimeError(
|
||||||
|
"%s: Not a airborne velocity message, expecting TC=19" % msg
|
||||||
|
)
|
||||||
|
|
||||||
msgbin = common.hex2bin(msg)
|
msgbin = common.hex2bin(msg)
|
||||||
sign = -1 if int(msgbin[80]) else 1
|
sign = -1 if int(msgbin[80]) else 1
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
# Data link capability report
|
# Data link capability report
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is10(msg):
|
def is10(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 1,0
|
"""Check if a message is likely to be BDS code 1,0
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -38,7 +39,7 @@ def is10(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def ovc10(msg):
|
def ovc10(msg: str) -> int:
|
||||||
"""Return the overlay control capability
|
"""Return the overlay control capability
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
# Common usage GICB capability report
|
# Common usage GICB capability report
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from typing import List
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is17(msg):
|
def is17(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 1,7
|
"""Check if a message is likely to be BDS code 1,7
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -21,7 +23,7 @@ def is17(msg):
|
|||||||
|
|
||||||
d = common.hex2bin(common.data(msg))
|
d = common.hex2bin(common.data(msg))
|
||||||
|
|
||||||
if common.bin2int(d[28:56]) != 0:
|
if common.bin2int(d[24:56]) != 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
caps = cap17(msg)
|
caps = cap17(msg)
|
||||||
@ -38,14 +40,14 @@ def is17(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def cap17(msg):
|
def cap17(msg: str) -> List[str]:
|
||||||
"""Extract capacities from BDS 1,7 message
|
"""Extract capacities from BDS 1,7 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: list of support BDS codes
|
list: list of supported BDS codes
|
||||||
"""
|
"""
|
||||||
allbds = [
|
allbds = [
|
||||||
"05",
|
"05",
|
||||||
@ -72,14 +74,10 @@ def cap17(msg):
|
|||||||
"56",
|
"56",
|
||||||
"5F",
|
"5F",
|
||||||
"60",
|
"60",
|
||||||
"NA",
|
|
||||||
"NA",
|
|
||||||
"E1",
|
|
||||||
"E2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
d = common.hex2bin(common.data(msg))
|
d = common.hex2bin(common.data(msg))
|
||||||
idx = [i for i, v in enumerate(d[:28]) if v == "1"]
|
idx = [i for i, v in enumerate(d[:24]) if v == "1"]
|
||||||
capacity = ["BDS" + allbds[i] for i in idx if allbds[i] is not "NA"]
|
capacity = ["BDS" + allbds[i] for i in idx]
|
||||||
|
|
||||||
return capacity
|
return capacity
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
# Aircraft identification
|
# Aircraft identification
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is20(msg):
|
def is20(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 2,0
|
"""Check if a message is likely to be BDS code 2,0
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -24,15 +24,17 @@ def is20(msg):
|
|||||||
if d[0:8] != "00100000":
|
if d[0:8] != "00100000":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cs = cs20(msg)
|
# allow empty callsign
|
||||||
|
if common.bin2int(d[8:56]) == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
if "#" in cs:
|
if "#" in cs20(msg):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def cs20(msg):
|
def cs20(msg: str) -> str:
|
||||||
"""Aircraft callsign
|
"""Aircraft callsign
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
# ACAS active resolution advisory
|
# ACAS active resolution advisory
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is30(msg):
|
def is30(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 2,0
|
"""Check if a message is likely to be BDS code 3,0
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from pyModeS import common
|
from typing import Optional
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is40(msg):
|
def is40(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 4,0
|
"""Check if a message is likely to be BDS code 4,0
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -50,7 +52,7 @@ def is40(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def selalt40mcp(msg):
|
def selalt40mcp(msg: str) -> Optional[int]:
|
||||||
"""Selected altitude, MCP/FCU
|
"""Selected altitude, MCP/FCU
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -68,7 +70,7 @@ def selalt40mcp(msg):
|
|||||||
return alt
|
return alt
|
||||||
|
|
||||||
|
|
||||||
def selalt40fms(msg):
|
def selalt40fms(msg: str) -> Optional[int]:
|
||||||
"""Selected altitude, FMS
|
"""Selected altitude, FMS
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -86,7 +88,7 @@ def selalt40fms(msg):
|
|||||||
return alt
|
return alt
|
||||||
|
|
||||||
|
|
||||||
def p40baro(msg):
|
def p40baro(msg: str) -> Optional[float]:
|
||||||
"""Barometric pressure setting
|
"""Barometric pressure setting
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -104,17 +106,19 @@ def p40baro(msg):
|
|||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
def alt40mcp(msg):
|
def alt40mcp(msg: str) -> Optional[int]:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"alt40mcp() has been renamed to selalt40mcp(). It will be removed in the future.",
|
"""alt40mcp() has been renamed to selalt40mcp().
|
||||||
|
It will be removed in the future.""",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return selalt40mcp(msg)
|
return selalt40mcp(msg)
|
||||||
|
|
||||||
|
|
||||||
def alt40fms(msg):
|
def alt40fms(msg: str) -> Optional[int]:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"alt40fms() has been renamed to selalt40fms(). It will be removed in the future.",
|
"""alt40fms() has been renamed to selalt40fms().
|
||||||
|
It will be removed in the future.""",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return selalt40mcp(msg)
|
return selalt40fms(msg)
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
# Meteorological routine air report
|
# Meteorological routine air report
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is44(msg):
|
def is44(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 4,4.
|
"""Check if a message is likely to be BDS code 4,4.
|
||||||
|
|
||||||
Meteorological routine air report
|
Meteorological routine air report
|
||||||
@ -51,7 +53,7 @@ def is44(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def wind44(msg):
|
def wind44(msg: str) -> Tuple[Optional[int], Optional[float]]:
|
||||||
"""Wind speed and direction.
|
"""Wind speed and direction.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -68,12 +70,12 @@ def wind44(msg):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
speed = common.bin2int(d[5:14]) # knots
|
speed = common.bin2int(d[5:14]) # knots
|
||||||
direction = common.bin2int(d[14:23]) * 180.0 / 256.0 # degree
|
direction = common.bin2int(d[14:23]) * 180 / 256 # degree
|
||||||
|
|
||||||
return round(speed, 0), round(direction, 1)
|
return round(speed, 0), round(direction, 1)
|
||||||
|
|
||||||
|
|
||||||
def temp44(msg):
|
def temp44(msg: str) -> Tuple[float, float]:
|
||||||
"""Static air temperature.
|
"""Static air temperature.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -102,7 +104,7 @@ def temp44(msg):
|
|||||||
return temp, temp_alternative
|
return temp, temp_alternative
|
||||||
|
|
||||||
|
|
||||||
def p44(msg):
|
def p44(msg: str) -> Optional[int]:
|
||||||
"""Static pressure.
|
"""Static pressure.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -122,7 +124,7 @@ def p44(msg):
|
|||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
def hum44(msg):
|
def hum44(msg: str) -> Optional[float]:
|
||||||
"""humidity
|
"""humidity
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -136,13 +138,13 @@ def hum44(msg):
|
|||||||
if d[49] == "0":
|
if d[49] == "0":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
hm = common.bin2int(d[50:56]) * 100.0 / 64 # %
|
hm = common.bin2int(d[50:56]) * 100 / 64 # %
|
||||||
|
|
||||||
return round(hm, 1)
|
return round(hm, 1)
|
||||||
|
|
||||||
|
|
||||||
def turb44(msg):
|
def turb44(msg: str) -> Optional[int]:
|
||||||
"""Turblence.
|
"""Turbulence.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg (str): 28 hexdigits string
|
msg (str): 28 hexdigits string
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
# Meteorological hazard report
|
# Meteorological hazard report
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from typing import Optional
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is45(msg):
|
def is45(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 4,5.
|
"""Check if a message is likely to be BDS code 4,5.
|
||||||
|
|
||||||
Meteorological hazard report
|
Meteorological hazard report
|
||||||
@ -60,7 +62,7 @@ def is45(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def turb45(msg):
|
def turb45(msg: str) -> Optional[int]:
|
||||||
"""Turbulence.
|
"""Turbulence.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -78,7 +80,7 @@ def turb45(msg):
|
|||||||
return turb
|
return turb
|
||||||
|
|
||||||
|
|
||||||
def ws45(msg):
|
def ws45(msg: str) -> Optional[int]:
|
||||||
"""Wind shear.
|
"""Wind shear.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -96,7 +98,7 @@ def ws45(msg):
|
|||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
|
||||||
def mb45(msg):
|
def mb45(msg: str) -> Optional[int]:
|
||||||
"""Microburst.
|
"""Microburst.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -114,7 +116,7 @@ def mb45(msg):
|
|||||||
return mb
|
return mb
|
||||||
|
|
||||||
|
|
||||||
def ic45(msg):
|
def ic45(msg: str) -> Optional[int]:
|
||||||
"""Icing.
|
"""Icing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -132,7 +134,7 @@ def ic45(msg):
|
|||||||
return ic
|
return ic
|
||||||
|
|
||||||
|
|
||||||
def wv45(msg):
|
def wv45(msg: str) -> Optional[int]:
|
||||||
"""Wake vortex.
|
"""Wake vortex.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -150,7 +152,7 @@ def wv45(msg):
|
|||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
|
||||||
def temp45(msg):
|
def temp45(msg: str) -> Optional[float]:
|
||||||
"""Static air temperature.
|
"""Static air temperature.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -174,7 +176,7 @@ def temp45(msg):
|
|||||||
return temp
|
return temp
|
||||||
|
|
||||||
|
|
||||||
def p45(msg):
|
def p45(msg: str) -> Optional[int]:
|
||||||
"""Average static pressure.
|
"""Average static pressure.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -191,7 +193,7 @@ def p45(msg):
|
|||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
def rh45(msg):
|
def rh45(msg: str) -> Optional[int]:
|
||||||
"""Radio height.
|
"""Radio height.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
# Track and turn report
|
# Track and turn report
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from typing import Optional
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is50(msg):
|
def is50(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 5,0
|
"""Check if a message is likely to be BDS code 5,0
|
||||||
(Track and turn report)
|
(Track and turn report)
|
||||||
|
|
||||||
@ -40,7 +42,7 @@ def is50(msg):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
roll = roll50(msg)
|
roll = roll50(msg)
|
||||||
if (roll is not None) and abs(roll) > 60:
|
if (roll is not None) and abs(roll) > 50:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
gs = gs50(msg)
|
gs = gs50(msg)
|
||||||
@ -57,7 +59,7 @@ def is50(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def roll50(msg):
|
def roll50(msg: str) -> Optional[float]:
|
||||||
"""Roll angle, BDS 5,0 message
|
"""Roll angle, BDS 5,0 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -78,11 +80,11 @@ def roll50(msg):
|
|||||||
if sign:
|
if sign:
|
||||||
value = value - 512
|
value = value - 512
|
||||||
|
|
||||||
angle = value * 45.0 / 256.0 # degree
|
angle = value * 45 / 256 # degree
|
||||||
return round(angle, 1)
|
return round(angle, 1)
|
||||||
|
|
||||||
|
|
||||||
def trk50(msg):
|
def trk50(msg: str) -> Optional[float]:
|
||||||
"""True track angle, BDS 5,0 message
|
"""True track angle, BDS 5,0 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -102,7 +104,7 @@ def trk50(msg):
|
|||||||
if sign:
|
if sign:
|
||||||
value = value - 1024
|
value = value - 1024
|
||||||
|
|
||||||
trk = value * 90.0 / 512.0
|
trk = value * 90 / 512.0
|
||||||
|
|
||||||
# convert from [-180, 180] to [0, 360]
|
# convert from [-180, 180] to [0, 360]
|
||||||
if trk < 0:
|
if trk < 0:
|
||||||
@ -111,7 +113,7 @@ def trk50(msg):
|
|||||||
return round(trk, 3)
|
return round(trk, 3)
|
||||||
|
|
||||||
|
|
||||||
def gs50(msg):
|
def gs50(msg: str) -> Optional[float]:
|
||||||
"""Ground speed, BDS 5,0 message
|
"""Ground speed, BDS 5,0 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -129,7 +131,7 @@ def gs50(msg):
|
|||||||
return spd
|
return spd
|
||||||
|
|
||||||
|
|
||||||
def rtrk50(msg):
|
def rtrk50(msg: str) -> Optional[float]:
|
||||||
"""Track angle rate, BDS 5,0 message
|
"""Track angle rate, BDS 5,0 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -151,11 +153,11 @@ def rtrk50(msg):
|
|||||||
if sign:
|
if sign:
|
||||||
value = value - 512
|
value = value - 512
|
||||||
|
|
||||||
angle = value * 8.0 / 256.0 # degree / sec
|
angle = value * 8 / 256 # degree / sec
|
||||||
return round(angle, 3)
|
return round(angle, 3)
|
||||||
|
|
||||||
|
|
||||||
def tas50(msg):
|
def tas50(msg: str) -> Optional[float]:
|
||||||
"""Aircraft true airspeed, BDS 5,0 message
|
"""Aircraft true airspeed, BDS 5,0 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
# Air-referenced state vector
|
# Air-referenced state vector
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from typing import Optional
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is53(msg):
|
def is53(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 5,3
|
"""Check if a message is likely to be BDS code 5,3
|
||||||
(Air-referenced state vector)
|
(Air-referenced state vector)
|
||||||
|
|
||||||
@ -58,7 +60,7 @@ def is53(msg):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def hdg53(msg):
|
def hdg53(msg: str) -> Optional[float]:
|
||||||
"""Magnetic heading, BDS 5,3 message
|
"""Magnetic heading, BDS 5,3 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -78,7 +80,7 @@ def hdg53(msg):
|
|||||||
if sign:
|
if sign:
|
||||||
value = value - 1024
|
value = value - 1024
|
||||||
|
|
||||||
hdg = value * 90.0 / 512.0 # degree
|
hdg = value * 90 / 512 # degree
|
||||||
|
|
||||||
# convert from [-180, 180] to [0, 360]
|
# convert from [-180, 180] to [0, 360]
|
||||||
if hdg < 0:
|
if hdg < 0:
|
||||||
@ -87,7 +89,7 @@ def hdg53(msg):
|
|||||||
return round(hdg, 3)
|
return round(hdg, 3)
|
||||||
|
|
||||||
|
|
||||||
def ias53(msg):
|
def ias53(msg: str) -> Optional[float]:
|
||||||
"""Indicated airspeed, DBS 5,3 message
|
"""Indicated airspeed, DBS 5,3 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -105,7 +107,7 @@ def ias53(msg):
|
|||||||
return ias
|
return ias
|
||||||
|
|
||||||
|
|
||||||
def mach53(msg):
|
def mach53(msg: str) -> Optional[float]:
|
||||||
"""MACH number, DBS 5,3 message
|
"""MACH number, DBS 5,3 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -123,7 +125,7 @@ def mach53(msg):
|
|||||||
return round(mach, 3)
|
return round(mach, 3)
|
||||||
|
|
||||||
|
|
||||||
def tas53(msg):
|
def tas53(msg: str) -> Optional[float]:
|
||||||
"""Aircraft true airspeed, BDS 5,3 message
|
"""Aircraft true airspeed, BDS 5,3 message
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -141,7 +143,7 @@ def tas53(msg):
|
|||||||
return round(tas, 1)
|
return round(tas, 1)
|
||||||
|
|
||||||
|
|
||||||
def vr53(msg):
|
def vr53(msg: str) -> Optional[int]:
|
||||||
"""Vertical rate
|
"""Vertical rate
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
# Heading and speed report
|
# Heading and speed report
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from typing import Optional
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
from ...extra import aero
|
||||||
|
|
||||||
|
|
||||||
def is60(msg):
|
def is60(msg: str) -> bool:
|
||||||
"""Check if a message is likely to be BDS code 6,0
|
"""Check if a message is likely to be BDS code 6,0
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -54,10 +57,18 @@ def is60(msg):
|
|||||||
if vr_ins is not None and abs(vr_ins) > 6000:
|
if vr_ins is not None and abs(vr_ins) > 6000:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def hdg60(msg):
|
def hdg60(msg: str) -> Optional[float]:
|
||||||
"""Megnetic heading of aircraft
|
"""Megnetic heading of aircraft
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -77,16 +88,16 @@ def hdg60(msg):
|
|||||||
if sign:
|
if sign:
|
||||||
value = value - 1024
|
value = value - 1024
|
||||||
|
|
||||||
hdg = value * 90 / 512.0 # degree
|
hdg = value * 90 / 512 # degree
|
||||||
|
|
||||||
# convert from [-180, 180] to [0, 360]
|
# convert from [-180, 180] to [0, 360]
|
||||||
if hdg < 0:
|
if hdg < 0:
|
||||||
hdg = 360 + hdg
|
hdg = 360 + hdg
|
||||||
|
|
||||||
return round(hdg, 3)
|
return hdg
|
||||||
|
|
||||||
|
|
||||||
def ias60(msg):
|
def ias60(msg: str) -> Optional[float]:
|
||||||
"""Indicated airspeed
|
"""Indicated airspeed
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -104,7 +115,7 @@ def ias60(msg):
|
|||||||
return ias
|
return ias
|
||||||
|
|
||||||
|
|
||||||
def mach60(msg):
|
def mach60(msg: str) -> Optional[float]:
|
||||||
"""Aircraft MACH number
|
"""Aircraft MACH number
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -119,10 +130,10 @@ def mach60(msg):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
mach = common.bin2int(d[24:34]) * 2.048 / 512.0
|
mach = common.bin2int(d[24:34]) * 2.048 / 512.0
|
||||||
return round(mach, 3)
|
return mach
|
||||||
|
|
||||||
|
|
||||||
def vr60baro(msg):
|
def vr60baro(msg: str) -> Optional[int]:
|
||||||
"""Vertical rate from barometric measurement, this value may be very noisy.
|
"""Vertical rate from barometric measurement, this value may be very noisy.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -148,7 +159,7 @@ def vr60baro(msg):
|
|||||||
return roc
|
return roc
|
||||||
|
|
||||||
|
|
||||||
def vr60ins(msg):
|
def vr60ins(msg: str) -> Optional[int]:
|
||||||
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
|
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
# (Subtype 1)
|
# (Subtype 1)
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
from pyModeS import common
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
def is_emergency(msg: str) -> bool:
|
def is_emergency(msg: str) -> bool:
|
||||||
@ -19,7 +19,9 @@ def is_emergency(msg: str) -> bool:
|
|||||||
:return: if the aircraft has declared an emergency
|
:return: if the aircraft has declared an emergency
|
||||||
"""
|
"""
|
||||||
if common.typecode(msg) != 28:
|
if common.typecode(msg) != 28:
|
||||||
raise RuntimeError("%s: Not an airborne status message, expecting TC=28" % msg)
|
raise RuntimeError(
|
||||||
|
"%s: Not an airborne status message, expecting TC=28" % msg
|
||||||
|
)
|
||||||
|
|
||||||
mb = common.hex2bin(msg)[32:]
|
mb = common.hex2bin(msg)[32:]
|
||||||
subtype = common.bin2int(mb[5:8])
|
subtype = common.bin2int(mb[5:8])
|
||||||
@ -73,12 +75,14 @@ def emergency_squawk(msg: str) -> str:
|
|||||||
:return: aircraft squawk code
|
:return: aircraft squawk code
|
||||||
"""
|
"""
|
||||||
if common.typecode(msg) != 28:
|
if common.typecode(msg) != 28:
|
||||||
raise RuntimeError("%s: Not an airborne status message, expecting TC=28" % msg)
|
raise RuntimeError(
|
||||||
|
"%s: Not an airborne status message, expecting TC=28" % msg
|
||||||
|
)
|
||||||
|
|
||||||
msgbin = common.hex2bin(msg)
|
msgbin = common.hex2bin(msg)
|
||||||
|
|
||||||
# construct the 13 bits Mode A ID code
|
# construct the 13 bits Mode A ID code
|
||||||
idcode = msgbin[43:49] + "0" + msgbin[49:55]
|
idcode = msgbin[43:56]
|
||||||
|
|
||||||
squawk = common.squawk(idcode)
|
squawk = common.squawk(idcode)
|
||||||
return squawk
|
return squawk
|
||||||
|
553
pyModeS/decoder/bds/bds62.py
Normal file
553
pyModeS/decoder/bds/bds62.py
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
# ------------------------------------------
|
||||||
|
# BDS 6,2
|
||||||
|
# ADS-B TC=29
|
||||||
|
# Target State and Status
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ... import common
|
||||||
|
|
||||||
|
|
||||||
|
def selected_altitude(msg: str) -> tuple[None | float, str]:
|
||||||
|
"""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])
|
||||||
|
if alt == 0:
|
||||||
|
return None, "N/A"
|
||||||
|
alt = (alt - 1) * 32
|
||||||
|
alt_source = "MCP/FCU" if int(mb[8]) == 0 else "FMS"
|
||||||
|
|
||||||
|
return alt, alt_source
|
||||||
|
|
||||||
|
|
||||||
|
def target_altitude(msg: str) -> tuple[None | int, str, str]:
|
||||||
|
"""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, "N/A", ""
|
||||||
|
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: str) -> None | int:
|
||||||
|
"""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: str) -> None | int:
|
||||||
|
"""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: str) -> None | float:
|
||||||
|
"""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:
|
||||||
|
return 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: str) -> tuple[None | int, str, str]:
|
||||||
|
"""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:
|
||||||
|
return None, "", "N/A"
|
||||||
|
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: str) -> None | float:
|
||||||
|
"""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])
|
||||||
|
if baro == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return 800 + (baro - 1) * 0.8
|
||||||
|
|
||||||
|
|
||||||
|
def autopilot(msg) -> None | 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) -> None | 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) -> None | 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) -> None | 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) -> None | 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) -> None | 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])
|
@ -23,16 +23,66 @@ MRAR and MHR
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# ELS - elementary surveillance
|
# ELS - elementary surveillance
|
||||||
from pyModeS.decoder.bds.bds10 import *
|
from .bds.bds10 import is10, ovc10
|
||||||
from pyModeS.decoder.bds.bds17 import *
|
from .bds.bds17 import is17, cap17
|
||||||
from pyModeS.decoder.bds.bds20 import *
|
from .bds.bds20 import is20, cs20
|
||||||
from pyModeS.decoder.bds.bds30 import *
|
from .bds.bds30 import is30
|
||||||
|
|
||||||
# ELS - enhanced surveillance
|
# ELS - enhanced surveillance
|
||||||
from pyModeS.decoder.bds.bds40 import *
|
from .bds.bds40 import (
|
||||||
from pyModeS.decoder.bds.bds50 import *
|
is40,
|
||||||
from pyModeS.decoder.bds.bds60 import *
|
selalt40fms,
|
||||||
|
selalt40mcp,
|
||||||
|
p40baro,
|
||||||
|
alt40fms,
|
||||||
|
alt40mcp,
|
||||||
|
)
|
||||||
|
from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50
|
||||||
|
from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins
|
||||||
|
|
||||||
# MRAR and MHR
|
# MRAR and MHR
|
||||||
from pyModeS.decoder.bds.bds44 import *
|
from .bds.bds44 import is44, wind44, temp44, p44, hum44, turb44
|
||||||
from pyModeS.decoder.bds.bds45 import *
|
from .bds.bds45 import is45, turb45, ws45, mb45, ic45, wv45, temp45, p45, rh45
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"is10",
|
||||||
|
"ovc10",
|
||||||
|
"is17",
|
||||||
|
"cap17",
|
||||||
|
"is20",
|
||||||
|
"cs20",
|
||||||
|
"is30",
|
||||||
|
"is40",
|
||||||
|
"selalt40fms",
|
||||||
|
"selalt40mcp",
|
||||||
|
"p40baro",
|
||||||
|
"alt40fms",
|
||||||
|
"alt40mcp",
|
||||||
|
"is50",
|
||||||
|
"roll50",
|
||||||
|
"trk50",
|
||||||
|
"gs50",
|
||||||
|
"rtrk50",
|
||||||
|
"tas50",
|
||||||
|
"is60",
|
||||||
|
"hdg60",
|
||||||
|
"ias60",
|
||||||
|
"mach60",
|
||||||
|
"vr60baro",
|
||||||
|
"vr60ins",
|
||||||
|
"is44",
|
||||||
|
"wind44",
|
||||||
|
"temp44",
|
||||||
|
"p44",
|
||||||
|
"hum44",
|
||||||
|
"turb44",
|
||||||
|
"is45",
|
||||||
|
"turb45",
|
||||||
|
"ws45",
|
||||||
|
"mb45",
|
||||||
|
"ic45",
|
||||||
|
"wv45",
|
||||||
|
"temp45",
|
||||||
|
"p45",
|
||||||
|
"rh45",
|
||||||
|
]
|
||||||
|
@ -11,25 +11,56 @@ The EHS wrapper imports all functions from the following modules:
|
|||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from pyModeS.decoder.bds.bds40 import *
|
from .bds.bds40 import (
|
||||||
from pyModeS.decoder.bds.bds50 import *
|
is40,
|
||||||
from pyModeS.decoder.bds.bds60 import *
|
selalt40fms,
|
||||||
from pyModeS.decoder.bds import infer
|
selalt40mcp,
|
||||||
|
p40baro,
|
||||||
|
alt40fms,
|
||||||
|
alt40mcp,
|
||||||
|
)
|
||||||
|
from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50
|
||||||
|
from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins
|
||||||
|
from .bds import infer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"is40",
|
||||||
|
"selalt40fms",
|
||||||
|
"selalt40mcp",
|
||||||
|
"p40baro",
|
||||||
|
"alt40fms",
|
||||||
|
"alt40mcp",
|
||||||
|
"is50",
|
||||||
|
"roll50",
|
||||||
|
"trk50",
|
||||||
|
"gs50",
|
||||||
|
"rtrk50",
|
||||||
|
"tas50",
|
||||||
|
"is60",
|
||||||
|
"hdg60",
|
||||||
|
"ias60",
|
||||||
|
"mach60",
|
||||||
|
"vr60baro",
|
||||||
|
"vr60ins",
|
||||||
|
"infer",
|
||||||
|
]
|
||||||
|
|
||||||
warnings.simplefilter("once", DeprecationWarning)
|
warnings.simplefilter("once", DeprecationWarning)
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning
|
"pms.ehs module is deprecated. Please use pms.commb instead.",
|
||||||
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def BDS(msg):
|
def BDS(msg):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning
|
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.",
|
||||||
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return infer(msg)
|
return infer(msg)
|
||||||
|
|
||||||
|
|
||||||
def icao(msg):
|
def icao(msg):
|
||||||
from pyModeS.decoder.common import icao
|
from . import common
|
||||||
|
|
||||||
return icao(msg)
|
return common.icao(msg)
|
||||||
|
@ -10,14 +10,26 @@ The ELS wrapper imports all functions from the following modules:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pyModeS.decoder.bds.bds10 import *
|
|
||||||
from pyModeS.decoder.bds.bds17 import *
|
|
||||||
from pyModeS.decoder.bds.bds20 import *
|
|
||||||
from pyModeS.decoder.bds.bds30 import *
|
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from .bds.bds10 import is10, ovc10
|
||||||
|
from .bds.bds17 import cap17, is17
|
||||||
|
from .bds.bds20 import cs20, is20
|
||||||
|
from .bds.bds30 import is30
|
||||||
|
|
||||||
warnings.simplefilter("once", DeprecationWarning)
|
warnings.simplefilter("once", DeprecationWarning)
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning
|
"pms.els module is deprecated. Please use pms.commb instead.",
|
||||||
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"is10",
|
||||||
|
"ovc10",
|
||||||
|
"is17",
|
||||||
|
"cap17",
|
||||||
|
"is20",
|
||||||
|
"cs20",
|
||||||
|
"is30",
|
||||||
|
]
|
||||||
|
25
pyModeS/decoder/flarm/__init__.py
Normal file
25
pyModeS/decoder/flarm/__init__.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from .decode import flarm as flarm_decode
|
||||||
|
|
||||||
|
__all__ = ["DecodedMessage", "flarm"]
|
||||||
|
|
||||||
|
|
||||||
|
class DecodedMessage(TypedDict):
|
||||||
|
timestamp: int
|
||||||
|
icao24: str
|
||||||
|
latitude: float
|
||||||
|
longitude: float
|
||||||
|
altitude: int
|
||||||
|
vertical_speed: float
|
||||||
|
groundspeed: int
|
||||||
|
track: int
|
||||||
|
type: str
|
||||||
|
sensorLatitude: float
|
||||||
|
sensorLongitude: float
|
||||||
|
isIcao24: bool
|
||||||
|
noTrack: bool
|
||||||
|
stealth: bool
|
||||||
|
|
||||||
|
|
||||||
|
flarm = flarm_decode
|
13
pyModeS/decoder/flarm/core.h
Normal file
13
pyModeS/decoder/flarm/core.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#ifndef __CORE_H__
|
||||||
|
#define __CORE_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define DELTA 0x9e3779b9
|
||||||
|
#define MX (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))
|
||||||
|
|
||||||
|
void make_key(int *key, long time, long address);
|
||||||
|
long obscure(long key, unsigned long seed);
|
||||||
|
void btea(uint32_t *v, int n, uint32_t const key[4]);
|
||||||
|
|
||||||
|
#endif
|
4
pyModeS/decoder/flarm/core.pxd
Normal file
4
pyModeS/decoder/flarm/core.pxd
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
cdef extern from "core.h":
|
||||||
|
void make_key(int*, long time, long address)
|
||||||
|
void btea(int*, int, int*)
|
14
pyModeS/decoder/flarm/decode.pyi
Normal file
14
pyModeS/decoder/flarm/decode.pyi
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from . import DecodedMessage
|
||||||
|
|
||||||
|
AIRCRAFT_TYPES: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
def flarm(
|
||||||
|
timestamp: int,
|
||||||
|
msg: str,
|
||||||
|
refLat: float,
|
||||||
|
refLon: float,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> DecodedMessage: ...
|
145
pyModeS/decoder/flarm/decode.pyx
Normal file
145
pyModeS/decoder/flarm/decode.pyx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
from core cimport make_key as c_make_key, btea as c_btea
|
||||||
|
from cpython cimport array
|
||||||
|
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
from ctypes import c_byte
|
||||||
|
from textwrap import wrap
|
||||||
|
|
||||||
|
AIRCRAFT_TYPES = [
|
||||||
|
"Unknown", # 0
|
||||||
|
"Glider", # 1
|
||||||
|
"Tow-Plane", # 2
|
||||||
|
"Helicopter", # 3
|
||||||
|
"Parachute", # 4
|
||||||
|
"Parachute Drop-Plane", # 5
|
||||||
|
"Hangglider", # 6
|
||||||
|
"Paraglider", # 7
|
||||||
|
"Aircraft", # 8
|
||||||
|
"Jet", # 9
|
||||||
|
"UFO", # 10
|
||||||
|
"Balloon", # 11
|
||||||
|
"Airship", # 12
|
||||||
|
"UAV", # 13
|
||||||
|
"Reserved", # 14
|
||||||
|
"Static Obstacle", # 15
|
||||||
|
]
|
||||||
|
|
||||||
|
cdef long bytearray2int(str icao24):
|
||||||
|
return (
|
||||||
|
(int(icao24[4:6], 16) & 0xFF)
|
||||||
|
| ((int(icao24[2:4], 16) & 0xFF) << 8)
|
||||||
|
| ((int(icao24[:2], 16) & 0xFF) << 16)
|
||||||
|
)
|
||||||
|
|
||||||
|
cpdef array.array make_key(long timestamp, str icao24):
|
||||||
|
cdef long addr = bytearray2int(icao24)
|
||||||
|
cdef array.array a = array.array('i', [0, 0, 0, 0])
|
||||||
|
c_make_key(a.data.as_ints, timestamp, (addr << 8) & 0xffffff)
|
||||||
|
return a
|
||||||
|
|
||||||
|
cpdef array.array btea(long timestamp, str msg):
|
||||||
|
cdef int p
|
||||||
|
cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2]
|
||||||
|
cdef array.array key = make_key(timestamp, icao24)
|
||||||
|
|
||||||
|
pieces = wrap(msg[8:], 8)
|
||||||
|
cdef array.array toDecode = array.array('i', len(pieces) * [0])
|
||||||
|
for i, piece in enumerate(pieces):
|
||||||
|
p = 0
|
||||||
|
for elt in wrap(piece, 2)[::-1]:
|
||||||
|
p = (p << 8) + int(elt, 16)
|
||||||
|
toDecode[i] = p
|
||||||
|
|
||||||
|
c_btea(toDecode.data.as_ints, -5, key.data.as_ints)
|
||||||
|
return toDecode
|
||||||
|
|
||||||
|
cdef float velocity(int ns, int ew):
|
||||||
|
return math.hypot(ew / 4, ns / 4)
|
||||||
|
|
||||||
|
def heading(ns, ew, velocity):
|
||||||
|
if velocity < 1e-6:
|
||||||
|
velocity = 1
|
||||||
|
return (math.atan2(ew / velocity / 4, ns / velocity / 4) / 0.01745) % 360
|
||||||
|
|
||||||
|
def turningRate(a1, a2):
|
||||||
|
return ((((a2 - a1)) + 540) % 360) - 180
|
||||||
|
|
||||||
|
def flarm(long timestamp, str msg, float refLat, float refLon, **kwargs):
|
||||||
|
"""Decode a FLARM message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timestamp (int)
|
||||||
|
msg (str)
|
||||||
|
refLat (float): the receiver's location
|
||||||
|
refLon (float): the receiver's location
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a dictionary with all decoded fields. Any extra keyword argument passed
|
||||||
|
is included in the output dictionary.
|
||||||
|
"""
|
||||||
|
cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2]
|
||||||
|
cdef int magic = int(msg[6:8], 16)
|
||||||
|
|
||||||
|
if magic != 0x10 and magic != 0x20:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cdef array.array decoded = btea(timestamp, msg)
|
||||||
|
|
||||||
|
cdef int aircraft_type = (decoded[0] >> 28) & 0xF
|
||||||
|
cdef int gps = (decoded[0] >> 16) & 0xFFF
|
||||||
|
cdef int raw_vs = c_byte(decoded[0] & 0x3FF).value
|
||||||
|
|
||||||
|
noTrack = ((decoded[0] >> 14) & 0x1) == 1
|
||||||
|
stealth = ((decoded[0] >> 13) & 0x1) == 1
|
||||||
|
|
||||||
|
cdef int altitude = (decoded[1] >> 19) & 0x1FFF
|
||||||
|
|
||||||
|
cdef int lat = decoded[1] & 0x7FFFF
|
||||||
|
|
||||||
|
cdef int mult_factor = 1 << ((decoded[2] >> 30) & 0x3)
|
||||||
|
cdef int lon = decoded[2] & 0xFFFFF
|
||||||
|
|
||||||
|
ns = list(
|
||||||
|
c_byte((decoded[3] >> (i * 8)) & 0xFF).value * mult_factor
|
||||||
|
for i in range(4)
|
||||||
|
)
|
||||||
|
ew = list(
|
||||||
|
c_byte((decoded[4] >> (i * 8)) & 0xFF).value * mult_factor
|
||||||
|
for i in range(4)
|
||||||
|
)
|
||||||
|
|
||||||
|
cdef int roundLat = int(refLat * 1e7) >> 7
|
||||||
|
lat = (lat - roundLat) % 0x080000
|
||||||
|
if lat >= 0x040000:
|
||||||
|
lat -= 0x080000
|
||||||
|
lat = (((lat + roundLat) << 7) + 0x40)
|
||||||
|
|
||||||
|
roundLon = int(refLon * 1e7) >> 7
|
||||||
|
lon = (lon - roundLon) % 0x100000
|
||||||
|
if lon >= 0x080000:
|
||||||
|
lon -= 0x100000
|
||||||
|
lon = (((lon + roundLon) << 7) + 0x40)
|
||||||
|
|
||||||
|
speed = sum(velocity(n, e) for n, e in zip(ns, ew)) / 4
|
||||||
|
|
||||||
|
heading4 = heading(ns[0], ew[0], speed)
|
||||||
|
heading8 = heading(ns[1], ew[1], speed)
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
timestamp=timestamp,
|
||||||
|
icao24=icao24,
|
||||||
|
latitude=round(lat * 1e-7, 6),
|
||||||
|
longitude=round(lon * 1e-7, 6),
|
||||||
|
geoaltitude=altitude,
|
||||||
|
vertical_speed=raw_vs * mult_factor / 10,
|
||||||
|
groundspeed=round(speed),
|
||||||
|
track=round(heading4 - 4 * turningRate(heading4, heading8) / 4),
|
||||||
|
type=AIRCRAFT_TYPES[aircraft_type],
|
||||||
|
sensorLatitude=refLat,
|
||||||
|
sensorLongitude=refLon,
|
||||||
|
isIcao24=magic==0x10,
|
||||||
|
noTrack=noTrack,
|
||||||
|
stealth=stealth,
|
||||||
|
**kwargs
|
||||||
|
)
|
@ -1,5 +1,138 @@
|
|||||||
"""
|
"""
|
||||||
Warpper for short roll call surveillance replies DF=4/5
|
Decode short roll call surveillance replies, with downlink format 4 or 5
|
||||||
|
"""
|
||||||
|
|
||||||
[To be implemented]
|
from __future__ import annotations
|
||||||
"""
|
from typing import Callable, TypeVar
|
||||||
|
|
||||||
|
from .. import common
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
F = Callable[[str], T]
|
||||||
|
|
||||||
|
|
||||||
|
def _checkdf(func: F[T]) -> F[T]:
|
||||||
|
"""Ensure downlink format is 4 or 5."""
|
||||||
|
|
||||||
|
def wrapper(msg: str) -> T:
|
||||||
|
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 fs(msg: str) -> tuple[int, str]:
|
||||||
|
"""Decode flight status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (str): 14 hexdigits string
|
||||||
|
Returns:
|
||||||
|
int, str: flight status, description
|
||||||
|
|
||||||
|
"""
|
||||||
|
msgbin = common.hex2bin(msg)
|
||||||
|
fs = common.bin2int(msgbin[5:8])
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@_checkdf
|
||||||
|
def dr(msg: str) -> tuple[int, str]:
|
||||||
|
"""Decode downlink request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (str): 14 hexdigits string
|
||||||
|
Returns:
|
||||||
|
int, str: downlink request, description
|
||||||
|
|
||||||
|
"""
|
||||||
|
msgbin = common.hex2bin(msg)
|
||||||
|
dr = common.bin2int(msgbin[8:13])
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@_checkdf
|
||||||
|
def um(msg: str) -> tuple[int, int, None | str]:
|
||||||
|
"""Decode utility message.
|
||||||
|
|
||||||
|
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 = common.hex2bin(msg)
|
||||||
|
iis = common.bin2int(msgbin[13:17])
|
||||||
|
ids = common.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
|
||||||
|
|
||||||
|
|
||||||
|
@_checkdf
|
||||||
|
def altitude(msg: str) -> None | int:
|
||||||
|
"""Decode altitude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (String): 14 hexdigits string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: altitude in ft
|
||||||
|
|
||||||
|
"""
|
||||||
|
return common.altcode(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@_checkdf
|
||||||
|
def identity(msg: str) -> str:
|
||||||
|
"""Decode squawk code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (String): 14 hexdigits string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: squawk code
|
||||||
|
|
||||||
|
"""
|
||||||
|
return common.idcode(msg)
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
"""Uncertainty parameters.
|
"""Uncertainty parameters.
|
||||||
|
|
||||||
See source code at: https://github.com/junzis/pyModeS/blob/master/pyModeS/decoder/uncertainty.py
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info < (3, 8):
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
else:
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
NA = None
|
NA = None
|
||||||
|
|
||||||
TC_NUCp_lookup = {
|
TC_NUCp_lookup = {
|
||||||
@ -26,7 +34,7 @@ TC_NUCp_lookup = {
|
|||||||
22: 0,
|
22: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
TC_NICv1_lookup = {
|
TC_NICv1_lookup: dict[int, int | dict[int, int]] = {
|
||||||
5: 11,
|
5: 11,
|
||||||
6: 10,
|
6: 10,
|
||||||
7: 9,
|
7: 9,
|
||||||
@ -46,7 +54,7 @@ TC_NICv1_lookup = {
|
|||||||
22: 0,
|
22: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
TC_NICv2_lookup = {
|
TC_NICv2_lookup: dict[int, int | dict[int, int]] = {
|
||||||
5: 11,
|
5: 11,
|
||||||
6: 10,
|
6: 10,
|
||||||
7: {2: 9, 0: 8},
|
7: {2: 9, 0: 8},
|
||||||
@ -67,7 +75,13 @@ TC_NICv2_lookup = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NUCp = {
|
class NUCpEntry(TypedDict):
|
||||||
|
HPL: None | float
|
||||||
|
RCu: None | int
|
||||||
|
RCv: None | int
|
||||||
|
|
||||||
|
|
||||||
|
NUCp: dict[int, NUCpEntry] = {
|
||||||
9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
|
9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
|
||||||
8: {"HPL": 25, "RCu": 10, "RCv": 15},
|
8: {"HPL": 25, "RCu": 10, "RCv": 15},
|
||||||
7: {"HPL": 185, "RCu": 93, "RCv": NA},
|
7: {"HPL": 185, "RCu": 93, "RCv": NA},
|
||||||
@ -80,7 +94,13 @@ NUCp = {
|
|||||||
0: {"HPL": NA, "RCu": NA, "RCv": NA},
|
0: {"HPL": NA, "RCu": NA, "RCv": NA},
|
||||||
}
|
}
|
||||||
|
|
||||||
NUCv = {
|
|
||||||
|
class NUCvEntry(TypedDict):
|
||||||
|
HVE: None | float
|
||||||
|
VVE: None | float
|
||||||
|
|
||||||
|
|
||||||
|
NUCv: dict[int, NUCvEntry] = {
|
||||||
0: {"HVE": NA, "VVE": NA},
|
0: {"HVE": NA, "VVE": NA},
|
||||||
1: {"HVE": 10, "VVE": 15.2},
|
1: {"HVE": 10, "VVE": 15.2},
|
||||||
2: {"HVE": 3, "VVE": 4.5},
|
2: {"HVE": 3, "VVE": 4.5},
|
||||||
@ -88,7 +108,13 @@ NUCv = {
|
|||||||
4: {"HVE": 0.3, "VVE": 0.46},
|
4: {"HVE": 0.3, "VVE": 0.46},
|
||||||
}
|
}
|
||||||
|
|
||||||
NACp = {
|
|
||||||
|
class NACpEntry(TypedDict):
|
||||||
|
EPU: None | int
|
||||||
|
VEPU: None | int
|
||||||
|
|
||||||
|
|
||||||
|
NACp: dict[int, NACpEntry] = {
|
||||||
11: {"EPU": 3, "VEPU": 4},
|
11: {"EPU": 3, "VEPU": 4},
|
||||||
10: {"EPU": 10, "VEPU": 15},
|
10: {"EPU": 10, "VEPU": 15},
|
||||||
9: {"EPU": 30, "VEPU": 45},
|
9: {"EPU": 30, "VEPU": 45},
|
||||||
@ -103,7 +129,13 @@ NACp = {
|
|||||||
0: {"EPU": NA, "VEPU": NA},
|
0: {"EPU": NA, "VEPU": NA},
|
||||||
}
|
}
|
||||||
|
|
||||||
NACv = {
|
|
||||||
|
class NACvEntry(TypedDict):
|
||||||
|
HFOMr: None | float
|
||||||
|
VFOMr: None | float
|
||||||
|
|
||||||
|
|
||||||
|
NACv: dict[int, NACvEntry] = {
|
||||||
0: {"HFOMr": NA, "VFOMr": NA},
|
0: {"HFOMr": NA, "VFOMr": NA},
|
||||||
1: {"HFOMr": 10, "VFOMr": 15.2},
|
1: {"HFOMr": 10, "VFOMr": 15.2},
|
||||||
2: {"HFOMr": 3, "VFOMr": 4.5},
|
2: {"HFOMr": 3, "VFOMr": 4.5},
|
||||||
@ -111,7 +143,13 @@ NACv = {
|
|||||||
4: {"HFOMr": 0.3, "VFOMr": 0.46},
|
4: {"HFOMr": 0.3, "VFOMr": 0.46},
|
||||||
}
|
}
|
||||||
|
|
||||||
SIL = {
|
|
||||||
|
class SILEntry(TypedDict):
|
||||||
|
PE_RCu: None | float
|
||||||
|
PE_VPL: None | float
|
||||||
|
|
||||||
|
|
||||||
|
SIL: dict[int, SILEntry] = {
|
||||||
3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
|
3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
|
||||||
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
|
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
|
||||||
1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
|
1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
|
||||||
@ -119,7 +157,12 @@ SIL = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NICv1 = {
|
class NICv1Entry(TypedDict):
|
||||||
|
Rc: None | float
|
||||||
|
VPL: None | float
|
||||||
|
|
||||||
|
|
||||||
|
NICv1: dict[int, dict[int, NICv1Entry]] = {
|
||||||
# NIC is used as the index at second Level
|
# NIC is used as the index at second Level
|
||||||
11: {0: {"Rc": 7.5, "VPL": 11}},
|
11: {0: {"Rc": 7.5, "VPL": 11}},
|
||||||
10: {0: {"Rc": 25, "VPL": 37.5}},
|
10: {0: {"Rc": 25, "VPL": 37.5}},
|
||||||
@ -135,7 +178,12 @@ NICv1 = {
|
|||||||
0: {0: {"Rc": NA, "VPL": NA}},
|
0: {0: {"Rc": NA, "VPL": NA}},
|
||||||
}
|
}
|
||||||
|
|
||||||
NICv2 = {
|
|
||||||
|
class NICv2Entry(TypedDict):
|
||||||
|
Rc: None | float
|
||||||
|
|
||||||
|
|
||||||
|
NICv2: dict[int, dict[int, NICv2Entry]] = {
|
||||||
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
|
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
|
||||||
11: {0: {"Rc": 7.5}},
|
11: {0: {"Rc": 7.5}},
|
||||||
10: {0: {"Rc": 25}},
|
10: {0: {"Rc": 25}},
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
from pyModeS import common
|
from typing import Optional
|
||||||
|
from .. import common
|
||||||
|
from textwrap import wrap
|
||||||
|
|
||||||
|
|
||||||
def uplink_icao(msg):
|
def uplink_icao(msg: str) -> str:
|
||||||
"""Calculate the ICAO address from a Mode-S interrogation (uplink message)"""
|
"Calculate the ICAO address from a Mode-S interrogation (uplink message)"
|
||||||
p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4)
|
p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4)
|
||||||
data = int(msg[:-6], 16)
|
data = int(msg[:-6], 16)
|
||||||
PA = int(msg[-6:], 16)
|
PA = int(msg[-6:], 16)
|
||||||
@ -19,7 +21,203 @@ def uplink_icao(msg):
|
|||||||
return "%06X" % (ad >> 2)
|
return "%06X" % (ad >> 2)
|
||||||
|
|
||||||
|
|
||||||
def uf(msg):
|
def uf(msg: str) -> int:
|
||||||
"""Decode Uplink Format value, bits 1 to 5."""
|
"""Decode Uplink Format value, bits 1 to 5."""
|
||||||
ufbin = common.hex2bin(msg[:2])
|
ufbin = common.hex2bin(msg[:2])
|
||||||
return min(common.bin2int(ufbin[0:5]), 24)
|
return min(common.bin2int(ufbin[0:5]), 24)
|
||||||
|
|
||||||
|
|
||||||
|
def bds(msg: str) -> Optional[str]:
|
||||||
|
"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 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:
|
||||||
|
# for other values of DI, the BDS2 is assumed 0
|
||||||
|
# (as per ICAO Annex 10 Vol IV)
|
||||||
|
BDS2 = 0
|
||||||
|
|
||||||
|
return str(format(BDS1,"X")) + str(format(BDS2,"X"))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def pr(msg: str) -> Optional[int]:
|
||||||
|
"""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: str) -> Optional[str]:
|
||||||
|
"""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
|
||||||
|
if UF == 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 in {4, 5, 20, 21}:
|
||||||
|
di = mbytes[1] & 0x7
|
||||||
|
RR = mbytes[1] >> 3 & 0x1F
|
||||||
|
if RR > 15:
|
||||||
|
BDS1 = RR - 16 # noqa: F841
|
||||||
|
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 = ""
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
@ -17,7 +17,7 @@ Speed conversion at altitude H[m] in ISA
|
|||||||
::
|
::
|
||||||
|
|
||||||
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
|
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
|
||||||
Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion
|
Vtas = mach2tas(Mach,H) # mach number to true airspeed (Vtas) conversion
|
||||||
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
|
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
|
||||||
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
|
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
|
||||||
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
|
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
|
||||||
@ -35,18 +35,18 @@ ft = 0.3048 # ft -> m
|
|||||||
fpm = 0.00508 # ft/min -> m/s
|
fpm = 0.00508 # ft/min -> m/s
|
||||||
inch = 0.0254 # inch -> m
|
inch = 0.0254 # inch -> m
|
||||||
sqft = 0.09290304 # 1 square foot
|
sqft = 0.09290304 # 1 square foot
|
||||||
nm = 1852.0 # nautical mile -> m
|
nm = 1852 # nautical mile -> m
|
||||||
lbs = 0.453592 # pound -> kg
|
lbs = 0.453592 # pound -> kg
|
||||||
g0 = 9.80665 # m/s2, Sea level gravity constant
|
g0 = 9.80665 # m/s2, Sea level gravity constant
|
||||||
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
|
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
|
||||||
p0 = 101325.0 # Pa, air pressure, sea level ISA
|
p0 = 101325 # Pa, air pressure, sea level ISA
|
||||||
rho0 = 1.225 # kg/m3, air density, sea level ISA
|
rho0 = 1.225 # kg/m3, air density, sea level ISA
|
||||||
T0 = 288.15 # K, temperature, sea level ISA
|
T0 = 288.15 # K, temperature, sea level ISA
|
||||||
gamma = 1.40 # cp/cv for air
|
gamma = 1.40 # cp/cv for air
|
||||||
gamma1 = 0.2 # (gamma-1)/2 for air
|
gamma1 = 0.2 # (gamma-1)/2 for air
|
||||||
gamma2 = 3.5 # gamma/(gamma-1) for air
|
gamma2 = 3.5 # gamma/(gamma-1) for air
|
||||||
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
|
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
|
||||||
r_earth = 6371000.0 # m, average earth radius
|
r_earth = 6371000 # m, average earth radius
|
||||||
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
|
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
|
# phi = 90 - latitude
|
||||||
phi1 = np.radians(90.0 - lat1)
|
phi1 = np.radians(90 - lat1)
|
||||||
phi2 = np.radians(90.0 - lat2)
|
phi2 = np.radians(90 - lat2)
|
||||||
|
|
||||||
# theta = longitude
|
# theta = longitude
|
||||||
theta1 = np.radians(lon1)
|
theta1 = np.radians(lon1)
|
||||||
@ -158,16 +158,16 @@ def tas2eas(Vtas, H):
|
|||||||
def cas2tas(Vcas, H):
|
def cas2tas(Vcas, H):
|
||||||
"""Calibrated Airspeed to True Airspeed"""
|
"""Calibrated Airspeed to True Airspeed"""
|
||||||
p, rho, T = atmos(H)
|
p, rho, T = atmos(H)
|
||||||
qdyn = p0 * ((1.0 + rho0 * Vcas * Vcas / (7.0 * p0)) ** 3.5 - 1.0)
|
qdyn = p0 * ((1 + rho0 * Vcas * Vcas / (7 * p0)) ** 3.5 - 1.0)
|
||||||
Vtas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
|
Vtas = np.sqrt(7 * p / rho * ((1 + qdyn / p) ** (2 / 7.0) - 1.0))
|
||||||
return Vtas
|
return Vtas
|
||||||
|
|
||||||
|
|
||||||
def tas2cas(Vtas, H):
|
def tas2cas(Vtas, H):
|
||||||
"""True Airspeed to Calibrated Airspeed"""
|
"""True Airspeed to Calibrated Airspeed"""
|
||||||
p, rho, T = atmos(H)
|
p, rho, T = atmos(H)
|
||||||
qdyn = p * ((1.0 + rho * Vtas * Vtas / (7.0 * p)) ** 3.5 - 1.0)
|
qdyn = p * ((1 + rho * Vtas * Vtas / (7 * p)) ** 3.5 - 1.0)
|
||||||
Vcas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
|
Vcas = np.sqrt(7 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2 / 7.0) - 1.0))
|
||||||
return Vcas
|
return Vcas
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyModeS as pms
|
import pyModeS as pms
|
||||||
from rtlsdr import RtlSdr
|
|
||||||
import time
|
try:
|
||||||
|
import rtlsdr # type: ignore
|
||||||
|
except:
|
||||||
|
print("------------------------------------------------------------------------")
|
||||||
|
print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !")
|
||||||
|
print("------------------------------------------------------------------------")
|
||||||
|
|
||||||
sampling_rate = 2e6
|
sampling_rate = 2e6
|
||||||
smaples_per_microsec = 2
|
smaples_per_microsec = 2
|
||||||
@ -21,7 +27,7 @@ class RtlReader(object):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(RtlReader, self).__init__()
|
super(RtlReader, self).__init__()
|
||||||
self.signal_buffer = [] # amplitude of the sample only
|
self.signal_buffer = [] # amplitude of the sample only
|
||||||
self.sdr = RtlSdr()
|
self.sdr = rtlsdr.RtlSdr()
|
||||||
self.sdr.sample_rate = sampling_rate
|
self.sdr.sample_rate = sampling_rate
|
||||||
self.sdr.center_freq = modes_frequency
|
self.sdr.center_freq = modes_frequency
|
||||||
self.sdr.gain = "auto"
|
self.sdr.gain = "auto"
|
||||||
@ -31,6 +37,8 @@ class RtlReader(object):
|
|||||||
self.stop_flag = False
|
self.stop_flag = False
|
||||||
self.noise_floor = 1e6
|
self.noise_floor = 1e6
|
||||||
|
|
||||||
|
self.exception_queue = None
|
||||||
|
|
||||||
def _calc_noise(self):
|
def _calc_noise(self):
|
||||||
"""Calculate noise floor"""
|
"""Calculate noise floor"""
|
||||||
window = smaples_per_microsec * 100
|
window = smaples_per_microsec * 100
|
||||||
@ -162,6 +170,7 @@ class RtlReader(object):
|
|||||||
|
|
||||||
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
|
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
|
||||||
self.raw_pipe_in = raw_pipe_in
|
self.raw_pipe_in = raw_pipe_in
|
||||||
|
self.exception_queue = exception_queue
|
||||||
self.stop_flag = stop_flag
|
self.stop_flag = stop_flag
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -173,8 +182,8 @@ class RtlReader(object):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
if exception_queue is not None:
|
if self.exception_queue is not None:
|
||||||
exception_queue.put(tb)
|
self.exception_queue.put(tb)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,11 +7,6 @@ import pyModeS as pms
|
|||||||
import traceback
|
import traceback
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
if sys.version_info > (3, 0):
|
|
||||||
PY_VERSION = 3
|
|
||||||
else:
|
|
||||||
PY_VERSION = 2
|
|
||||||
|
|
||||||
|
|
||||||
class TcpClient(object):
|
class TcpClient(object):
|
||||||
def __init__(self, host, port, datatype):
|
def __init__(self, host, port, datatype):
|
||||||
@ -28,6 +23,8 @@ class TcpClient(object):
|
|||||||
self.raw_pipe_in = None
|
self.raw_pipe_in = None
|
||||||
self.stop_flag = False
|
self.stop_flag = False
|
||||||
|
|
||||||
|
self.exception_queue = None
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.socket = zmq.Context().socket(zmq.STREAM)
|
self.socket = zmq.Context().socket(zmq.STREAM)
|
||||||
self.socket.setsockopt(zmq.LINGER, 0)
|
self.socket.setsockopt(zmq.LINGER, 0)
|
||||||
@ -35,7 +32,7 @@ class TcpClient(object):
|
|||||||
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
|
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.socket.disconnect()
|
self.socket.close()
|
||||||
|
|
||||||
def read_raw_buffer(self):
|
def read_raw_buffer(self):
|
||||||
""" Read raw ADS-B data type.
|
""" Read raw ADS-B data type.
|
||||||
@ -255,6 +252,7 @@ class TcpClient(object):
|
|||||||
|
|
||||||
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
|
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
|
||||||
self.raw_pipe_in = raw_pipe_in
|
self.raw_pipe_in = raw_pipe_in
|
||||||
|
self.exception_queue = exception_queue
|
||||||
self.stop_flag = stop_flag
|
self.stop_flag = stop_flag
|
||||||
self.connect()
|
self.connect()
|
||||||
|
|
||||||
@ -262,9 +260,6 @@ class TcpClient(object):
|
|||||||
try:
|
try:
|
||||||
received = [i for i in self.socket.recv(4096)]
|
received = [i for i in self.socket.recv(4096)]
|
||||||
|
|
||||||
if PY_VERSION == 2:
|
|
||||||
received = [ord(i) for i in received]
|
|
||||||
|
|
||||||
self.buffer.extend(received)
|
self.buffer.extend(received)
|
||||||
# print(''.join(x.encode('hex') for x in self.buffer))
|
# print(''.join(x.encode('hex') for x in self.buffer))
|
||||||
|
|
||||||
@ -286,7 +281,8 @@ class TcpClient(object):
|
|||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
exception_queue.put(tb)
|
if self.exception_queue is not None:
|
||||||
|
self.exception_queue.put(tb)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@ -296,4 +292,7 @@ if __name__ == "__main__":
|
|||||||
port = int(sys.argv[2])
|
port = int(sys.argv[2])
|
||||||
datatype = sys.argv[3]
|
datatype = sys.argv[3]
|
||||||
client = TcpClient(host=host, port=port, datatype=datatype)
|
client = TcpClient(host=host, port=port, datatype=datatype)
|
||||||
|
try:
|
||||||
client.run()
|
client.run()
|
||||||
|
finally:
|
||||||
|
client.stop()
|
||||||
|
0
pyModeS/py.typed
Normal file
0
pyModeS/py.typed
Normal file
@ -5,14 +5,14 @@ from textwrap import wrap
|
|||||||
|
|
||||||
|
|
||||||
def hex2bin(hexstr: str) -> str:
|
def hex2bin(hexstr: str) -> str:
|
||||||
"""Convert a hexdecimal string to binary string, with zero fillings."""
|
"""Convert a hexadecimal string to binary string, with zero fillings."""
|
||||||
num_of_bits = len(hexstr) * 4
|
num_of_bits = len(hexstr) * 4
|
||||||
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
|
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
|
||||||
return binstr
|
return binstr
|
||||||
|
|
||||||
|
|
||||||
def hex2int(hexstr: str) -> int:
|
def hex2int(hexstr: str) -> int:
|
||||||
"""Convert a hexdecimal string to integer."""
|
"""Convert a hexadecimal string to integer."""
|
||||||
return int(hexstr, 16)
|
return int(hexstr, 16)
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ def bin2int(binstr: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def bin2hex(binstr: str) -> str:
|
def bin2hex(binstr: str) -> str:
|
||||||
"""Convert a binary string to hexdecimal string."""
|
"""Convert a binary string to hexadecimal string."""
|
||||||
return "{0:X}".format(int(binstr, 2))
|
return "{0:X}".format(int(binstr, 2))
|
||||||
|
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ def cprNL(lat: float) -> int:
|
|||||||
|
|
||||||
nz = 15
|
nz = 15
|
||||||
a = 1 - np.cos(np.pi / (2 * nz))
|
a = 1 - np.cos(np.pi / (2 * nz))
|
||||||
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
|
b = np.cos(np.pi / 180 * abs(lat)) ** 2
|
||||||
nl = 2 * np.pi / (np.arccos(1 - a / b))
|
nl = 2 * np.pi / (np.arccos(1 - a / b))
|
||||||
NL = floor(nl)
|
NL = floor(nl)
|
||||||
return NL
|
return NL
|
||||||
@ -234,7 +234,7 @@ def squawk(binstr: str) -> str:
|
|||||||
int: altitude in ft
|
int: altitude in ft
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if len(binstr) != 13 or set(binstr) != set("01"):
|
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
|
||||||
raise RuntimeError("Input must be 13 bits binary string")
|
raise RuntimeError("Input must be 13 bits binary string")
|
||||||
|
|
||||||
C1 = binstr[0]
|
C1 = binstr[0]
|
||||||
@ -296,7 +296,7 @@ def altitude(binstr: str) -> Optional[int]:
|
|||||||
"""
|
"""
|
||||||
alt: Optional[int]
|
alt: Optional[int]
|
||||||
|
|
||||||
if len(binstr) != 13 or set(binstr) != set("01"):
|
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
|
||||||
raise RuntimeError("Input must be 13 bits binary string")
|
raise RuntimeError("Input must be 13 bits binary string")
|
||||||
|
|
||||||
Mbit = binstr[6]
|
Mbit = binstr[6]
|
||||||
@ -404,3 +404,85 @@ def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
import csv
|
import csv
|
||||||
import pyModeS as pms
|
import pyModeS as pms
|
||||||
@ -231,10 +232,13 @@ class Decode:
|
|||||||
self.acs[icao]["t60"] = t
|
self.acs[icao]["t60"] = t
|
||||||
if ias60:
|
if ias60:
|
||||||
self.acs[icao]["ias"] = ias60
|
self.acs[icao]["ias"] = ias60
|
||||||
|
output_buffer.append([t, icao, "ias60", ias60])
|
||||||
if hdg60:
|
if hdg60:
|
||||||
self.acs[icao]["hdg"] = hdg60
|
self.acs[icao]["hdg"] = hdg60
|
||||||
|
output_buffer.append([t, icao, "hdg60", hdg60])
|
||||||
if mach60:
|
if mach60:
|
||||||
self.acs[icao]["mach"] = mach60
|
self.acs[icao]["mach"] = mach60
|
||||||
|
output_buffer.append([t, icao, "mach60", mach60])
|
||||||
|
|
||||||
if roc60baro:
|
if roc60baro:
|
||||||
output_buffer.append([t, icao, "roc60baro", roc60baro])
|
output_buffer.append([t, icao, "roc60baro", roc60baro])
|
||||||
|
@ -12,9 +12,6 @@ from pyModeS.streamer.screen import Screen
|
|||||||
from pyModeS.streamer.source import NetSource, RtlSdrSource
|
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"]
|
support_rawtypes = ["raw", "beast", "skysense"]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@ -26,8 +23,9 @@ parser.add_argument(
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--connect",
|
"--connect",
|
||||||
help="Define server, port and data type. Supported data types are: %s"
|
help="Define server, port and data type. Supported data types are: {}".format(
|
||||||
% support_rawtypes,
|
support_rawtypes
|
||||||
|
),
|
||||||
nargs=3,
|
nargs=3,
|
||||||
metavar=("SERVER", "PORT", "DATATYPE"),
|
metavar=("SERVER", "PORT", "DATATYPE"),
|
||||||
default=None,
|
default=None,
|
||||||
@ -86,6 +84,10 @@ if DUMPTO is not None:
|
|||||||
sys.exit(1)
|
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()
|
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
|
||||||
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
|
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
|
||||||
exception_queue = multiprocessing.Queue()
|
exception_queue = multiprocessing.Queue()
|
||||||
|
3
setup.cfg
Normal file
3
setup.cfg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://github.com/embray/setup.cfg
|
||||||
|
[metadata]
|
||||||
|
license_file = LICENSE
|
55
setup.py
55
setup.py
@ -11,6 +11,8 @@ Steps for deploying a new version:
|
|||||||
4. twine upload dist/*
|
4. twine upload dist/*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
# Always prefer setuptools over distutils
|
# Always prefer setuptools over distutils
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f:
|
|||||||
|
|
||||||
details = dict(
|
details = dict(
|
||||||
name="pyModeS",
|
name="pyModeS",
|
||||||
version="2.8",
|
version="2.11",
|
||||||
description="Python Mode-S and ADS-B Decoder",
|
description="Python Mode-S and ADS-B Decoder",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
url="https://github.com/junzis/pyModeS",
|
url="https://github.com/junzis/pyModeS",
|
||||||
@ -43,18 +45,57 @@ details = dict(
|
|||||||
],
|
],
|
||||||
keywords="Mode-S ADS-B EHS ELS Comm-B",
|
keywords="Mode-S ADS-B EHS ELS Comm-B",
|
||||||
packages=find_packages(exclude=["contrib", "docs", "tests"]),
|
packages=find_packages(exclude=["contrib", "docs", "tests"]),
|
||||||
install_requires=["numpy", "pyzmq", "pyrtlsdr"],
|
# typing_extensions are no longer necessary after Python 3.8 (TypedDict)
|
||||||
package_data={"pyModeS": ["*.pyx", "*.pxd"]},
|
install_requires=["numpy", "pyzmq", "typing_extensions"],
|
||||||
|
extras_require={"fast": ["Cython"]},
|
||||||
|
package_data={
|
||||||
|
"pyModeS": ["*.pyx", "*.pxd", "py.typed"],
|
||||||
|
"pyModeS.decoder.flarm": ["*.pyx", "*.pxd", "*.pyi"],
|
||||||
|
},
|
||||||
scripts=["pyModeS/streamer/modeslive"],
|
scripts=["pyModeS/streamer/modeslive"],
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from setuptools.extension import Extension
|
from distutils.core import Extension
|
||||||
from Cython.Build import cythonize
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
extensions = [Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"])]
|
compile_args = []
|
||||||
|
include_dirs = ["pyModeS/decoder/flarm"]
|
||||||
|
|
||||||
setup(**dict(details, ext_modules=cythonize(extensions)))
|
if sys.platform == "linux":
|
||||||
|
compile_args += [
|
||||||
|
"-march=native",
|
||||||
|
"-O3",
|
||||||
|
"-msse",
|
||||||
|
"-msse2",
|
||||||
|
"-mfma",
|
||||||
|
"-mfpmath=sse",
|
||||||
|
"-Wno-pointer-sign",
|
||||||
|
]
|
||||||
|
|
||||||
except:
|
extensions = [
|
||||||
|
Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"]),
|
||||||
|
Extension(
|
||||||
|
"pyModeS.decoder.flarm.decode",
|
||||||
|
[
|
||||||
|
"pyModeS/decoder/flarm/decode.pyx",
|
||||||
|
"pyModeS/decoder/flarm/core.c",
|
||||||
|
],
|
||||||
|
extra_compile_args=compile_args,
|
||||||
|
include_dirs=include_dirs,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
**dict(
|
||||||
|
details,
|
||||||
|
ext_modules=cythonize(
|
||||||
|
extensions,
|
||||||
|
include_path=include_dirs,
|
||||||
|
compiler_directives={"binding": True, "language_level": 3},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
setup(**details)
|
setup(**details)
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import sys
|
|
||||||
import time
|
|
||||||
import csv
|
import csv
|
||||||
|
import time
|
||||||
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "cython":
|
from pyModeS.decoder import adsb
|
||||||
from pyModeS.c_decoder import adsb
|
|
||||||
else:
|
|
||||||
from pyModeS.decoder import adsb
|
|
||||||
|
|
||||||
print("===== Decode ADS-B sample data=====")
|
print("===== Decode ADS-B sample data=====")
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def bds_info(BDS, m):
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
info = None
|
info = []
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@ -87,5 +87,5 @@ def commb_decode_all(df, n=None):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
commb_decode_all(df=20, n=100)
|
commb_decode_all(df=20, n=500)
|
||||||
commb_decode_all(df=21, n=100)
|
commb_decode_all(df=21, n=500)
|
||||||
|
@ -43,14 +43,20 @@ def test_adsb_position_with_ref():
|
|||||||
|
|
||||||
|
|
||||||
def test_adsb_airborne_position_with_ref():
|
def test_adsb_airborne_position_with_ref():
|
||||||
pos = adsb.airborne_position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0)
|
pos = adsb.airborne_position_with_ref(
|
||||||
|
"8D40058B58C901375147EFD09357", 49.0, 6.0
|
||||||
|
)
|
||||||
assert pos == (49.82410, 6.06785)
|
assert pos == (49.82410, 6.06785)
|
||||||
pos = adsb.airborne_position_with_ref("8D40058B58C904A87F402D3B8C59", 49.0, 6.0)
|
pos = adsb.airborne_position_with_ref(
|
||||||
|
"8D40058B58C904A87F402D3B8C59", 49.0, 6.0
|
||||||
|
)
|
||||||
assert pos == (49.81755, 6.08442)
|
assert pos == (49.81755, 6.08442)
|
||||||
|
|
||||||
|
|
||||||
def test_adsb_surface_position_with_ref():
|
def test_adsb_surface_position_with_ref():
|
||||||
pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5)
|
pos = adsb.surface_position_with_ref(
|
||||||
|
"8FC8200A3AB8F5F893096B000000", -43.5, 172.5
|
||||||
|
)
|
||||||
assert pos == (-43.48564, 172.53942)
|
assert pos == (-43.48564, 172.53942)
|
||||||
|
|
||||||
|
|
||||||
@ -76,14 +82,27 @@ def test_adsb_velocity():
|
|||||||
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
|
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
|
||||||
assert vgs == (159, 182.88, -832, "GS")
|
assert vgs == (159, 182.88, -832, "GS")
|
||||||
assert vas == (375, 243.98, -2304, "TAS")
|
assert vas == (375, 243.98, -2304, "TAS")
|
||||||
assert vgs_surface == (19.0, 42.2, 0, "GS")
|
assert vgs_surface == (19, 42.2, 0, "GS")
|
||||||
assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
|
assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
|
||||||
|
|
||||||
|
|
||||||
def test_adsb_emergency():
|
def test_adsb_emergency():
|
||||||
assert not adsb.is_emergency("8DA2C1B6E112B600000000760759")
|
assert not adsb.is_emergency("8DA2C1B6E112B600000000760759")
|
||||||
assert adsb.emergency_state("8DA2C1B6E112B600000000760759") == 0
|
assert adsb.emergency_state("8DA2C1B6E112B600000000760759") == 0
|
||||||
assert adsb.emergency_squawk("8DA2C1B6E112B600000000760759") == "6615"
|
assert adsb.emergency_squawk("8DA2C1B6E112B600000000760759") == "6513"
|
||||||
|
|
||||||
|
|
||||||
|
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") is True
|
||||||
|
assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True
|
||||||
|
assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") is False
|
||||||
|
assert adsb.approach_mode("8DA05629EA21485CBF3F8CADAEEB") is False
|
||||||
|
assert adsb.tcas_operational("8DA05629EA21485CBF3F8CADAEEB") is True
|
||||||
|
assert adsb.lnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True
|
||||||
|
|
||||||
|
|
||||||
# def test_nic():
|
# def test_nic():
|
||||||
|
13
tests/test_allcall.py
Normal file
13
tests/test_allcall.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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
|
@ -1,6 +1,12 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
from pyModeS import bds
|
from pyModeS import bds
|
||||||
|
|
||||||
|
# this one fails on GitHub action for some unknown reason
|
||||||
|
# it looks successful on other Windows instances though
|
||||||
|
# TODO fix later
|
||||||
|
@pytest.mark.skipif(sys.platform == "win32", reason="GitHub Action")
|
||||||
def test_bds_infer():
|
def test_bds_infer():
|
||||||
assert bds.infer("8D406B902015A678D4D220AA4BDA") == "BDS08"
|
assert bds.infer("8D406B902015A678D4D220AA4BDA") == "BDS08"
|
||||||
assert bds.infer("8FC8200A3AB8F5F893096B000000") == "BDS06"
|
assert bds.infer("8FC8200A3AB8F5F893096B000000") == "BDS06"
|
||||||
@ -17,8 +23,8 @@ def test_bds_infer():
|
|||||||
|
|
||||||
def test_bds_is50or60():
|
def test_bds_is50or60():
|
||||||
assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
|
assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
|
||||||
assert bds.is50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == "BDS50"
|
assert bds.is50or60("A8001EBCFFFB23286004A73F6A5B", 320, 250, 14000) == "BDS50"
|
||||||
assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == "BDS60"
|
assert bds.is50or60("A8001EBCFE1B29287FDCA807BCFC", 320, 250, 14000) == "BDS50"
|
||||||
|
|
||||||
|
|
||||||
def test_surface_position():
|
def test_surface_position():
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pyModeS import bds, commb
|
from pyModeS import bds, commb
|
||||||
|
import pytest
|
||||||
|
|
||||||
# from pyModeS import ehs, els # deprecated
|
# from pyModeS import ehs, els # deprecated
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ def test_bds40_functions():
|
|||||||
|
|
||||||
def test_bds50_functions():
|
def test_bds50_functions():
|
||||||
assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1
|
assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1
|
||||||
assert bds.bds50.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
|
assert bds.bds50.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4
|
||||||
assert bds.bds50.trk50("A000139381951536E024D4CCF6B5") == 114.258
|
assert bds.bds50.trk50("A000139381951536E024D4CCF6B5") == 114.258
|
||||||
assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438
|
assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438
|
||||||
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
|
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
|
||||||
@ -38,14 +39,16 @@ def test_bds50_functions():
|
|||||||
|
|
||||||
|
|
||||||
def test_bds60_functions():
|
def test_bds60_functions():
|
||||||
assert bds.bds60.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
|
msg = "A00004128F39F91A7E27C46ADC21"
|
||||||
assert bds.bds60.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
|
||||||
assert bds.bds60.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
|
||||||
assert bds.bds60.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
|
||||||
assert bds.bds60.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
|
||||||
|
|
||||||
assert commb.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
|
assert bds.bds60.hdg60(msg) == pytest.approx(42.71484)
|
||||||
assert commb.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
assert bds.bds60.ias60(msg) == 252
|
||||||
assert commb.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
assert bds.bds60.mach60(msg) == 0.42
|
||||||
assert commb.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
assert bds.bds60.vr60baro(msg) == -1920
|
||||||
assert commb.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
assert bds.bds60.vr60ins(msg) == -1920
|
||||||
|
|
||||||
|
assert commb.hdg60(msg) == pytest.approx(42.71484)
|
||||||
|
assert commb.ias60(msg) == 252
|
||||||
|
assert commb.mach60(msg) == 0.42
|
||||||
|
assert commb.vr60baro(msg) == -1920
|
||||||
|
assert commb.vr60ins(msg) == -1920
|
||||||
|
22
tests/test_surv.py
Normal file
22
tests/test_surv.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
|
Loading…
Reference in New Issue
Block a user