diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..92c2e03
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,12 @@
+[run]
+branch = True
+include = */pyModeS/*
+omit = *tests*
+
+[report]
+exclude_lines =
+ coverage: ignore
+ raise NotImplementedError
+ if TYPE_CHECKING:
+
+ignore_errors = True
\ No newline at end of file
diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml
new file mode 100644
index 0000000..eb6956a
--- /dev/null
+++ b/.github/workflows/pypi-publish.yml
@@ -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/*
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..6e88061
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -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
diff --git a/README.rst b/README.rst
index 4731b59..24f0b68 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
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 `_, `Aerospace Engineering Faculty `_, `CNS/ATM research group `_. It is supported by many `contributors `_ from different institutions.
@@ -72,21 +72,35 @@ Installation examples::
# stable version
pip install pyModeS
+ # conda (compiled) version
+ conda install -c conda-forge pymodes
+
# development version
pip install git+https://github.com/junzis/pyModeS
-Dependencies ``numpy``, ``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)
------------------------------------------
If you want to make use of the (faster) c module, install ``pyModeS`` as follows::
+ # conda (compiled) version
+ conda install -c conda-forge pymodes
+
+ # stable version (to be compiled on your side)
+ pip install pyModeS[fast]
+
+ # development version
git clone https://github.com/junzis/pyModeS
cd pyModeS
- make ext
- make install
+ pip install .[fast]
View live traffic (modeslive)
@@ -112,7 +126,7 @@ General usage::
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
@@ -261,8 +275,20 @@ Mode-S Enhanced Surveillance (EHS)
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
@@ -273,8 +299,8 @@ Meteorological routine air report (MRAR) [Experimental]
pms.commb.hum44(msg) # Humidity (%)
-Meteorological hazard air report (MHR) [Experimental]
-*******************************************************
+Meteorological hazard air report (MHR)
++++++++++++++++++++++++++++++++++++++++++
.. code:: python
diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py
index 138241a..a820278 100644
--- a/pyModeS/__init__.py
+++ b/pyModeS/__init__.py
@@ -4,18 +4,32 @@ import warnings
try:
from . import c_common as common
from .c_common import *
-except:
- from . import py_common as common
- from .py_common import *
+except Exception:
+ from . import py_common as common # type: ignore
+ from .py_common import * # type: ignore
from .decoder import tell
from .decoder import adsb
from .decoder import acas
from .decoder import commb
+from .decoder import allcall
+from .decoder import surv
from .decoder import bds
from .extra import aero
from .extra import tcpclient
+__all__ = [
+ "common",
+ "tell",
+ "adsb",
+ "commb",
+ "allcall",
+ "surv",
+ "bds",
+ "aero",
+ "tcpclient",
+]
+
warnings.simplefilter("once", DeprecationWarning)
diff --git a/pyModeS/c_common.pyi b/pyModeS/c_common.pyi
new file mode 100644
index 0000000..2c68584
--- /dev/null
+++ b/pyModeS/c_common.pyi
@@ -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: ...
diff --git a/pyModeS/c_common.pyx b/pyModeS/c_common.pyx
index e774586..71d5083 100644
--- a/pyModeS/c_common.pyx
+++ b/pyModeS/c_common.pyx
@@ -25,7 +25,7 @@ cdef unsigned char int_to_char(unsigned char i):
@cython.boundscheck(False)
@cython.overflowcheck(False)
cpdef str hex2bin(str hexstr):
- """Convert a hexdecimal string to binary string, with zero fillings."""
+ """Convert a hexadecimal string to binary string, with zero fillings."""
# num_of_bits = len(hexstr) * 4
cdef hexbytes = bytes(hexstr.encode())
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
@@ -73,7 +73,7 @@ cpdef str bin2hex(str binstr):
@cython.boundscheck(False)
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])
# return min(bin2int(dfbin[0:5]), 24)
cdef long df = bin2int(dfbin[0:5])
@@ -228,7 +228,7 @@ cpdef int cprNL(double lat):
cdef int nz = 15
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))
NL = floor(nl)
return NL
@@ -295,7 +295,7 @@ cpdef int altcode(str msg):
@cython.wraparound(False)
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")
cdef bytearray _mbin = bytearray(binstr.encode())
diff --git a/pyModeS/common.pyi b/pyModeS/common.pyi
new file mode 100644
index 0000000..1f279f5
--- /dev/null
+++ b/pyModeS/common.pyi
@@ -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: ...
diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py
index c961ffc..63341df 100644
--- a/pyModeS/decoder/__init__.py
+++ b/pyModeS/decoder/__init__.py
@@ -1,8 +1,8 @@
def tell(msg: str) -> None:
- from pyModeS import common, adsb, commb, bds
+ from .. import common, adsb, commb, bds
def _print(label, value, unit=None):
- print("%20s: " % label, end="")
+ print("%28s: " % label, end="")
print("%s " % value, end="")
if unit:
print(unit)
@@ -20,9 +20,14 @@ def tell(msg: str) -> None:
_print("Protocol", "Mode-S Extended Squitter (ADS-B)")
tc = common.typecode(msg)
+
+ if tc is None:
+ _print("ERROR", "Unknown typecode")
+ return
+
if 1 <= tc <= 4: # callsign
callsign = adsb.callsign(msg)
- _print("Type", "Identitification and category")
+ _print("Type", "Identification and category")
_print("Callsign:", callsign)
if 5 <= tc <= 8: # surface position
@@ -52,12 +57,14 @@ def tell(msg: str) -> None:
if tc == 19:
_print("Type", "Airborne velocity")
- spd, trk, vr, t = adsb.velocity(msg)
- types = {"GS": "Ground speed", "TAS": "True airspeed"}
- _print("Speed", spd, "knots")
- _print("Track", trk, "degrees")
- _print("Vertical rate", vr, "feet/minute")
- _print("Type", types[t])
+ velocity = adsb.velocity(msg)
+ if velocity is not None:
+ spd, trk, vr, t = velocity
+ types = {"GS": "Ground speed", "TAS": "True airspeed"}
+ _print("Speed", spd, "knots")
+ _print("Track", trk, "degrees")
+ _print("Vertical rate", vr, "feet/minute")
+ _print("Type", types[t])
if 20 <= tc <= 22: # airborne position
_print("Type", "Airborne position (with GNSS altitude)")
@@ -71,6 +78,94 @@ def tell(msg: str) -> None:
_print("CPR Longitude", cprlon)
_print("Altitude", alt, "feet")
+ if tc == 29: # target state and status
+ _print("Type", "Target State and Status")
+ subtype = common.bin2int((common.hex2bin(msg)[32:])[5:7])
+ _print("Subtype", subtype)
+ tcas_operational = adsb.tcas_operational(msg)
+ types_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:
_print("Protocol", "Mode-S Comm-B altitude reply")
_print("Altitude", common.altcode(msg), "feet")
@@ -94,7 +189,7 @@ def tell(msg: str) -> None:
}
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]))
else:
_print("BDS", BDS)
diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py
index 7271a15..de81cf5 100644
--- a/pyModeS/decoder/adsb.py
+++ b/pyModeS/decoder/adsb.py
@@ -2,52 +2,120 @@
The ADS-B module also imports functions from the following modules:
-- pyModeS.decoder.bds.bds05: ``airborne_position()``, ``airborne_position_with_ref()``, ``altitude()``
-- pyModeS.decoder.bds.bds06: ``surface_position()``, ``surface_position_with_ref()``, ``surface_velocity()``
-- pyModeS.decoder.bds.bds08: ``category()``, ``callsign()``
-- pyModeS.decoder.bds.bds09: ``airborne_velocity()``, ``altitude_diff()``
+- bds05: ``airborne_position()``, ``airborne_position_with_ref()``,
+ ``altitude()``
+- bds06: ``surface_position()``, ``surface_position_with_ref()``,
+ ``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 pyModeS.decoder import uncertainty
-
-# from pyModeS.decoder.bds import bds05, bds06, bds09
-from pyModeS.decoder.bds.bds05 import (
- airborne_position,
- airborne_position_with_ref,
- altitude as altitude05,
-)
-from pyModeS.decoder.bds.bds06 import (
+from .. import common
+from . import uncertainty
+from .bds.bds05 import airborne_position, airborne_position_with_ref
+from .bds.bds05 import altitude as altitude05
+from .bds.bds06 import (
surface_position,
surface_position_with_ref,
surface_velocity,
)
-from pyModeS.decoder.bds.bds08 import category, callsign
-from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
-from pyModeS.decoder.bds.bds61_st1 import (
- is_emergency,
- emergency_state,
- emergency_squawk,
+from .bds.bds08 import callsign, category
+from .bds.bds09 import airborne_velocity, altitude_diff
+from .bds.bds61_st1 import emergency_squawk, emergency_state, is_emergency
+from .bds.bds62 import (
+ altitude_hold_mode,
+ 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)
-def icao(msg):
+def icao(msg: str) -> None | str:
return common.icao(msg)
-def typecode(msg):
+def typecode(msg: str) -> None | int:
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
position messages.
@@ -69,6 +137,9 @@ def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
tc0 = typecode(msg0)
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 lat_ref is None or lon_ref is None:
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")
-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.
A reference position is required, which can be previously
calculated location, ground station, or airport location.
The function works with both airborne and surface position messages.
- The reference position shall be with in 180NM (airborne) or 45NM (surface)
+ The reference position shall be within 180NM (airborne) or 45NM (surface)
of the true position.
Args:
@@ -110,6 +181,9 @@ def position_with_ref(msg, lat_ref, lon_ref):
tc = typecode(msg)
+ if tc is None:
+ raise RuntimeError("incorrect or inconsistent message types")
+
if 5 <= tc <= 8:
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")
-def altitude(msg):
+def altitude(msg: str) -> None | float:
"""Decode aircraft altitude.
Args:
@@ -132,7 +206,7 @@ def altitude(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)
elif tc >= 5 and tc <= 8:
@@ -144,39 +218,47 @@ def altitude(msg):
return altitude05(msg)
-def velocity(msg, source=False):
- """Calculate the speed, heading, and vertical rate (handles both airborne or surface message).
+def velocity(
+ 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:
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.
Returns:
- int, float, int, string, [string], [string]: Four or six parameters, including:
+ int, float, int, string, [string], [string]:
- Speed (kt)
- Angle (degree), either ground track or heading
- Vertical rate (ft/min)
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- - [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
+ - [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
- [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 None | tuple[None | float, None | float]:
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
@@ -186,11 +268,14 @@ def speed_heading(msg):
Returns:
(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
-def oe_flag(msg):
+def oe_flag(msg: str) -> int:
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
Args:
msg (str): 28 hexdigits string
@@ -201,7 +286,7 @@ def oe_flag(msg):
return int(msgbin[53])
-def version(msg):
+def version(msg: str) -> int:
"""ADS-B Version
Args:
@@ -223,13 +308,15 @@ def version(msg):
return version
-def nuc_p(msg):
- """Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
+def nuc_p(msg: str) -> tuple[int, None | float, None | int, None | int]:
+ """Calculate NUCp, Navigation Uncertainty Category - Position
+ (ADS-B version 1)
Args:
msg (str): 28 hexdigits string,
Returns:
+ int: NUCp, Navigation Uncertainty Category (position)
int: Horizontal Protection Limit
int: 95% Containment Radius - Horizontal (meters)
int: 95% Containment Radius - Vertical (meters)
@@ -237,7 +324,7 @@ def nuc_p(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(
"%s: Not a surface position message (5 tuple[int, None | float, None | float]:
+ """Calculate NUCv, Navigation Uncertainty Category - Velocity
+ (ADS-B version 1)
Args:
msg (str): 28 hexdigits string,
Returns:
+ int: NUCv, Navigation Uncertainty Category (velocity)
int or string: 95% Horizontal Velocity Error
int or string: 95% Vertical Velocity Error
"""
@@ -278,17 +374,18 @@ def nuc_v(msg):
msgbin = common.hex2bin(msg)
NUCv = common.bin2int(msgbin[42:45])
+ index = uncertainty.NUCv.get(NUCv, None)
- try:
- HVE = uncertainty.NUCv[NUCv]["HVE"]
- VVE = uncertainty.NUCv[NUCv]["VVE"]
- except KeyError:
+ if index is not None:
+ HVE = index["HVE"]
+ VVE = index["VVE"]
+ else:
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
Args:
@@ -296,10 +393,12 @@ def nic_v1(msg, NICs):
NICs (int or string): NIC supplement
Returns:
+ int: NIC, Navigation Integrity Category
int or string: Horizontal Radius of Containment
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(
"%s: Not a surface position message (5 tuple[int, int]:
"""Calculate NIC, navigation integrity category, for ADS-B version 2
Args:
msg (str): 28 hexdigits string
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:
+ int: NIC, Navigation Integrity Category
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(
"%s: Not a surface position message (5 int:
"""Obtain NIC supplement bit, TC=31 message
Args:
@@ -382,7 +484,7 @@ def nic_s(msg):
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
Args:
@@ -405,7 +507,7 @@ def nic_a_c(msg):
return nic_a, nic_c
-def nic_b(msg):
+def nic_b(msg: str) -> int:
"""Obtain NICb, navigation integrity category supplement-b
Args:
@@ -416,7 +518,7 @@ def nic_b(msg):
"""
tc = typecode(msg)
- if tc < 9 or tc > 18:
+ if tc is None or tc < 9 or tc > 18:
raise RuntimeError(
"%s: Not a airborne position message, expecting 8 tuple[int, int | None, int | None]:
"""Calculate NACp, Navigation Accuracy Category - Position
Args:
msg (str): 28 hexdigits string, TC = 29 or 31
Returns:
- int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
- int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
+ int: NACp, Navigation Accuracy Category (position)
+ int or string: 95% horizontal accuracy bounds,
+ Estimated Position Uncertainty
+ int or string: 95% vertical accuracy bounds,
+ Vertical Estimated Position Uncertainty
"""
tc = typecode(msg)
@@ -459,18 +564,21 @@ def nac_p(msg):
except KeyError:
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
Args:
msg (str): 28 hexdigits string, TC = 19
Returns:
- 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
+ int: NACv, Navigation Accuracy Category (velocity)
+ 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)
@@ -488,18 +596,23 @@ def nac_v(msg):
except KeyError:
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
Args:
msg (str): 28 hexdigits string with TC = 29, 31
Returns:
- int or string: Probability of exceeding Horizontal Radius of Containment RCu
- int or string: Probability of exceeding Vertical Integrity Containment Region VPL
+ int or string:
+ 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'
"""
tc = typecode(msg)
diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py
index 6e296ca..3023ab3 100644
--- a/pyModeS/decoder/allcall.py
+++ b/pyModeS/decoder/allcall.py
@@ -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
diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py
index f3ed2d2..a3851f0 100644
--- a/pyModeS/decoder/bds/__init__.py
+++ b/pyModeS/decoder/bds/__init__.py
@@ -18,16 +18,13 @@
Common functions for Mode-S decoding
"""
+from typing import Optional
+
import numpy as np
-from pyModeS.extra import aero
-from pyModeS import common
-
-from pyModeS.decoder.bds import (
- bds05,
- bds06,
- bds08,
- bds09,
+from ... import common
+from ...extra import aero
+from . import ( # noqa: F401
bds10,
bds17,
bds20,
@@ -36,12 +33,13 @@ from pyModeS.decoder.bds import (
bds44,
bds45,
bds50,
- bds53,
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.
Args:
@@ -51,7 +49,8 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
alt_ref (float): reference altitude (ADS-B altitude), ft
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))
return vx, vy
+ # message must be both BDS 50 and 60 before processing
if not (bds50.is50(msg) and bds60.is60(msg)):
return None
- h50 = bds50.trk50(msg)
- v50 = bds50.gs50(msg)
-
- if h50 is None or v50 is None:
- return "BDS50,BDS60"
-
+ # --- assuming BDS60 ---
h60 = bds60.hdg60(msg)
m60 = bds60.mach60(msg)
i60 = bds60.ias60(msg)
+ # additional check now knowing the altitude
+ if (m60 is not None) and (i60 is not None):
+ ias_ = aero.mach2cas(m60, alt_ref * aero.ft) / aero.kts
+ if abs(i60 - ias_) > 20:
+ return "BDS50"
+
if h60 is None or (m60 is None and i60 is None):
return "BDS50,BDS60"
m60 = np.nan if m60 is None else m60
i60 = np.nan if i60 is None else i60
+ # --- assuming BDS50 ---
+ h50 = bds50.trk50(msg)
+ v50 = bds50.gs50(msg)
+
+ if h50 is None or v50 is None:
+ return "BDS50,BDS60"
+
XY5 = vxy(v50 * aero.kts, h50)
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
@@ -104,15 +112,17 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
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.
Args:
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:
- 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)
@@ -123,6 +133,8 @@ def infer(msg, mrar=False):
# For ADS-B / Mode-S extended squitter
if df == 17:
tc = common.typecode(msg)
+ if tc is None:
+ return None
if 1 <= tc <= 4:
return "BDS08" # identification and category
diff --git a/pyModeS/decoder/bds/bds05.py b/pyModeS/decoder/bds/bds05.py
index 120c31c..bf07f1a 100644
--- a/pyModeS/decoder/bds/bds05.py
+++ b/pyModeS/decoder/bds/bds05.py
@@ -1,14 +1,20 @@
# ------------------------------------------
# BDS 0,5
# 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):
- """Decode airborn position from a pair of even and odd position message
+def airborne_position(
+ 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:
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.")
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
- cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
- cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
- cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
- cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
+ cprlat_even = common.bin2int(mb0[22:39]) / 131072
+ cprlon_even = common.bin2int(mb0[39:56]) / 131072
+ cprlat_odd = common.bin2int(mb1[22:39]) / 131072
+ cprlon_odd = common.bin2int(mb1[39:56]) / 131072
- air_d_lat_even = 360.0 / 60
- air_d_lat_odd = 360.0 / 59
+ air_d_lat_even = 360 / 60
+ air_d_lat_odd = 360 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
@@ -59,18 +65,19 @@ def airborne_position(msg0, msg1, t0, t1):
return None
# 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
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
- lon = (360.0 / ni) * (m % ni + cprlon_even)
+ lon = (360 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 1, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
- lon = (360.0 / ni) * (m % ni + cprlon_odd)
+ lon = (360 / ni) * (m % ni + cprlon_odd)
if lon > 180:
lon = lon - 360
@@ -78,11 +85,13 @@ def airborne_position(msg0, msg1, t0, t1):
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,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
- be with in 180NM of the true position.
+ be within 180NM of the true position.
Args:
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:]
- cprlat = common.bin2int(mb[22:39]) / 131072.0
- cprlon = common.bin2int(mb[39:56]) / 131072.0
+ cprlat = common.bin2int(mb[22:39]) / 131072
+ cprlon = common.bin2int(mb[39:56]) / 131072
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(
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
if ni > 0:
- d_lon = 360.0 / ni
+ d_lon = 360 / ni
else:
- d_lon = 360.0
+ d_lon = 360
m = common.floor(lon_ref / d_lon) + common.floor(
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)
-def altitude(msg):
+def altitude(msg: str) -> None | int:
"""Decode aircraft altitude
Args:
@@ -135,17 +144,14 @@ def altitude(msg):
tc = common.typecode(msg)
- if tc < 9 or tc == 19 or tc > 22:
- raise RuntimeError("%s: Not a airborn position message" % msg)
+ if tc is None or tc < 9 or tc == 19 or tc > 22:
+ raise RuntimeError("%s: Not an airborne position message" % msg)
mb = common.hex2bin(msg)[32:]
altbin = mb[8:20]
if tc < 19:
altcode = altbin[0:6] + "0" + altbin[6:]
+ return common.altitude(altcode)
else:
- altcode = altbin[0:6] + "0" + altbin[6:]
-
- alt = common.altitude(altcode)
-
- return alt
+ return common.bin2int(altbin) * 3.28084 # type: ignore
diff --git a/pyModeS/decoder/bds/bds06.py b/pyModeS/decoder/bds/bds06.py
index 2c366ce..9077275 100644
--- a/pyModeS/decoder/bds/bds06.py
+++ b/pyModeS/decoder/bds/bds06.py
@@ -1,13 +1,25 @@
# ------------------------------------------
# BDS 0,6
# 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,
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)
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
- cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
- cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
- cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
- cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
+ cprlat_even = common.bin2int(msgbin0[54:71]) / 131072
+ cprlon_even = common.bin2int(msgbin0[71:88]) / 131072
+ cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072
+ cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072
- air_d_lat_even = 90.0 / 60
- air_d_lat_odd = 90.0 / 59
+ air_d_lat_even = 90 / 60
+ air_d_lat_odd = 90 / 59
# compute latitude index 'j'
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))
# solution for north hemisphere
- lat_even_s = lat_even_n - 90.0
- lat_odd_s = lat_odd_n - 90.0
+ lat_even_s = lat_even_n - 90
+ lat_odd_s = lat_odd_n - 90
# chose which solution corrispondes to receiver location
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
# 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
nl = common.cprNL(lat_even)
ni = max(common.cprNL(lat_even) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
- lon = (90.0 / ni) * (m % ni + cprlon_even)
+ lon = (90 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat_odd)
ni = max(common.cprNL(lat_odd) - 1, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
- lon = (90.0 / ni) * (m % ni + cprlon_odd)
+ lon = (90 / ni) * (m % ni + cprlon_odd)
# 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
- 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
- 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__)
lon = lons[imin]
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,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
- be with in 45NM of the true position.
+ be within 45NM of the true position.
Args:
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:]
- cprlat = common.bin2int(mb[22:39]) / 131072.0
- cprlon = common.bin2int(mb[39:56]) / 131072.0
+ cprlat = common.bin2int(mb[22:39]) / 131072
+ cprlon = common.bin2int(mb[39:56]) / 131072
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(
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
if ni > 0:
- d_lon = 90.0 / ni
+ d_lon = 90 / ni
else:
- d_lon = 90.0
+ d_lon = 90
m = common.floor(lon_ref / d_lon) + common.floor(
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)
-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
Args:
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.
Returns:
- int, float, int, string, [string], [string]: Four or six parameters, including:
+ int, float, int, string, [string], [string]:
- Speed (kt)
- Angle (degree), ground track
- Vertical rate, always 0
@@ -145,7 +163,8 @@ def surface_velocity(msg, source=False):
- [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 124:
spd = None
elif mov == 1:
- spd = 0
+ spd = 0.0
elif mov == 124:
- spd = 175
+ spd = 175.0
else:
- movs = [2, 9, 13, 39, 94, 109, 124]
- kts = [0.125, 1, 2, 15, 70, 100, 175]
- i = next(m[0] for m in enumerate(movs) if m[1] > mov)
- step = (kts[i] - kts[i - 1]) * 1.0 / (movs[i] - movs[i - 1])
- spd = kts[i - 1] + (mov - movs[i - 1]) * step
- spd = round(spd, 2)
+ mov_lb = [2, 9, 13, 39, 94, 109, 124]
+ kts_lb: list[float] = [0.125, 1, 2, 15, 70, 100, 175]
+ step: list[float] = [0.125, 0.25, 0.5, 1, 2, 5]
+ i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov)
+ spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1]
if source:
- return spd, trk, 0, "GS", "TRUE_NORTH", None
+ return spd, trk, 0, "GS", "TRUE_NORTH", None # type: ignore
else:
return spd, trk, 0, "GS"
diff --git a/pyModeS/decoder/bds/bds08.py b/pyModeS/decoder/bds/bds08.py
index efacab6..82402b1 100644
--- a/pyModeS/decoder/bds/bds08.py
+++ b/pyModeS/decoder/bds/bds08.py
@@ -1,13 +1,13 @@
# ------------------------------------------
# BDS 0,8
# 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
Args:
@@ -17,7 +17,8 @@ def category(msg):
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)
msgbin = common.hex2bin(msg)
@@ -25,7 +26,7 @@ def category(msg):
return common.bin2int(mebin[5:8])
-def callsign(msg):
+def callsign(msg: str) -> str:
"""Aircraft callsign
Args:
@@ -34,8 +35,9 @@ def callsign(msg):
Returns:
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)
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
diff --git a/pyModeS/decoder/bds/bds09.py b/pyModeS/decoder/bds/bds09.py
index 43acf00..e462feb 100644
--- a/pyModeS/decoder/bds/bds09.py
+++ b/pyModeS/decoder/bds/bds09.py
@@ -1,35 +1,41 @@
# ------------------------------------------
# BDS 0,9
# ADS-B TC=19
-# Aircraft Airborn velocity
+# Aircraft Airborne velocity
# ------------------------------------------
-from pyModeS import common
-
+from __future__ import annotations
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.
Args:
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.
Returns:
- int, float, int, string, [string], [string]: Four or six parameters, including:
+ int, float, int, string, [string], [string]:
- Speed (kt)
- Angle (degree), either ground track or heading
- Vertical rate (ft/min)
- Speed type ('GS' for ground speed, 'AS' for airspeed)
- - [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
+ - [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
- [Optional] Vertical rate source ('BARO' or 'GNSS')
"""
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:]
@@ -38,43 +44,56 @@ def airborne_velocity(msg, source=False):
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
return None
+ trk_or_hdg: None | float
+ spd: None | float
+
if subtype in (1, 2):
- v_ew_sign = -1 if mb[13] == "1" else 1
- v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
- if subtype == 2: # Supersonic
- v_ew *= 4
- v_ns_sign = -1 if mb[24] == "1" else 1
- v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
- if subtype == 2: # Supersonic
- v_ns *= 4
+ v_ew = common.bin2int(mb[14:24])
+ v_ns = common.bin2int(mb[25:35])
- v_we = v_ew_sign * v_ew
- v_sn = v_ns_sign * v_ns
+ if v_ew == 0 or v_ns == 0:
+ spd = None
+ trk_or_hdg = None
+ vs = None
+ else:
+ v_ew_sign = -1 if mb[13] == "1" else 1
+ v_ew = v_ew - 1 # east-west velocity
+ if subtype == 2: # Supersonic
+ v_ew *= 4
- spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
- spd = int(spd)
+ v_ns_sign = -1 if mb[24] == "1" else 1
+ v_ns = v_ns - 1 # north-south velocity
+ if subtype == 2: # Supersonic
+ v_ns *= 4
- trk = math.atan2(v_we, v_sn)
- trk = math.degrees(trk) # convert to degrees
- trk = trk if trk >= 0 else trk + 360 # no negative val
+ v_we = v_ew_sign * v_ew
+ v_sn = v_ns_sign * v_ns
+
+ spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
+ spd = int(spd)
+
+ trk = math.atan2(v_we, v_sn)
+ trk = math.degrees(trk) # convert to degrees
+ trk = trk if trk >= 0 else trk + 360 # no negative val
+
+ trk_or_hdg = round(trk, 2)
spd_type = "GS"
- trk_or_hdg = round(trk, 2)
dir_type = "TRUE_NORTH"
else:
if mb[13] == "0":
hdg = None
else:
- hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
+ hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
hdg = round(hdg, 2)
trk_or_hdg = hdg
spd = common.bin2int(mb[25:35])
spd = None if spd == 0 else spd - 1
- if subtype == 4: # Supersonic
+ if subtype == 4 and spd is not None: # Supersonic
spd *= 4
if mb[24] == "0":
@@ -82,7 +101,7 @@ def airborne_velocity(msg, source=False):
else:
spd_type = "TAS"
- dir_type = "MAGENTIC_NORTH"
+ dir_type = "MAGNETIC_NORTH"
vr_source = "GNSS" if mb[35] == "0" else "BARO"
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)
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:
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.
Args:
@@ -108,8 +134,10 @@ def altitude_diff(msg):
"""
tc = common.typecode(msg)
- if tc != 19:
- raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
+ if tc is None or tc != 19:
+ raise RuntimeError(
+ "%s: Not a airborne velocity message, expecting TC=19" % msg
+ )
msgbin = common.hex2bin(msg)
sign = -1 if int(msgbin[80]) else 1
diff --git a/pyModeS/decoder/bds/bds10.py b/pyModeS/decoder/bds/bds10.py
index b903bcb..b234ff1 100644
--- a/pyModeS/decoder/bds/bds10.py
+++ b/pyModeS/decoder/bds/bds10.py
@@ -3,10 +3,11 @@
# 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
Args:
@@ -38,7 +39,7 @@ def is10(msg):
return True
-def ovc10(msg):
+def ovc10(msg: str) -> int:
"""Return the overlay control capability
Args:
diff --git a/pyModeS/decoder/bds/bds17.py b/pyModeS/decoder/bds/bds17.py
index cb5270a..56b4853 100644
--- a/pyModeS/decoder/bds/bds17.py
+++ b/pyModeS/decoder/bds/bds17.py
@@ -3,10 +3,12 @@
# 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
Args:
@@ -21,7 +23,7 @@ def is17(msg):
d = common.hex2bin(common.data(msg))
- if common.bin2int(d[28:56]) != 0:
+ if common.bin2int(d[24:56]) != 0:
return False
caps = cap17(msg)
@@ -38,14 +40,14 @@ def is17(msg):
return True
-def cap17(msg):
+def cap17(msg: str) -> List[str]:
"""Extract capacities from BDS 1,7 message
Args:
msg (str): 28 hexdigits string
Returns:
- list: list of support BDS codes
+ list: list of supported BDS codes
"""
allbds = [
"05",
@@ -72,14 +74,10 @@ def cap17(msg):
"56",
"5F",
"60",
- "NA",
- "NA",
- "E1",
- "E2",
]
d = common.hex2bin(common.data(msg))
- idx = [i for i, v in enumerate(d[:28]) if v == "1"]
- capacity = ["BDS" + allbds[i] for i in idx if allbds[i] is not "NA"]
+ idx = [i for i, v in enumerate(d[:24]) if v == "1"]
+ capacity = ["BDS" + allbds[i] for i in idx]
return capacity
diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py
index 63e10cd..8ee5513 100644
--- a/pyModeS/decoder/bds/bds20.py
+++ b/pyModeS/decoder/bds/bds20.py
@@ -3,10 +3,10 @@
# 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
Args:
@@ -24,15 +24,17 @@ def is20(msg):
if d[0:8] != "00100000":
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 True
-def cs20(msg):
+def cs20(msg: str) -> str:
"""Aircraft callsign
Args:
diff --git a/pyModeS/decoder/bds/bds30.py b/pyModeS/decoder/bds/bds30.py
index 7270d3c..da23f34 100644
--- a/pyModeS/decoder/bds/bds30.py
+++ b/pyModeS/decoder/bds/bds30.py
@@ -3,11 +3,11 @@
# ACAS active resolution advisory
# ------------------------------------------
-from pyModeS import common
+from ... import common
-def is30(msg):
- """Check if a message is likely to be BDS code 2,0
+def is30(msg: str) -> bool:
+ """Check if a message is likely to be BDS code 3,0
Args:
msg (str): 28 hexdigits string
diff --git a/pyModeS/decoder/bds/bds40.py b/pyModeS/decoder/bds/bds40.py
index bc4950e..66f6874 100644
--- a/pyModeS/decoder/bds/bds40.py
+++ b/pyModeS/decoder/bds/bds40.py
@@ -4,10 +4,12 @@
# ------------------------------------------
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
Args:
@@ -50,7 +52,7 @@ def is40(msg):
return True
-def selalt40mcp(msg):
+def selalt40mcp(msg: str) -> Optional[int]:
"""Selected altitude, MCP/FCU
Args:
@@ -68,7 +70,7 @@ def selalt40mcp(msg):
return alt
-def selalt40fms(msg):
+def selalt40fms(msg: str) -> Optional[int]:
"""Selected altitude, FMS
Args:
@@ -86,7 +88,7 @@ def selalt40fms(msg):
return alt
-def p40baro(msg):
+def p40baro(msg: str) -> Optional[float]:
"""Barometric pressure setting
Args:
@@ -104,17 +106,19 @@ def p40baro(msg):
return p
-def alt40mcp(msg):
+def alt40mcp(msg: str) -> Optional[int]:
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,
)
return selalt40mcp(msg)
-def alt40fms(msg):
+def alt40fms(msg: str) -> Optional[int]:
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,
)
- return selalt40mcp(msg)
+ return selalt40fms(msg)
diff --git a/pyModeS/decoder/bds/bds44.py b/pyModeS/decoder/bds/bds44.py
index a2198a3..ea86210 100644
--- a/pyModeS/decoder/bds/bds44.py
+++ b/pyModeS/decoder/bds/bds44.py
@@ -3,10 +3,12 @@
# 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.
Meteorological routine air report
@@ -51,7 +53,7 @@ def is44(msg):
return True
-def wind44(msg):
+def wind44(msg: str) -> Tuple[Optional[int], Optional[float]]:
"""Wind speed and direction.
Args:
@@ -68,12 +70,12 @@ def wind44(msg):
return None, None
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)
-def temp44(msg):
+def temp44(msg: str) -> Tuple[float, float]:
"""Static air temperature.
Args:
@@ -102,7 +104,7 @@ def temp44(msg):
return temp, temp_alternative
-def p44(msg):
+def p44(msg: str) -> Optional[int]:
"""Static pressure.
Args:
@@ -122,7 +124,7 @@ def p44(msg):
return p
-def hum44(msg):
+def hum44(msg: str) -> Optional[float]:
"""humidity
Args:
@@ -136,13 +138,13 @@ def hum44(msg):
if d[49] == "0":
return None
- hm = common.bin2int(d[50:56]) * 100.0 / 64 # %
+ hm = common.bin2int(d[50:56]) * 100 / 64 # %
return round(hm, 1)
-def turb44(msg):
- """Turblence.
+def turb44(msg: str) -> Optional[int]:
+ """Turbulence.
Args:
msg (str): 28 hexdigits string
diff --git a/pyModeS/decoder/bds/bds45.py b/pyModeS/decoder/bds/bds45.py
index 8dca85c..3c955be 100644
--- a/pyModeS/decoder/bds/bds45.py
+++ b/pyModeS/decoder/bds/bds45.py
@@ -3,10 +3,12 @@
# 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.
Meteorological hazard report
@@ -60,7 +62,7 @@ def is45(msg):
return True
-def turb45(msg):
+def turb45(msg: str) -> Optional[int]:
"""Turbulence.
Args:
@@ -78,7 +80,7 @@ def turb45(msg):
return turb
-def ws45(msg):
+def ws45(msg: str) -> Optional[int]:
"""Wind shear.
Args:
@@ -96,7 +98,7 @@ def ws45(msg):
return ws
-def mb45(msg):
+def mb45(msg: str) -> Optional[int]:
"""Microburst.
Args:
@@ -114,7 +116,7 @@ def mb45(msg):
return mb
-def ic45(msg):
+def ic45(msg: str) -> Optional[int]:
"""Icing.
Args:
@@ -132,7 +134,7 @@ def ic45(msg):
return ic
-def wv45(msg):
+def wv45(msg: str) -> Optional[int]:
"""Wake vortex.
Args:
@@ -150,7 +152,7 @@ def wv45(msg):
return ws
-def temp45(msg):
+def temp45(msg: str) -> Optional[float]:
"""Static air temperature.
Args:
@@ -174,7 +176,7 @@ def temp45(msg):
return temp
-def p45(msg):
+def p45(msg: str) -> Optional[int]:
"""Average static pressure.
Args:
@@ -191,7 +193,7 @@ def p45(msg):
return p
-def rh45(msg):
+def rh45(msg: str) -> Optional[int]:
"""Radio height.
Args:
diff --git a/pyModeS/decoder/bds/bds50.py b/pyModeS/decoder/bds/bds50.py
index a2e534a..b5e10e0 100644
--- a/pyModeS/decoder/bds/bds50.py
+++ b/pyModeS/decoder/bds/bds50.py
@@ -3,10 +3,12 @@
# 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
(Track and turn report)
@@ -40,7 +42,7 @@ def is50(msg):
return False
roll = roll50(msg)
- if (roll is not None) and abs(roll) > 60:
+ if (roll is not None) and abs(roll) > 50:
return False
gs = gs50(msg)
@@ -57,7 +59,7 @@ def is50(msg):
return True
-def roll50(msg):
+def roll50(msg: str) -> Optional[float]:
"""Roll angle, BDS 5,0 message
Args:
@@ -78,11 +80,11 @@ def roll50(msg):
if sign:
value = value - 512
- angle = value * 45.0 / 256.0 # degree
+ angle = value * 45 / 256 # degree
return round(angle, 1)
-def trk50(msg):
+def trk50(msg: str) -> Optional[float]:
"""True track angle, BDS 5,0 message
Args:
@@ -102,7 +104,7 @@ def trk50(msg):
if sign:
value = value - 1024
- trk = value * 90.0 / 512.0
+ trk = value * 90 / 512.0
# convert from [-180, 180] to [0, 360]
if trk < 0:
@@ -111,7 +113,7 @@ def trk50(msg):
return round(trk, 3)
-def gs50(msg):
+def gs50(msg: str) -> Optional[float]:
"""Ground speed, BDS 5,0 message
Args:
@@ -129,7 +131,7 @@ def gs50(msg):
return spd
-def rtrk50(msg):
+def rtrk50(msg: str) -> Optional[float]:
"""Track angle rate, BDS 5,0 message
Args:
@@ -151,11 +153,11 @@ def rtrk50(msg):
if sign:
value = value - 512
- angle = value * 8.0 / 256.0 # degree / sec
+ angle = value * 8 / 256 # degree / sec
return round(angle, 3)
-def tas50(msg):
+def tas50(msg: str) -> Optional[float]:
"""Aircraft true airspeed, BDS 5,0 message
Args:
diff --git a/pyModeS/decoder/bds/bds53.py b/pyModeS/decoder/bds/bds53.py
index 023f7ef..00847dc 100644
--- a/pyModeS/decoder/bds/bds53.py
+++ b/pyModeS/decoder/bds/bds53.py
@@ -3,10 +3,12 @@
# 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
(Air-referenced state vector)
@@ -58,7 +60,7 @@ def is53(msg):
return True
-def hdg53(msg):
+def hdg53(msg: str) -> Optional[float]:
"""Magnetic heading, BDS 5,3 message
Args:
@@ -78,7 +80,7 @@ def hdg53(msg):
if sign:
value = value - 1024
- hdg = value * 90.0 / 512.0 # degree
+ hdg = value * 90 / 512 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
@@ -87,7 +89,7 @@ def hdg53(msg):
return round(hdg, 3)
-def ias53(msg):
+def ias53(msg: str) -> Optional[float]:
"""Indicated airspeed, DBS 5,3 message
Args:
@@ -105,7 +107,7 @@ def ias53(msg):
return ias
-def mach53(msg):
+def mach53(msg: str) -> Optional[float]:
"""MACH number, DBS 5,3 message
Args:
@@ -123,7 +125,7 @@ def mach53(msg):
return round(mach, 3)
-def tas53(msg):
+def tas53(msg: str) -> Optional[float]:
"""Aircraft true airspeed, BDS 5,3 message
Args:
@@ -141,7 +143,7 @@ def tas53(msg):
return round(tas, 1)
-def vr53(msg):
+def vr53(msg: str) -> Optional[int]:
"""Vertical rate
Args:
diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py
index b8d91cb..98c9a79 100644
--- a/pyModeS/decoder/bds/bds60.py
+++ b/pyModeS/decoder/bds/bds60.py
@@ -3,10 +3,13 @@
# 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
Args:
@@ -54,10 +57,18 @@ def is60(msg):
if vr_ins is not None and abs(vr_ins) > 6000:
return False
+ # additional check knowing altitude
+ if (mach is not None) and (ias is not None) and (common.df(msg) == 20):
+ alt = common.altcode(msg)
+ if alt is not None:
+ ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts
+ if abs(ias - ias_) > 20:
+ return False
+
return True
-def hdg60(msg):
+def hdg60(msg: str) -> Optional[float]:
"""Megnetic heading of aircraft
Args:
@@ -77,16 +88,16 @@ def hdg60(msg):
if sign:
value = value - 1024
- hdg = value * 90 / 512.0 # degree
+ hdg = value * 90 / 512 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
hdg = 360 + hdg
- return round(hdg, 3)
+ return hdg
-def ias60(msg):
+def ias60(msg: str) -> Optional[float]:
"""Indicated airspeed
Args:
@@ -104,7 +115,7 @@ def ias60(msg):
return ias
-def mach60(msg):
+def mach60(msg: str) -> Optional[float]:
"""Aircraft MACH number
Args:
@@ -119,10 +130,10 @@ def mach60(msg):
return None
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.
Args:
@@ -148,7 +159,7 @@ def vr60baro(msg):
return roc
-def vr60ins(msg):
+def vr60ins(msg: str) -> Optional[int]:
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
Args:
diff --git a/pyModeS/decoder/bds/bds61_st1.py b/pyModeS/decoder/bds/bds61_st1.py
index 9010639..8f7e2a3 100644
--- a/pyModeS/decoder/bds/bds61_st1.py
+++ b/pyModeS/decoder/bds/bds61_st1.py
@@ -5,7 +5,7 @@
# (Subtype 1)
# ------------------------------------------
-from pyModeS import common
+from ... import common
def is_emergency(msg: str) -> bool:
@@ -19,7 +19,9 @@ def is_emergency(msg: str) -> bool:
:return: if the aircraft has declared an emergency
"""
if common.typecode(msg) != 28:
- raise RuntimeError("%s: Not an airborne status message, expecting TC=28" % msg)
+ raise RuntimeError(
+ "%s: Not an airborne status message, expecting TC=28" % msg
+ )
mb = common.hex2bin(msg)[32:]
subtype = common.bin2int(mb[5:8])
@@ -73,12 +75,14 @@ def emergency_squawk(msg: str) -> str:
:return: aircraft squawk code
"""
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)
# construct the 13 bits Mode A ID code
- idcode = msgbin[43:49] + "0" + msgbin[49:55]
+ idcode = msgbin[43:56]
squawk = common.squawk(idcode)
return squawk
diff --git a/pyModeS/decoder/bds/bds62.py b/pyModeS/decoder/bds/bds62.py
new file mode 100644
index 0000000..4b822de
--- /dev/null
+++ b/pyModeS/decoder/bds/bds62.py
@@ -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])
diff --git a/pyModeS/decoder/commb.py b/pyModeS/decoder/commb.py
index 4957681..3a214c6 100644
--- a/pyModeS/decoder/commb.py
+++ b/pyModeS/decoder/commb.py
@@ -23,16 +23,66 @@ MRAR and MHR
"""
# ELS - elementary surveillance
-from pyModeS.decoder.bds.bds10 import *
-from pyModeS.decoder.bds.bds17 import *
-from pyModeS.decoder.bds.bds20 import *
-from pyModeS.decoder.bds.bds30 import *
+from .bds.bds10 import is10, ovc10
+from .bds.bds17 import is17, cap17
+from .bds.bds20 import is20, cs20
+from .bds.bds30 import is30
# ELS - enhanced surveillance
-from pyModeS.decoder.bds.bds40 import *
-from pyModeS.decoder.bds.bds50 import *
-from pyModeS.decoder.bds.bds60 import *
+from .bds.bds40 import (
+ is40,
+ 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
-from pyModeS.decoder.bds.bds44 import *
-from pyModeS.decoder.bds.bds45 import *
+from .bds.bds44 import is44, wind44, temp44, p44, hum44, turb44
+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",
+]
diff --git a/pyModeS/decoder/ehs.py b/pyModeS/decoder/ehs.py
index bb91a29..849bef5 100644
--- a/pyModeS/decoder/ehs.py
+++ b/pyModeS/decoder/ehs.py
@@ -11,25 +11,56 @@ The EHS wrapper imports all functions from the following modules:
import warnings
-from pyModeS.decoder.bds.bds40 import *
-from pyModeS.decoder.bds.bds50 import *
-from pyModeS.decoder.bds.bds60 import *
-from pyModeS.decoder.bds import infer
+from .bds.bds40 import (
+ is40,
+ 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
+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.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):
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)
def icao(msg):
- from pyModeS.decoder.common import icao
+ from . import common
- return icao(msg)
+ return common.icao(msg)
diff --git a/pyModeS/decoder/els.py b/pyModeS/decoder/els.py
index 9c26b93..be25c1b 100644
--- a/pyModeS/decoder/els.py
+++ b/pyModeS/decoder/els.py
@@ -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
+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.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",
+]
diff --git a/pyModeS/decoder/flarm/__init__.py b/pyModeS/decoder/flarm/__init__.py
new file mode 100644
index 0000000..513b0ed
--- /dev/null
+++ b/pyModeS/decoder/flarm/__init__.py
@@ -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
diff --git a/pyModeS/decoder/flarm/core.h b/pyModeS/decoder/flarm/core.h
new file mode 100644
index 0000000..a8e7b0e
--- /dev/null
+++ b/pyModeS/decoder/flarm/core.h
@@ -0,0 +1,13 @@
+#ifndef __CORE_H__
+#define __CORE_H__
+
+#include
+
+#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
diff --git a/pyModeS/decoder/flarm/core.pxd b/pyModeS/decoder/flarm/core.pxd
new file mode 100644
index 0000000..e1ad1fd
--- /dev/null
+++ b/pyModeS/decoder/flarm/core.pxd
@@ -0,0 +1,4 @@
+
+cdef extern from "core.h":
+ void make_key(int*, long time, long address)
+ void btea(int*, int, int*)
\ No newline at end of file
diff --git a/pyModeS/decoder/flarm/decode.pyi b/pyModeS/decoder/flarm/decode.pyi
new file mode 100644
index 0000000..3b9cbec
--- /dev/null
+++ b/pyModeS/decoder/flarm/decode.pyi
@@ -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: ...
diff --git a/pyModeS/decoder/flarm/decode.pyx b/pyModeS/decoder/flarm/decode.pyx
new file mode 100644
index 0000000..816463d
--- /dev/null
+++ b/pyModeS/decoder/flarm/decode.pyx
@@ -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
+ )
\ No newline at end of file
diff --git a/pyModeS/decoder/surv.py b/pyModeS/decoder/surv.py
index 746f47f..d1dbf3a 100644
--- a/pyModeS/decoder/surv.py
+++ b/pyModeS/decoder/surv.py
@@ -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)
diff --git a/pyModeS/decoder/uncertainty.py b/pyModeS/decoder/uncertainty.py
index cb74cb0..00cf350 100644
--- a/pyModeS/decoder/uncertainty.py
+++ b/pyModeS/decoder/uncertainty.py
@@ -1,8 +1,16 @@
"""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
TC_NUCp_lookup = {
@@ -26,7 +34,7 @@ TC_NUCp_lookup = {
22: 0,
}
-TC_NICv1_lookup = {
+TC_NICv1_lookup: dict[int, int | dict[int, int]] = {
5: 11,
6: 10,
7: 9,
@@ -46,7 +54,7 @@ TC_NICv1_lookup = {
22: 0,
}
-TC_NICv2_lookup = {
+TC_NICv2_lookup: dict[int, int | dict[int, int]] = {
5: 11,
6: 10,
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},
8: {"HPL": 25, "RCu": 10, "RCv": 15},
7: {"HPL": 185, "RCu": 93, "RCv": NA},
@@ -80,7 +94,13 @@ NUCp = {
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},
1: {"HVE": 10, "VVE": 15.2},
2: {"HVE": 3, "VVE": 4.5},
@@ -88,7 +108,13 @@ NUCv = {
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},
10: {"EPU": 10, "VEPU": 15},
9: {"EPU": 30, "VEPU": 45},
@@ -103,7 +129,13 @@ NACp = {
0: {"EPU": NA, "VEPU": NA},
}
-NACv = {
+
+class NACvEntry(TypedDict):
+ HFOMr: None | float
+ VFOMr: None | float
+
+
+NACv: dict[int, NACvEntry] = {
0: {"HFOMr": NA, "VFOMr": NA},
1: {"HFOMr": 10, "VFOMr": 15.2},
2: {"HFOMr": 3, "VFOMr": 4.5},
@@ -111,7 +143,13 @@ NACv = {
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},
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
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
11: {0: {"Rc": 7.5, "VPL": 11}},
10: {0: {"Rc": 25, "VPL": 37.5}},
@@ -135,7 +178,12 @@ NICv1 = {
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
11: {0: {"Rc": 7.5}},
10: {0: {"Rc": 25}},
diff --git a/pyModeS/decoder/uplink.py b/pyModeS/decoder/uplink.py
index 586ee56..fa7bf9d 100644
--- a/pyModeS/decoder/uplink.py
+++ b/pyModeS/decoder/uplink.py
@@ -1,8 +1,10 @@
-from pyModeS import common
+from typing import Optional
+from .. import common
+from textwrap import wrap
-def uplink_icao(msg):
- """Calculate the ICAO address from a Mode-S interrogation (uplink message)"""
+def uplink_icao(msg: str) -> str:
+ "Calculate the ICAO address from a Mode-S interrogation (uplink message)"
p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4)
data = int(msg[:-6], 16)
PA = int(msg[-6:], 16)
@@ -19,7 +21,203 @@ def uplink_icao(msg):
return "%06X" % (ad >> 2)
-def uf(msg):
+def uf(msg: str) -> int:
"""Decode Uplink Format value, bits 1 to 5."""
ufbin = common.hex2bin(msg[:2])
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,
+ }
diff --git a/pyModeS/extra/aero.py b/pyModeS/extra/aero.py
index 8d6a32c..5446192 100644
--- a/pyModeS/extra/aero.py
+++ b/pyModeS/extra/aero.py
@@ -17,7 +17,7 @@ Speed conversion at altitude H[m] in ISA
::
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]
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]
@@ -35,18 +35,18 @@ ft = 0.3048 # ft -> m
fpm = 0.00508 # ft/min -> m/s
inch = 0.0254 # inch -> m
sqft = 0.09290304 # 1 square foot
-nm = 1852.0 # nautical mile -> m
+nm = 1852 # nautical mile -> m
lbs = 0.453592 # pound -> kg
g0 = 9.80665 # m/s2, Sea level gravity constant
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
-p0 = 101325.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
T0 = 288.15 # K, temperature, sea level ISA
gamma = 1.40 # cp/cv for air
gamma1 = 0.2 # (gamma-1)/2 for air
gamma2 = 3.5 # gamma/(gamma-1) for air
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
-r_earth = 6371000.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)
@@ -94,8 +94,8 @@ def distance(lat1, lon1, lat2, lon2, H=0):
"""
# phi = 90 - latitude
- phi1 = np.radians(90.0 - lat1)
- phi2 = np.radians(90.0 - lat2)
+ phi1 = np.radians(90 - lat1)
+ phi2 = np.radians(90 - lat2)
# theta = longitude
theta1 = np.radians(lon1)
@@ -158,16 +158,16 @@ def tas2eas(Vtas, H):
def cas2tas(Vcas, H):
"""Calibrated Airspeed to True Airspeed"""
p, rho, T = atmos(H)
- qdyn = p0 * ((1.0 + rho0 * Vcas * Vcas / (7.0 * p0)) ** 3.5 - 1.0)
- Vtas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
+ qdyn = p0 * ((1 + rho0 * Vcas * Vcas / (7 * p0)) ** 3.5 - 1.0)
+ Vtas = np.sqrt(7 * p / rho * ((1 + qdyn / p) ** (2 / 7.0) - 1.0))
return Vtas
def tas2cas(Vtas, H):
"""True Airspeed to Calibrated Airspeed"""
p, rho, T = atmos(H)
- qdyn = p * ((1.0 + rho * Vtas * Vtas / (7.0 * p)) ** 3.5 - 1.0)
- Vcas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
+ qdyn = p * ((1 + rho * Vtas * Vtas / (7 * p)) ** 3.5 - 1.0)
+ Vcas = np.sqrt(7 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2 / 7.0) - 1.0))
return Vcas
diff --git a/pyModeS/extra/rtlreader.py b/pyModeS/extra/rtlreader.py
index 3c40ebc..2d099a2 100644
--- a/pyModeS/extra/rtlreader.py
+++ b/pyModeS/extra/rtlreader.py
@@ -1,8 +1,14 @@
+import time
import traceback
import numpy as np
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
smaples_per_microsec = 2
@@ -21,7 +27,7 @@ class RtlReader(object):
def __init__(self, **kwargs):
super(RtlReader, self).__init__()
self.signal_buffer = [] # amplitude of the sample only
- self.sdr = RtlSdr()
+ self.sdr = rtlsdr.RtlSdr()
self.sdr.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency
self.sdr.gain = "auto"
@@ -31,6 +37,8 @@ class RtlReader(object):
self.stop_flag = False
self.noise_floor = 1e6
+ self.exception_queue = None
+
def _calc_noise(self):
"""Calculate noise floor"""
window = smaples_per_microsec * 100
@@ -162,6 +170,7 @@ class RtlReader(object):
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self.raw_pipe_in = raw_pipe_in
+ self.exception_queue = exception_queue
self.stop_flag = stop_flag
try:
@@ -173,8 +182,8 @@ class RtlReader(object):
except Exception as e:
tb = traceback.format_exc()
- if exception_queue is not None:
- exception_queue.put(tb)
+ if self.exception_queue is not None:
+ self.exception_queue.put(tb)
raise e
diff --git a/pyModeS/extra/tcpclient.py b/pyModeS/extra/tcpclient.py
index 26be3be..9c90441 100644
--- a/pyModeS/extra/tcpclient.py
+++ b/pyModeS/extra/tcpclient.py
@@ -7,11 +7,6 @@ import pyModeS as pms
import traceback
import zmq
-if sys.version_info > (3, 0):
- PY_VERSION = 3
-else:
- PY_VERSION = 2
-
class TcpClient(object):
def __init__(self, host, port, datatype):
@@ -28,6 +23,8 @@ class TcpClient(object):
self.raw_pipe_in = None
self.stop_flag = False
+ self.exception_queue = None
+
def connect(self):
self.socket = zmq.Context().socket(zmq.STREAM)
self.socket.setsockopt(zmq.LINGER, 0)
@@ -35,7 +32,7 @@ class TcpClient(object):
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
def stop(self):
- self.socket.disconnect()
+ self.socket.close()
def read_raw_buffer(self):
""" 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):
self.raw_pipe_in = raw_pipe_in
+ self.exception_queue = exception_queue
self.stop_flag = stop_flag
self.connect()
@@ -262,9 +260,6 @@ class TcpClient(object):
try:
received = [i for i in self.socket.recv(4096)]
- if PY_VERSION == 2:
- received = [ord(i) for i in received]
-
self.buffer.extend(received)
# print(''.join(x.encode('hex') for x in self.buffer))
@@ -286,7 +281,8 @@ class TcpClient(object):
continue
except Exception as e:
tb = traceback.format_exc()
- exception_queue.put(tb)
+ if self.exception_queue is not None:
+ self.exception_queue.put(tb)
raise e
@@ -296,4 +292,7 @@ if __name__ == "__main__":
port = int(sys.argv[2])
datatype = sys.argv[3]
client = TcpClient(host=host, port=port, datatype=datatype)
- client.run()
+ try:
+ client.run()
+ finally:
+ client.stop()
diff --git a/pyModeS/py.typed b/pyModeS/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyModeS/py_common.py b/pyModeS/py_common.py
index 719f8a6..42673c0 100644
--- a/pyModeS/py_common.py
+++ b/pyModeS/py_common.py
@@ -5,14 +5,14 @@ from textwrap import wrap
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
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
return binstr
def hex2int(hexstr: str) -> int:
- """Convert a hexdecimal string to integer."""
+ """Convert a hexadecimal string to integer."""
return int(hexstr, 16)
@@ -22,7 +22,7 @@ def bin2int(binstr: str) -> int:
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))
@@ -199,7 +199,7 @@ def cprNL(lat: float) -> int:
nz = 15
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 = floor(nl)
return NL
@@ -234,7 +234,7 @@ def squawk(binstr: str) -> str:
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")
C1 = binstr[0]
@@ -296,7 +296,7 @@ def altitude(binstr: str) -> 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")
Mbit = binstr[6]
@@ -404,3 +404,85 @@ def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool:
return True
return False
+
+
+def fs(msg):
+ """Decode flight status for DF 4, 5, 20, and 21.
+
+ Args:
+ msg (str): 14 hexdigits string
+ Returns:
+ int, str: flight status, description
+
+ """
+ msgbin = hex2bin(msg)
+ fs = bin2int(msgbin[5:8])
+ text = None
+
+ if fs == 0:
+ text = "no alert, no SPI, aircraft is airborne"
+ elif fs == 1:
+ text = "no alert, no SPI, aircraft is on-ground"
+ elif fs == 2:
+ text = "alert, no SPI, aircraft is airborne"
+ elif fs == 3:
+ text = "alert, no SPI, aircraft is on-ground"
+ elif fs == 4:
+ text = "alert, SPI, aircraft is airborne or on-ground"
+ elif fs == 5:
+ text = "no alert, SPI, aircraft is airborne or on-ground"
+
+ return fs, text
+
+
+def dr(msg):
+ """Decode downlink request for DF 4, 5, 20, and 21.
+
+ Args:
+ msg (str): 14 hexdigits string
+ Returns:
+ int, str: downlink request, description
+
+ """
+ msgbin = hex2bin(msg)
+ dr = bin2int(msgbin[8:13])
+
+ text = None
+
+ if dr == 0:
+ text = "no downlink request"
+ elif dr == 1:
+ text = "request to send Comm-B message"
+ elif dr == 4:
+ text = "Comm-B broadcast 1 available"
+ elif dr == 5:
+ text = "Comm-B broadcast 2 available"
+ elif dr >= 16:
+ text = "ELM downlink segments available: {}".format(dr - 15)
+
+ return dr, text
+
+
+def um(msg):
+ """Decode utility message for DF 4, 5, 20, and 21.
+
+ Utility message contains interrogator identifier and reservation type.
+
+ Args:
+ msg (str): 14 hexdigits string
+ Returns:
+ int, str: interrogator identifier code that triggered the reply, and
+ reservation type made by the interrogator
+ """
+ msgbin = hex2bin(msg)
+ iis = bin2int(msgbin[13:17])
+ ids = bin2int(msgbin[17:19])
+ if ids == 0:
+ ids_text = None
+ if ids == 1:
+ ids_text = "Comm-B interrogator identifier code"
+ if ids == 2:
+ ids_text = "Comm-C interrogator identifier code"
+ if ids == 3:
+ ids_text = "Comm-D interrogator identifier code"
+ return iis, ids, ids_text
diff --git a/pyModeS/streamer/decode.py b/pyModeS/streamer/decode.py
index 5297262..e5ae584 100644
--- a/pyModeS/streamer/decode.py
+++ b/pyModeS/streamer/decode.py
@@ -1,5 +1,6 @@
import os
import time
+import traceback
import datetime
import csv
import pyModeS as pms
@@ -231,10 +232,13 @@ class Decode:
self.acs[icao]["t60"] = t
if ias60:
self.acs[icao]["ias"] = ias60
+ output_buffer.append([t, icao, "ias60", ias60])
if hdg60:
self.acs[icao]["hdg"] = hdg60
+ output_buffer.append([t, icao, "hdg60", hdg60])
if mach60:
self.acs[icao]["mach"] = mach60
+ output_buffer.append([t, icao, "mach60", mach60])
if roc60baro:
output_buffer.append([t, icao, "roc60baro", roc60baro])
diff --git a/pyModeS/streamer/modeslive b/pyModeS/streamer/modeslive
index 47e7b58..168d5c5 100755
--- a/pyModeS/streamer/modeslive
+++ b/pyModeS/streamer/modeslive
@@ -12,9 +12,6 @@ from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource
-# redirect all stdout to null, avoiding messing up with the screen
-sys.stdout = open(os.devnull, "w")
-
support_rawtypes = ["raw", "beast", "skysense"]
parser = argparse.ArgumentParser()
@@ -26,8 +23,9 @@ parser.add_argument(
)
parser.add_argument(
"--connect",
- help="Define server, port and data type. Supported data types are: %s"
- % support_rawtypes,
+ help="Define server, port and data type. Supported data types are: {}".format(
+ support_rawtypes
+ ),
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
@@ -86,6 +84,10 @@ if DUMPTO is not None:
sys.exit(1)
+# redirect all stdout to null, avoiding messing up with the screen
+sys.stdout = open(os.devnull, "w")
+
+
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
exception_queue = multiprocessing.Queue()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..57a8d3b
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+# https://github.com/embray/setup.cfg
+[metadata]
+license_file = LICENSE
\ No newline at end of file
diff --git a/setup.py b/setup.py
index fb41c83..8c1ddc3 100644
--- a/setup.py
+++ b/setup.py
@@ -11,6 +11,8 @@ Steps for deploying a new version:
4. twine upload dist/*
"""
+import sys
+
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
@@ -27,7 +29,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f:
details = dict(
name="pyModeS",
- version="2.8",
+ version="2.11",
description="Python Mode-S and ADS-B Decoder",
long_description=long_description,
url="https://github.com/junzis/pyModeS",
@@ -43,18 +45,57 @@ details = dict(
],
keywords="Mode-S ADS-B EHS ELS Comm-B",
packages=find_packages(exclude=["contrib", "docs", "tests"]),
- install_requires=["numpy", "pyzmq", "pyrtlsdr"],
- package_data={"pyModeS": ["*.pyx", "*.pxd"]},
+ # typing_extensions are no longer necessary after Python 3.8 (TypedDict)
+ 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"],
)
try:
- from setuptools.extension import Extension
+ from distutils.core import Extension
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)
diff --git a/tests/sample_run_adsb.py b/tests/sample_run_adsb.py
index f0d5134..d561a35 100644
--- a/tests/sample_run_adsb.py
+++ b/tests/sample_run_adsb.py
@@ -1,11 +1,7 @@
-import sys
-import time
import csv
+import time
-if len(sys.argv) > 1 and sys.argv[1] == "cython":
- from pyModeS.c_decoder import adsb
-else:
- from pyModeS.decoder import adsb
+from pyModeS.decoder import adsb
print("===== Decode ADS-B sample data=====")
diff --git a/tests/sample_run_commb.py b/tests/sample_run_commb.py
index 369f4e7..1162875 100644
--- a/tests/sample_run_commb.py
+++ b/tests/sample_run_commb.py
@@ -46,7 +46,7 @@ def bds_info(BDS, m):
)
else:
- info = None
+ info = []
return info
@@ -87,5 +87,5 @@ def commb_decode_all(df, n=None):
if __name__ == "__main__":
- commb_decode_all(df=20, n=100)
- commb_decode_all(df=21, n=100)
+ commb_decode_all(df=20, n=500)
+ commb_decode_all(df=21, n=500)
diff --git a/tests/test_adsb.py b/tests/test_adsb.py
index c6e6de0..442b755 100644
--- a/tests/test_adsb.py
+++ b/tests/test_adsb.py
@@ -43,14 +43,20 @@ def test_adsb_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)
- 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)
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)
@@ -76,14 +82,27 @@ def test_adsb_velocity():
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, 182.88, -832, "GS")
assert vas == (375, 243.98, -2304, "TAS")
- assert vgs_surface == (19.0, 42.2, 0, "GS")
+ assert vgs_surface == (19, 42.2, 0, "GS")
assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
def test_adsb_emergency():
assert not adsb.is_emergency("8DA2C1B6E112B600000000760759")
assert adsb.emergency_state("8DA2C1B6E112B600000000760759") == 0
- assert adsb.emergency_squawk("8DA2C1B6E112B600000000760759") == "6615"
+ 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():
diff --git a/tests/test_allcall.py b/tests/test_allcall.py
new file mode 100644
index 0000000..b10a839
--- /dev/null
+++ b/tests/test_allcall.py
@@ -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
diff --git a/tests/test_bds_inference.py b/tests/test_bds_inference.py
index 5ef67f0..4bb67c7 100644
--- a/tests/test_bds_inference.py
+++ b/tests/test_bds_inference.py
@@ -1,6 +1,12 @@
+import sys
+
+import pytest
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():
assert bds.infer("8D406B902015A678D4D220AA4BDA") == "BDS08"
assert bds.infer("8FC8200A3AB8F5F893096B000000") == "BDS06"
@@ -17,8 +23,8 @@ def test_bds_infer():
def test_bds_is50or60():
assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
- assert bds.is50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == "BDS50"
- assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == "BDS60"
+ assert bds.is50or60("A8001EBCFFFB23286004A73F6A5B", 320, 250, 14000) == "BDS50"
+ assert bds.is50or60("A8001EBCFE1B29287FDCA807BCFC", 320, 250, 14000) == "BDS50"
def test_surface_position():
diff --git a/tests/test_commb.py b/tests/test_commb.py
index 45cc1ec..d9936fe 100644
--- a/tests/test_commb.py
+++ b/tests/test_commb.py
@@ -1,4 +1,5 @@
from pyModeS import bds, commb
+import pytest
# from pyModeS import ehs, els # deprecated
@@ -23,7 +24,7 @@ def test_bds40_functions():
def test_bds50_functions():
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.gs50("A000139381951536E024D4CCF6B5") == 438
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
@@ -38,14 +39,16 @@ def test_bds50_functions():
def test_bds60_functions():
- assert bds.bds60.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
- 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
+ msg = "A00004128F39F91A7E27C46ADC21"
- assert commb.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
- assert commb.ias60("A00004128F39F91A7E27C46ADC21") == 252
- assert commb.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
- assert commb.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
- assert commb.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
+ assert bds.bds60.hdg60(msg) == pytest.approx(42.71484)
+ assert bds.bds60.ias60(msg) == 252
+ assert bds.bds60.mach60(msg) == 0.42
+ assert bds.bds60.vr60baro(msg) == -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
diff --git a/tests/test_surv.py b/tests/test_surv.py
new file mode 100644
index 0000000..0286e27
--- /dev/null
+++ b/tests/test_surv.py
@@ -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