Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dc74a5548 | ||
|
|
e7edba9e25 | ||
|
|
ce5adbafe5 | ||
|
|
b52431ca51 | ||
|
|
00f04a8886 | ||
|
|
0eb2d1f6a2 | ||
|
|
26ef5d3ad9 | ||
|
|
b90c2e740d | ||
|
|
e39baf38ed | ||
|
|
b9471d7fcb | ||
|
|
a350050e6e | ||
|
|
01574a9f01 | ||
|
|
cdb7aef82e | ||
|
|
a0a8c9b2f7 | ||
|
|
0b2648bfe0 | ||
|
|
69ce39ab39 | ||
|
|
83e22892ba | ||
|
|
7434fc9ed3 | ||
|
|
7260bff7e9 | ||
|
|
dd1fd596f8 | ||
|
|
b9089d55d2 | ||
|
|
6ab147bffe | ||
|
|
a308b9a7e0 | ||
|
|
96f49a00e4 | ||
|
|
b8f8f4dbc0 | ||
|
|
a4ce3bfaf1 | ||
|
|
3bb8c361e9 | ||
|
|
89e67fae31 | ||
|
|
50cef7d424 |
29
.github/workflows/pypi-publish.yml
vendored
Normal file
29
.github/workflows/pypi-publish.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# This workflows will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: PyPI Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
@@ -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 <https://www.tudelft.nl/en/>`_, `Aerospace Engineering Faculty <https://www.tudelft.nl/en/ae/>`_, `CNS/ATM research group <http://cs.lr.tudelft.nl/atm/>`_. It is supported by many `contributors <https://github.com/junzis/pyModeS/graphs/contributors>`_ from different institutions.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,7 @@ def tell(msg: str) -> None:
|
||||
tc = common.typecode(msg)
|
||||
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
|
||||
@@ -71,6 +71,68 @@ def tell(msg: str) -> None:
|
||||
_print("CPR Longitude", cprlon)
|
||||
_print("Altitude", alt, "feet")
|
||||
|
||||
if tc == 29: # target state and status
|
||||
_print("Type", "Target State and Status")
|
||||
subtype = common.bin2int((common.hex2bin(msg)[32:])[5:7])
|
||||
_print("Subtype", subtype)
|
||||
tcas_operational = adsb.tcas_operational(msg)
|
||||
types = {0: "Not Engaged", 1: "Engaged"}
|
||||
tcas_operational_types = {0: "Not Operational", 1: "Operational"}
|
||||
if subtype == 0:
|
||||
emergency_types = {
|
||||
0: "No emergency",
|
||||
1: "General emergency",
|
||||
2: "Lifeguard/medical emergency",
|
||||
3: "Minimum fuel",
|
||||
4: "No communications",
|
||||
5: "Unlawful interference",
|
||||
6: "Downed aircraft",
|
||||
7: "Reserved"
|
||||
}
|
||||
vertical_horizontal_types = {
|
||||
1: "Acquiring mode",
|
||||
2: "Capturing/Maintaining mode"
|
||||
}
|
||||
tcas_ra_types = {0: "Not active", 1: "Active"}
|
||||
alt, alt_source, alt_ref = adsb.target_altitude(msg)
|
||||
angle, angle_type, angle_source = adsb.target_angle(msg)
|
||||
vertical_mode = adsb.vertical_mode(msg)
|
||||
horizontal_mode = adsb.horizontal_mode(msg)
|
||||
tcas_ra = adsb.tcas_ra(msg)
|
||||
emergency_status = adsb.emergency_status(msg)
|
||||
_print("Target altitude", alt, "feet")
|
||||
_print("Altitude source", alt_source)
|
||||
_print("Altitude reference", alt_ref)
|
||||
_print("Angle", angle, "°")
|
||||
_print("Angle Type", angle_type)
|
||||
_print("Angle Source", angle_source)
|
||||
_print("Vertical mode", vertical_horizontal_types[vertical_mode])
|
||||
_print("Horizontal mode", vertical_horizontal_types[horizontal_mode])
|
||||
_print("TCAS/ACAS", tcas_operational_types[tcas_operational])
|
||||
_print("TCAS/ACAS RA", tcas_ra_types[tcas_ra])
|
||||
_print("Emergency status", emergency_types[emergency_status])
|
||||
else:
|
||||
alt, alt_source = adsb.selected_altitude(msg)
|
||||
baro = adsb.baro_pressure_setting(msg)
|
||||
hdg = adsb.selected_heading(msg)
|
||||
autopilot = adsb.autopilot(msg)
|
||||
vnav = adsb.vnav_mode(msg)
|
||||
alt_hold = adsb.altitude_hold_mode(msg)
|
||||
app = adsb.approach_mode(msg)
|
||||
lnav = adsb.lnav_mode(msg)
|
||||
_print("Selected altitude", alt, "feet")
|
||||
_print("Altitude source", alt_source)
|
||||
_print("Barometric pressure setting", baro, "millibars")
|
||||
_print("Selected Heading", hdg, "°")
|
||||
if not(common.bin2int((common.hex2bin(msg)[32:])[46]) == 0):
|
||||
_print("Autopilot", types[autopilot])
|
||||
_print("VNAV mode", types[vnav])
|
||||
_print("Altitude hold mode", types[alt_hold])
|
||||
_print("Approach mode", types[app])
|
||||
_print("TCAS/ACAS", tcas_operational_types[tcas_operational])
|
||||
_print("LNAV mode", types[lnav])
|
||||
|
||||
|
||||
if df == 20:
|
||||
_print("Protocol", "Mode-S Comm-B altitude reply")
|
||||
_print("Altitude", common.altcode(msg), "feet")
|
||||
|
||||
@@ -29,6 +29,23 @@ from pyModeS.decoder.bds.bds06 import (
|
||||
from pyModeS.decoder.bds.bds08 import category, callsign
|
||||
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
|
||||
from pyModeS.decoder.bds.bds61 import is_emergency, emergency_state, emergency_squawk
|
||||
from pyModeS.decoder.bds.bds62 import (
|
||||
selected_altitude,
|
||||
selected_heading,
|
||||
target_altitude,
|
||||
target_angle,
|
||||
tcas_operational,
|
||||
tcas_ra,
|
||||
baro_pressure_setting,
|
||||
vertical_mode,
|
||||
horizontal_mode,
|
||||
vnav_mode,
|
||||
lnav_mode,
|
||||
autopilot,
|
||||
altitude_hold_mode,
|
||||
approach_mode,
|
||||
emergency_status
|
||||
)
|
||||
|
||||
|
||||
def df(msg):
|
||||
@@ -92,7 +109,7 @@ def position_with_ref(msg, lat_ref, lon_ref):
|
||||
A reference position is required, which can be previously
|
||||
calculated location, ground station, or airport location.
|
||||
The function works with both airborne and surface position messages.
|
||||
The reference position shall be with in 180NM (airborne) or 45NM (surface)
|
||||
The reference position shall be within 180NM (airborne) or 45NM (surface)
|
||||
of the true position.
|
||||
|
||||
Args:
|
||||
@@ -146,15 +163,15 @@ def velocity(msg, source=False):
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
source (boolean): Include direction and vertical rate sources in return. Default to False.
|
||||
If set to True, the function will return six value instead of four.
|
||||
If set to True, the function will return six values instead of four.
|
||||
|
||||
Returns:
|
||||
int, float, int, string, [string], [string]: Four or six parameters, including:
|
||||
(int, float, int, string, [string], [string]): Four or six parameters, including:
|
||||
- Speed (kt)
|
||||
- Angle (degree), either ground track or heading
|
||||
- Vertical rate (ft/min)
|
||||
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
||||
- [Optional] Direction source ('TRUE_NORTH' or '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.
|
||||
@@ -324,7 +341,7 @@ def nic_v2(msg, NICa, NICbc):
|
||||
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 or string: Horizontal Radius of Containment
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Decode all-call reply messages, with dowlink format 11
|
||||
Decode all-call reply messages, with downlink format 11
|
||||
"""
|
||||
|
||||
from pyModeS import common
|
||||
|
||||
@@ -38,6 +38,7 @@ from pyModeS.decoder.bds import (
|
||||
bds50,
|
||||
bds53,
|
||||
bds60,
|
||||
bds62
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ def airborne_position(msg0, msg1, t0, t1):
|
||||
raise RuntimeError("Both even and odd CPR frames are required.")
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(mb0[22:39]) / 131072.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)
|
||||
@@ -64,13 +64,13 @@ def airborne_position(msg0, msg1, t0, t1):
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.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
|
||||
@@ -82,7 +82,7 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode airborne position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 180NM of the true position.
|
||||
be within 180NM of the true position.
|
||||
|
||||
Args:
|
||||
msg (str): even message (28 hexdigits)
|
||||
@@ -95,11 +95,11 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.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 +110,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
|
||||
@@ -143,9 +143,8 @@ def altitude(msg):
|
||||
|
||||
if tc < 19:
|
||||
altcode = altbin[0:6] + "0" + altbin[6:]
|
||||
alt = common.altitude(altcode)
|
||||
else:
|
||||
altcode = altbin[0:6] + "0" + altbin[6:]
|
||||
|
||||
alt = common.altitude(altcode)
|
||||
alt = common.bin2int(altbin) * 3.28084
|
||||
|
||||
return alt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ------------------------------------------
|
||||
# BDS 0,6
|
||||
# ADS-B TC=5-8
|
||||
# Surface movment
|
||||
# Surface movement
|
||||
# ------------------------------------------
|
||||
|
||||
from pyModeS import common
|
||||
@@ -27,13 +27,13 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
msgbin1 = common.hex2bin(msg1)
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.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 +43,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_s = lat_even_n - 90.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
|
||||
@@ -60,16 +60,16 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
nl = common.cprNL(lat_even)
|
||||
ni = max(common.cprNL(lat_even) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.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]
|
||||
@@ -86,7 +86,7 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode surface position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 45NM of the true position.
|
||||
be within 45NM of the true position.
|
||||
|
||||
Args:
|
||||
msg (str): even message (28 hexdigits)
|
||||
@@ -99,11 +99,11 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.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 +114,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
|
||||
@@ -133,7 +133,7 @@ def surface_velocity(msg, source=False):
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
source (boolean): Include direction and vertical rate sources in return. Default to False.
|
||||
If set to True, the function will return six value instead of four.
|
||||
If set to True, the function will return six values instead of four.
|
||||
|
||||
Returns:
|
||||
int, float, int, string, [string], [string]: Four or six parameters, including:
|
||||
@@ -153,7 +153,7 @@ def surface_velocity(msg, source=False):
|
||||
# ground track
|
||||
trk_status = int(mb[12])
|
||||
if trk_status == 1:
|
||||
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
|
||||
trk = common.bin2int(mb[13:20]) * 360 / 128
|
||||
trk = round(trk, 1)
|
||||
else:
|
||||
trk = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ------------------------------------------
|
||||
# BDS 0,8
|
||||
# ADS-B TC=1-4
|
||||
# Aircraft identitification and category
|
||||
# Aircraft identification and category
|
||||
# ------------------------------------------
|
||||
|
||||
from pyModeS import common
|
||||
|
||||
@@ -16,7 +16,7 @@ def airborne_velocity(msg, source=False):
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
source (boolean): Include direction and vertical rate sources in return. Default to False.
|
||||
If set to True, the function will return six value instead of four.
|
||||
If set to True, the function will return six values instead of four.
|
||||
|
||||
Returns:
|
||||
int, float, int, string, [string], [string]: Four or six parameters, including:
|
||||
@@ -24,7 +24,7 @@ def airborne_velocity(msg, source=False):
|
||||
- Angle (degree), either ground track or heading
|
||||
- Vertical rate (ft/min)
|
||||
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
||||
- [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
|
||||
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
|
||||
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
||||
|
||||
"""
|
||||
@@ -35,45 +35,54 @@ def airborne_velocity(msg, source=False):
|
||||
|
||||
subtype = common.bin2int(mb[5:8])
|
||||
|
||||
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
|
||||
return None
|
||||
|
||||
if subtype in (1, 2):
|
||||
v_ew_sign = -1 if mb[13] == "1" else 1
|
||||
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
|
||||
if subtype == 2: # Supersonic
|
||||
v_ew *= 4
|
||||
|
||||
v_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
|
||||
spd *= 4
|
||||
|
||||
@@ -82,7 +91,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
|
||||
@@ -96,7 +105,7 @@ def airborne_velocity(msg, source=False):
|
||||
|
||||
|
||||
def altitude_diff(msg):
|
||||
"""Decode the differece between GNSS and barometric altitude.
|
||||
"""Decode the difference between GNSS and barometric altitude.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string, TC=19
|
||||
|
||||
@@ -45,7 +45,7 @@ def cap17(msg):
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
list: list of support BDS codes
|
||||
list: list of supported BDS codes
|
||||
"""
|
||||
allbds = [
|
||||
"05",
|
||||
|
||||
@@ -24,9 +24,11 @@ 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
|
||||
|
||||
@@ -68,7 +68,7 @@ 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)
|
||||
|
||||
@@ -136,13 +136,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.
|
||||
"""Turbulence.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
@@ -78,7 +78,7 @@ def roll50(msg):
|
||||
if sign:
|
||||
value = value - 512
|
||||
|
||||
angle = value * 45.0 / 256.0 # degree
|
||||
angle = value * 45 / 256 # degree
|
||||
return round(angle, 1)
|
||||
|
||||
|
||||
@@ -102,7 +102,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:
|
||||
@@ -151,7 +151,7 @@ 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)
|
||||
|
||||
|
||||
|
||||
@@ -78,7 +78,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:
|
||||
|
||||
@@ -86,7 +86,7 @@ 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:
|
||||
@@ -158,7 +158,7 @@ def vr60baro(msg):
|
||||
|
||||
|
||||
def vr60ins(msg):
|
||||
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
|
||||
"""Vertical rate measured by onboard equipment (IRS, AHRS)
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
@@ -77,7 +77,7 @@ def emergency_squawk(msg: str) -> str:
|
||||
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
|
||||
|
||||
474
pyModeS/decoder/bds/bds62.py
Normal file
474
pyModeS/decoder/bds/bds62.py
Normal file
@@ -0,0 +1,474 @@
|
||||
# ------------------------------------------
|
||||
# BDS 6,2
|
||||
# ADS-B TC=29
|
||||
# Target State and Status
|
||||
# ------------------------------------------
|
||||
|
||||
from pyModeS import common
|
||||
|
||||
def selected_altitude(msg):
|
||||
"""Decode selected altitude.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Selected altitude (ft)
|
||||
string: Source ('MCP/FCU' or 'FMS')
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain selected altitude, use target altitude instead" % msg)
|
||||
|
||||
alt = common.bin2int(mb[9:20])
|
||||
alt = None if alt == 0 else (alt - 1) * 32
|
||||
alt_source = "MCP/FCU" if int(mb[8]) == 0 else "FMS"
|
||||
|
||||
return alt, alt_source
|
||||
|
||||
|
||||
|
||||
def target_altitude(msg):
|
||||
"""Decode target altitude.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Target altitude (ft)
|
||||
string: Source ('MCP/FCU', 'Holding mode' or 'FMS/RNAV')
|
||||
string: Altitude reference, either pressure altitude or barometric corrected altitude ('FL' or 'MSL')
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain target altitude, use selected altitude instead" % msg)
|
||||
|
||||
alt_avail = common.bin2int(mb[7:9])
|
||||
if alt_avail == 0:
|
||||
return None
|
||||
elif alt_avail == 1:
|
||||
alt_source = "MCP/FCU"
|
||||
elif alt_avail == 2:
|
||||
alt_source = "Holding mode"
|
||||
else:
|
||||
alt_source = "FMS/RNAV"
|
||||
|
||||
alt_ref = "FL" if int(mb[9]) == 0 else "MSL"
|
||||
|
||||
alt = -1000 + common.bin2int(mb[15:25]) * 100
|
||||
|
||||
return alt, alt_source, alt_ref
|
||||
|
||||
|
||||
def vertical_mode(msg):
|
||||
"""Decode vertical mode.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
1 "Acquiring" mode
|
||||
2 "Capturing" or "Maintaining" mode
|
||||
3 Reserved
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Vertical mode
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain vertical mode, use vnav mode instead" % msg)
|
||||
|
||||
vertical_mode = common.bin2int(mb[13:15])
|
||||
if vertical_mode == 0:
|
||||
return None
|
||||
|
||||
return vertical_mode
|
||||
|
||||
|
||||
def horizontal_mode(msg):
|
||||
"""Decode horizontal mode.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
1 "Acquiring" mode
|
||||
2 "Capturing" or "Maintaining" mode
|
||||
3 Reserved
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Horizontal mode
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain horizontal mode, use lnav mode instead" % msg)
|
||||
|
||||
horizontal_mode = common.bin2int(mb[25:27])
|
||||
if horizontal_mode == 0:
|
||||
return None
|
||||
|
||||
return horizontal_mode
|
||||
|
||||
|
||||
def selected_heading(msg):
|
||||
"""Decode selected heading.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
float: Selected heading (degree)
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain selected heading, use target angle instead" % msg)
|
||||
|
||||
if int(mb[29]) == 0:
|
||||
hdg = None
|
||||
else:
|
||||
hdg_sign = int(mb[30])
|
||||
hdg = (hdg_sign+1) * common.bin2int(mb[31:39]) * (180/256)
|
||||
hdg = round(hdg, 2)
|
||||
|
||||
return hdg
|
||||
|
||||
|
||||
def target_angle(msg):
|
||||
"""Decode target heading/track angle.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Target angle (degree)
|
||||
string: Angle type ('Heading' or 'Track')
|
||||
string: Source ('MCP/FCU', 'Autopilot Mode' or 'FMS/RNAV')
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain target angle, use selected heading instead" % msg)
|
||||
|
||||
angle_avail = common.bin2int(mb[25:27])
|
||||
if angle_avail == 0:
|
||||
angle = None
|
||||
else:
|
||||
angle = common.bin2int(mb[27:36])
|
||||
|
||||
if angle_avail == 1:
|
||||
angle_source = "MCP/FCU"
|
||||
elif angle_avail == 2:
|
||||
angle_source = "Autopilot mode"
|
||||
else:
|
||||
angle_source = "FMS/RNAV"
|
||||
|
||||
angle_type = "Heading" if int(mb[36]) else "Track"
|
||||
|
||||
return angle, angle_type, angle_source
|
||||
|
||||
|
||||
def baro_pressure_setting(msg):
|
||||
"""Decode barometric pressure setting.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: Barometric pressure setting (millibars)
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain barometric pressure setting" % msg)
|
||||
|
||||
baro = common.bin2int(mb[20:29])
|
||||
baro = None if baro == 0 else 800 + (baro - 1) * 0.8
|
||||
baro = round(baro, 1)
|
||||
|
||||
return baro
|
||||
|
||||
def autopilot(msg) -> bool:
|
||||
"""Decode autopilot engagement.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: Autopilot engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain autopilot engagement" % msg)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
autopilot = True if int(mb[47]) == 1 else False
|
||||
|
||||
return autopilot
|
||||
|
||||
def vnav_mode(msg) -> bool:
|
||||
"""Decode VNAV mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: VNAV mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain vnav mode, use vertical mode instead" % msg)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
vnav_mode = True if int(mb[48]) == 1 else False
|
||||
|
||||
return vnav_mode
|
||||
|
||||
|
||||
def altitude_hold_mode(msg) -> bool:
|
||||
"""Decode altitude hold mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: Altitude hold mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain altitude hold mode" % msg)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
alt_hold_mode = True if int(mb[49]) == 1 else False
|
||||
|
||||
return alt_hold_mode
|
||||
|
||||
|
||||
|
||||
def approach_mode(msg) -> bool:
|
||||
"""Decode approach mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: Approach mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain approach mode" % msg)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
app_mode = True if int(mb[51]) == 1 else False
|
||||
|
||||
return app_mode
|
||||
|
||||
|
||||
def lnav_mode(msg) -> bool:
|
||||
"""Decode LNAV mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: LNAV mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError("%s: ADS-B version 1 target state and status message does not contain lnav mode, use horizontal mode instead" % msg)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
lnav_mode = True if int(mb[53]) == 1 else False
|
||||
|
||||
return lnav_mode
|
||||
|
||||
|
||||
def tcas_operational(msg) -> bool:
|
||||
"""Decode TCAS/ACAS operational.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: TCAS/ACAS operational
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
tcas = True if int(mb[51]) == 0 else False
|
||||
else:
|
||||
tcas = True if int(mb[52]) == 1 else False
|
||||
|
||||
return tcas
|
||||
|
||||
def tcas_ra(msg) -> bool:
|
||||
"""Decode TCAS/ACAS Resolution advisory.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: TCAS/ACAS Resolution advisory active
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain TCAS/ACAS RA" % msg)
|
||||
|
||||
tcas_ra = True if int(mb[52]) == 1 else False
|
||||
|
||||
return tcas_ra
|
||||
|
||||
|
||||
def emergency_status(msg) -> int:
|
||||
"""Decode aircraft emergency status.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
0 No emergency
|
||||
1 General emergency
|
||||
2 Lifeguard/medical emergency
|
||||
3 Minimum fuel
|
||||
4 No communications
|
||||
5 Unlawful interference
|
||||
6 Downed aircraft
|
||||
7 Reserved
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Emergency status
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError("%s: ADS-B version 2 target state and status message does not contain emergency status" % msg)
|
||||
|
||||
return common.bin2int(mb[53:56])
|
||||
@@ -36,3 +36,5 @@ from pyModeS.decoder.bds.bds60 import *
|
||||
# MRAR and MHR
|
||||
from pyModeS.decoder.bds.bds44 import *
|
||||
from pyModeS.decoder.bds.bds45 import *
|
||||
|
||||
from pyModeS.py_common import fs, dr, um
|
||||
|
||||
@@ -3,6 +3,7 @@ Decode short roll call surveillance replies, with downlink format 4 or 5
|
||||
"""
|
||||
|
||||
from pyModeS import common
|
||||
from pyModeS.py_common import fs, dr, um
|
||||
|
||||
|
||||
def _checkdf(func):
|
||||
@@ -19,91 +20,6 @@ def _checkdf(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
@_checkdf
|
||||
def fs(msg):
|
||||
"""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 = 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
|
||||
|
||||
|
||||
@_checkdf
|
||||
def dr(msg):
|
||||
"""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 = 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
|
||||
|
||||
|
||||
@_checkdf
|
||||
def um(msg):
|
||||
"""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):
|
||||
"""Decode altitude.
|
||||
|
||||
@@ -43,12 +43,12 @@ def bds(msg):
|
||||
RRS = mbytes[2] & 0x0F
|
||||
BDS2 = RRS
|
||||
elif di == 3:
|
||||
RRS = ((mbytes[2] & 0x1) << 4) | ((mbytes[3] & 0xE0) >> 5)
|
||||
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
|
||||
BDS2 = RRS
|
||||
else:
|
||||
BDS2 = 0 # for other values of DI, the BDS2 is assumed 0 (as per ICAO Annex 10 Vol IV)
|
||||
|
||||
return str(BDS1) + str(BDS2)
|
||||
return str(format(BDS1,"X")) + str(format(BDS2,"X"))
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
@@ -154,8 +154,7 @@ def uplink_fields(msg):
|
||||
di = ""
|
||||
RR = ""
|
||||
RRS = ""
|
||||
BDS1 = ""
|
||||
BDS2 = ""
|
||||
BDS = ""
|
||||
if uf(msg) == 11:
|
||||
|
||||
|
||||
@@ -208,8 +207,11 @@ def uplink_fields(msg):
|
||||
# SI
|
||||
SI = (mbytes[2] >> 2) & 0x3F
|
||||
IC = "SI" + str(SI)
|
||||
RRS = ((mbytes[2] & 0x1) << 4) | ((mbytes[3] & 0xE0) >> 5)
|
||||
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,
|
||||
@@ -217,5 +219,5 @@ def uplink_fields(msg):
|
||||
"PR": PR,
|
||||
"RR": RR,
|
||||
"RRS": RRS,
|
||||
"BDS": str(BDS1) + str(BDS2),
|
||||
"BDS": BDS,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ try:
|
||||
import rtlsdr
|
||||
except:
|
||||
print("------------------------------------------------------------------------")
|
||||
print("! Warining: pyrtlsdr not installed (required for using RTL-SDR devices) ")
|
||||
print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !")
|
||||
print("------------------------------------------------------------------------")
|
||||
|
||||
sampling_rate = 2e6
|
||||
|
||||
@@ -32,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.
|
||||
@@ -292,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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
3
setup.cfg
Normal file
3
setup.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://github.com/embray/setup.cfg
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
4
setup.py
4
setup.py
@@ -27,7 +27,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f:
|
||||
|
||||
details = dict(
|
||||
name="pyModeS",
|
||||
version="2.8",
|
||||
version="2.10",
|
||||
description="Python Mode-S and ADS-B Decoder",
|
||||
long_description=long_description,
|
||||
url="https://github.com/junzis/pyModeS",
|
||||
@@ -45,7 +45,7 @@ details = dict(
|
||||
packages=find_packages(exclude=["contrib", "docs", "tests"]),
|
||||
install_requires=["numpy", "pyzmq"],
|
||||
extras_require={"fast": ["Cython"]},
|
||||
package_data={"pyModeS": ["*.pyx", "*.pxd"]},
|
||||
package_data={"pyModeS": ["*.pyx", "*.pxd", "py.typed"]},
|
||||
scripts=["pyModeS/streamer/modeslive"],
|
||||
)
|
||||
|
||||
|
||||
@@ -86,6 +86,20 @@ def test_adsb_emergency():
|
||||
assert adsb.emergency_squawk("8DA2C1B6E112B600000000760759") == "6615"
|
||||
|
||||
|
||||
def test_adsb_target_state_status():
|
||||
sel_alt = adsb.selected_altitude("8DA05629EA21485CBF3F8CADAEEB")
|
||||
assert sel_alt == (16992, "MCP/FCU")
|
||||
assert adsb.baro_pressure_setting("8DA05629EA21485CBF3F8CADAEEB") == 1012.8
|
||||
assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB")== 66.8
|
||||
assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") == True
|
||||
assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") == True
|
||||
assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") == False
|
||||
assert adsb.approach_mode("8DA05629EA21485CBF3F8CADAEEB") == False
|
||||
assert adsb.tcas_operational("8DA05629EA21485CBF3F8CADAEEB") == True
|
||||
assert adsb.lnav_mode("8DA05629EA21485CBF3F8CADAEEB") == True
|
||||
|
||||
|
||||
|
||||
# def test_nic():
|
||||
# assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
|
||||
# assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
|
||||
|
||||
@@ -6,7 +6,7 @@ def test_icao():
|
||||
|
||||
|
||||
def test_interrogator():
|
||||
assert allcall.interrogator("5D484FDEA248F5") == 22
|
||||
assert allcall.interrogator("5D484FDEA248F5") == "SI6"
|
||||
|
||||
|
||||
def test_capability():
|
||||
|
||||
Reference in New Issue
Block a user