From 7348a10f1b365a683d6e04c95e6cc1508e4f5101 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 10 Jun 2020 00:07:26 +0200 Subject: [PATCH 01/64] minor bug fix --- pyModeS/c_common.pyx | 2 +- pyModeS/py_common.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyModeS/c_common.pyx b/pyModeS/c_common.pyx index e774586..18c1768 100644 --- a/pyModeS/c_common.pyx +++ b/pyModeS/c_common.pyx @@ -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/py_common.py b/pyModeS/py_common.py index 719f8a6..4310ac3 100644 --- a/pyModeS/py_common.py +++ b/pyModeS/py_common.py @@ -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] From 0d7f628f3c3ed8d70fd7f0dbdc766019487a6008 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Mon, 13 Jul 2020 18:52:42 +0200 Subject: [PATCH 02/64] fix modeslive --help bug --- pyModeS/streamer/modeslive | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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() From 1f15a953e38605d876fcba31801c5a890acc1440 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 29 Jul 2020 00:53:05 +0200 Subject: [PATCH 03/64] minor fix in surface speed calculation --- pyModeS/decoder/bds/bds06.py | 12 +++++------- tests/test_adsb.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pyModeS/decoder/bds/bds06.py b/pyModeS/decoder/bds/bds06.py index 2c366ce..bd7a8f8 100644 --- a/pyModeS/decoder/bds/bds06.py +++ b/pyModeS/decoder/bds/bds06.py @@ -168,13 +168,11 @@ def surface_velocity(msg, source=False): elif mov == 124: spd = 175 else: - movs = [2, 9, 13, 39, 94, 109, 124] - kts = [0.125, 1, 2, 15, 70, 100, 175] - i = next(m[0] for m in enumerate(movs) if m[1] > mov) - step = (kts[i] - kts[i - 1]) * 1.0 / (movs[i] - movs[i - 1]) - spd = kts[i - 1] + (mov - movs[i - 1]) * step - spd = round(spd, 2) - + mov_lb = [2, 9, 13, 39, 94, 109, 124] + kts_lb = [0.125, 1, 2, 15, 70, 100, 175] + step = [0.125, 0.25, 0.5, 1, 2, 5] + i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov) + spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1] if source: return spd, trk, 0, "GS", "TRUE_NORTH", None else: diff --git a/tests/test_adsb.py b/tests/test_adsb.py index c6e6de0..2b8b631 100644 --- a/tests/test_adsb.py +++ b/tests/test_adsb.py @@ -76,7 +76,7 @@ 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 From 93fc53692620018f3736e0c8a7cf967ac52cd3ff Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 5 Aug 2020 18:38:12 +0200 Subject: [PATCH 04/64] improve BDS 50 and 60 inference --- pyModeS/decoder/bds/__init__.py | 22 ++++++++++++++++------ pyModeS/decoder/bds/bds50.py | 2 +- pyModeS/decoder/bds/bds60.py | 8 ++++++++ tests/sample_run_commb.py | 6 +++--- tests/test_bds_inference.py | 4 ++-- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py index f3ed2d2..7ace4c2 100644 --- a/pyModeS/decoder/bds/__init__.py +++ b/pyModeS/decoder/bds/__init__.py @@ -60,25 +60,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) @@ -98,6 +107,7 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref): try: dist = np.linalg.norm(X - Mu, axis=1) BDS = allbds[np.nanargmin(dist)] + print(dist, BDS) except ValueError: return "BDS50,BDS60" diff --git a/pyModeS/decoder/bds/bds50.py b/pyModeS/decoder/bds/bds50.py index a2e534a..e20bd38 100644 --- a/pyModeS/decoder/bds/bds50.py +++ b/pyModeS/decoder/bds/bds50.py @@ -40,7 +40,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) diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py index b8d91cb..e5749cd 100644 --- a/pyModeS/decoder/bds/bds60.py +++ b/pyModeS/decoder/bds/bds60.py @@ -4,6 +4,7 @@ # ------------------------------------------ from pyModeS import common +from pyModeS.extra import aero def is60(msg): @@ -54,6 +55,13 @@ 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) + ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts + if abs(ias - ias_) > 20: + return False + return True 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_bds_inference.py b/tests/test_bds_inference.py index 5ef67f0..c5c849f 100644 --- a/tests/test_bds_inference.py +++ b/tests/test_bds_inference.py @@ -17,8 +17,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(): From 2bdd638cefd1f0c65ffe2a4f3513ab9e20fd14aa Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Fri, 14 Aug 2020 23:50:54 +0200 Subject: [PATCH 05/64] bug fix in is60() --- pyModeS/decoder/bds/bds60.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py index e5749cd..74255db 100644 --- a/pyModeS/decoder/bds/bds60.py +++ b/pyModeS/decoder/bds/bds60.py @@ -58,9 +58,10 @@ def is60(msg): # additional check knowing altitude if (mach is not None) and (ias is not None) and (common.df(msg) == 20): alt = common.altcode(msg) - ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts - if abs(ias - ias_) > 20: - return False + if alt is not None: + ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts + if abs(ias - ias_) > 20: + return False return True From 0ced97e3eb746a58dfe5039834d8b6be80135de3 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Mon, 17 Aug 2020 00:53:49 +0200 Subject: [PATCH 06/64] fix exception buffer bug --- pyModeS/extra/rtlreader.py | 7 +++++-- pyModeS/extra/tcpclient.py | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyModeS/extra/rtlreader.py b/pyModeS/extra/rtlreader.py index 3c40ebc..d14f270 100644 --- a/pyModeS/extra/rtlreader.py +++ b/pyModeS/extra/rtlreader.py @@ -31,6 +31,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 +164,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 +176,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..b9e1ca4 100644 --- a/pyModeS/extra/tcpclient.py +++ b/pyModeS/extra/tcpclient.py @@ -28,6 +28,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) @@ -255,6 +257,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() @@ -286,7 +289,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 From 7a4c465f7de8aeb60be1a59d7f9e9426443aefe9 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Mon, 17 Aug 2020 01:19:16 +0200 Subject: [PATCH 07/64] remove mandatory pyrtlsdr dependency --- pyModeS/extra/rtlreader.py | 12 +++++++++--- pyModeS/extra/tcpclient.py | 8 -------- setup.py | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pyModeS/extra/rtlreader.py b/pyModeS/extra/rtlreader.py index d14f270..a077a7e 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 +except: + print("------------------------------------------------------------------------") + print("! Warining: 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" diff --git a/pyModeS/extra/tcpclient.py b/pyModeS/extra/tcpclient.py index b9e1ca4..ee66ea0 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): @@ -265,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)) diff --git a/setup.py b/setup.py index fb41c83..2279735 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ details = dict( ], keywords="Mode-S ADS-B EHS ELS Comm-B", packages=find_packages(exclude=["contrib", "docs", "tests"]), - install_requires=["numpy", "pyzmq", "pyrtlsdr"], + install_requires=["numpy", "pyzmq"], package_data={"pyModeS": ["*.pyx", "*.pxd"]}, scripts=["pyModeS/streamer/modeslive"], ) From 7e966090ab04d4b076ac365c71bbd4766458d4ce Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Thu, 24 Sep 2020 22:13:13 +0200 Subject: [PATCH 08/64] update meteorological reports decoding example --- README.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 4731b59..fb2d3d6 100644 --- a/README.rst +++ b/README.rst @@ -261,8 +261,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 infer the BDS 4,4 and BDS 4,5 codes, you must set `mrar` argumetn to true in the inference function: + +.. code:: python + + pms.bds.infer(msg. mrar=True) + +Once the correct MRAR or MHR messages are identified, use the following decoding functions. + + +Meteorological routine air report (MRAR) ++++++++++++++++++++++++++++++++++++++++++ .. code:: python @@ -273,8 +285,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 From dd3a869c08a43c1757039429d4f23228a2eda9bd Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Thu, 24 Sep 2020 22:16:08 +0200 Subject: [PATCH 09/64] fix typos --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index fb2d3d6..9390673 100644 --- a/README.rst +++ b/README.rst @@ -264,13 +264,13 @@ Mode-S Enhanced Surveillance (EHS) Meteorological reports [Experimental] ************************************** -To infer the BDS 4,4 and BDS 4,5 codes, you must set `mrar` argumetn to true in the inference function: +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 or MHR messages are identified, use the following decoding functions. +Once the correct MRAR and MHR messages are identified, decode them as follows: Meteorological routine air report (MRAR) From c3976f1ca3bd0c6b53c67ff6f68214eeac4cbafa Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Fri, 16 Oct 2020 22:32:03 +0200 Subject: [PATCH 10/64] update cap17() --- pyModeS/decoder/bds/bds17.py | 10 +++------- pyModeS/decoder/bds/bds40.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pyModeS/decoder/bds/bds17.py b/pyModeS/decoder/bds/bds17.py index cb5270a..80210f1 100644 --- a/pyModeS/decoder/bds/bds17.py +++ b/pyModeS/decoder/bds/bds17.py @@ -21,7 +21,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) @@ -72,14 +72,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/bds40.py b/pyModeS/decoder/bds/bds40.py index bc4950e..5a145e7 100644 --- a/pyModeS/decoder/bds/bds40.py +++ b/pyModeS/decoder/bds/bds40.py @@ -117,4 +117,4 @@ def alt40fms(msg): "alt40fms() has been renamed to selalt40fms(). It will be removed in the future.", DeprecationWarning, ) - return selalt40mcp(msg) + return selalt40fms(msg) From 224a0b8a67ce75221d4e594e41a4c77bd4ee32df Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 4 Nov 2020 17:21:07 +0100 Subject: [PATCH 11/64] remove print statement --- pyModeS/decoder/bds/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py index 7ace4c2..84f428e 100644 --- a/pyModeS/decoder/bds/__init__.py +++ b/pyModeS/decoder/bds/__init__.py @@ -107,7 +107,6 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref): try: dist = np.linalg.norm(X - Mu, axis=1) BDS = allbds[np.nanargmin(dist)] - print(dist, BDS) except ValueError: return "BDS50,BDS60" From 84c62555674445ed43181b8c3cca998e2fd82b19 Mon Sep 17 00:00:00 2001 From: boringow <62092582+boringow@users.noreply.github.com> Date: Tue, 10 Nov 2020 20:44:32 +0100 Subject: [PATCH 12/64] Update allcall.py Tested, it is all functioning well, I created a new decoder since I couldn't understand yours. It may not be the most optimum, I am quite new into coding so it may no be optimal, but for sure it is working :) Hope you like it! --- pyModeS/decoder/allcall.py | 53 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py index 6e296ca..ebd132c 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -1,5 +1,54 @@ +from pyModeS import common """ Decoding all call replies DF=11 - -[To be implemented] """ + + +def ipcode(msg): + '''Returns the IP code of the Mode S all-call reply (DF11)''' + binaryraw=common.hex2bin(msg) + PI=binaryraw[-24:] + DATA=binaryraw[:-24] + + Gx='1111111111111010000001001000' + Mx=DATA[::-1].zfill(len(DATA)+24)[::-1] + initialdifference=len(Mx)-len(Gx) + Gx = Gx[::-1].zfill(len(Mx))[::-1] + while len(Mx)>initialdifference: + MSB=Mx[0] + if MSB=='1': + result=int(Mx,2)^int(Gx,2) + Mx = str(bin(result)[2:]) + Gx = Gx.rstrip('0')[::-1].zfill(len(Mx))[::-1] + + else: #If the Mx starts with a 0 + Mx=Mx[1:] + Gx=Gx[:-1] + + SIcode=(int(Mx,2)^int(PI,2)) #Mx is the parity sequence and PI is the last field of the DF11. + return SIcode + + +def capability(msg): + '''return the capability code''' + if common.df(msg) == 11:#check that the msg is DF11 + binaryraw=common.hex2bin(msg) + CA = (int(binaryraw[5:8],2)) + if CA == 0: #level 1 transponder + return 0 + if CA in [1, 2, 3]: #reserved + return None + if CA == 4:#level 2 transponder, ability to set CA to 7 and on ground + return 4 + if CA == 5:#level 2 transponder, ability to set CA to 7 and airborn + return 5 + if CA == 6:#level 2 transponder, ability to set CA to 7 and either airborn on ground + return 6 + if CA == 7:# (either DR field is 0 or FS field is 2,3,4 or 5), and either airborn on ground + return 7 + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def AA(msg): + '''returns the icao code of the aircraft''' + return (msg[2:8]) From fc135eacef7793491cc26b6256aa7ca930209c31 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Wed, 11 Nov 2020 00:08:30 +0100 Subject: [PATCH 13/64] Update uplink.py additional decoded fields added --- pyModeS/decoder/uplink.py | 204 +++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/uplink.py b/pyModeS/decoder/uplink.py index 586ee56..d1a6f70 100644 --- a/pyModeS/decoder/uplink.py +++ b/pyModeS/decoder/uplink.py @@ -1,4 +1,5 @@ from pyModeS import common +from textwrap import wrap def uplink_icao(msg): @@ -21,5 +22,204 @@ def uplink_icao(msg): def uf(msg): """Decode Uplink Format value, bits 1 to 5.""" - ufbin = common.hex2bin(msg[:2]) - return min(common.bin2int(ufbin[0:5]), 24) + dfbin = common.hex2bin(msg[:2]) + return min(common.bin2int(dfbin[0:5]), 24) + + +def bds(msg): + """Decode requested BDS register from selective (Roll Call) interrogation.""" + UF = uf(msg) + msgbin = common.hex2bin(msg) + msgbin_split = wrap(msgbin, 8) + mbytes = list(map(common.bin2int, msgbin_split)) + + if uf(msg) in {4, 5, 20, 21}: + # Decode the SI or II interrogator code + # 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 + if di == 7: + RRS = mbytes[2] & 0x0F + BDS2 = RRS + elif di == 3: + RRS = ((mbytes[2] & 0x1) << 4) | ((mbytes[3] & 0xE0) >> 5) + BDS2 = RRS + else: + BDS2 = 0 + + return str(BDS1) + str(BDS2) + else: + return None + else: + return None + + +def pr(msg): + """Decode PR (probability of reply) field from All Call interrogation. + Interpretation: + 0 signifies reply with probability of 1 + 1 signifies reply with probability of 1/2 + 2 signifies reply with probability of 1/4 + 3 signifies reply with probability of 1/8 + 4 signifies reply with probability of 1/16 + 5, 6, 7 not assigned + 8 signifies disregard lockout, reply with probability of 1 + 9 signifies disregard lockout, reply with probability of 1/2 + 10 signifies disregard lockout, reply with probability of 1/4 + 11 signifies disregard lockout, reply with probability of 1/8 + 12 signifies disregard lockout, reply with probability of 1/16 + 13, 14, 15 not assigned. + """ + msgbin = common.hex2bin(msg) + msgbin_split = wrap(msgbin, 8) + mbytes = list(map(common.bin2int, msgbin_split)) + if uf(msg) == 11: + return ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7) + else: + return None + + +def ic(msg): + """Decode IC (interrogator code) from a ground-based interrogation.""" + + UF = uf(msg) + msgbin = common.hex2bin(msg) + msgbin_split = wrap(msgbin, 8) + mbytes = list(map(common.bin2int, msgbin_split)) + IC = None + BDS2 = "" + if uf(msg) == 11: + + codeLabel = mbytes[1] & 0x7 + icField = (mbytes[1] >> 3) & 0xF + + # Store the Interogator Code + ic_switcher = { + 0: "II" + str(icField), + 1: "SI" + str(icField), + 2: "SI" + str(icField + 16), + 3: "SI" + str(icField + 32), + 4: "SI" + str(icField + 48), + } + IC = ic_switcher.get(codeLabel, "") + + if uf(msg) in {4, 5, 20, 21}: + di = mbytes[1] & 0x7 + RR = mbytes[1] >> 3 & 0x1F + if RR > 15: + BDS1 = RR - 16 + if di == 0 or di == 1 or di == 7: + # II + II = (mbytes[2] >> 4) & 0xF + IC = "II" + str(II) + elif di == 3: + # SI + SI = (mbytes[2] >> 2) & 0x3F + IC = "SI" + str(SI) + return IC + + +def lockout(msg): + """Decode the lockout command from selective (Roll Call) interrogation.""" + msgbin = common.hex2bin(msg) + msgbin_split = wrap(msgbin, 8) + mbytes = list(map(common.bin2int, msgbin_split)) + + if uf(msg) in {4, 5, 20, 21}: + lockout = False + di = mbytes[1] & 0x7 + if di == 7: + # LOS + if ((mbytes[3] & 0x40) >> 6) == 1: + lockout = True + elif di == 3: + # LSS + if ((mbytes[2] & 0x2) >> 1) == 1: + lockout = True + return lockout + else: + return None + + +def uplink_fields(msg): + """Decode individual fields of a ground-based interrogation.""" + msgbin = common.hex2bin(msg) + msgbin_split = wrap(msgbin, 8) + mbytes = list(map(common.bin2int, msgbin_split)) + PR = "" + LOS = "" + IC = "" + lockout = False + di = "" + RR = "" + RRS = "" + BDS1 = "" + BDS2 = "" + if uf(msg) == 11: + # Decode the SI or II interrogator code + # Get cl and ic bit fields from the data + + # Probability of Reply decoding + + PR = ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7) + + 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 SI or II interrogator code + # 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) << 4) | ((mbytes[3] & 0xE0) >> 5) + BDS2 = RRS + return { + "DI": di, + "IC": IC, + "LOS": lockout, + "PR": PR, + "RR": RR, + "RRS": RRS, + "BDS": str(BDS1) + str(BDS2), + } From beb50fc788c6ac697d5c554e4a7be0178b3cdda2 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Wed, 11 Nov 2020 00:12:47 +0100 Subject: [PATCH 14/64] Update uplink.py --- pyModeS/decoder/uplink.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/uplink.py b/pyModeS/decoder/uplink.py index d1a6f70..bfcfb15 100644 --- a/pyModeS/decoder/uplink.py +++ b/pyModeS/decoder/uplink.py @@ -22,8 +22,8 @@ def uplink_icao(msg): def uf(msg): """Decode Uplink Format value, bits 1 to 5.""" - dfbin = common.hex2bin(msg[:2]) - return min(common.bin2int(dfbin[0:5]), 24) + ufbin = common.hex2bin(msg[:2]) + return min(common.bin2int(ufbin[0:5]), 24) def bds(msg): From 35c0bfa5137c3828341280e33ece3d33334dbefc Mon Sep 17 00:00:00 2001 From: boringow <62092582+boringow@users.noreply.github.com> Date: Thu, 12 Nov 2020 14:46:33 +0100 Subject: [PATCH 15/64] Update allcall.py --- pyModeS/decoder/allcall.py | 75 +++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py index ebd132c..4e9fdf2 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -3,31 +3,61 @@ from pyModeS import common Decoding all call replies DF=11 """ - def ipcode(msg): - '''Returns the IP code of the Mode S all-call reply (DF11)''' - binaryraw=common.hex2bin(msg) - PI=binaryraw[-24:] - DATA=binaryraw[:-24] + if common.df(msg) == 11: # check that the msg is DF11 + '''Returns the IP code of the Mode S all-call reply (DF11)''' + binaryraw=common.hex2bin(msg) + PI=binaryraw[-24:] + DATA=binaryraw[:-24] - Gx='1111111111111010000001001000' - Mx=DATA[::-1].zfill(len(DATA)+24)[::-1] - initialdifference=len(Mx)-len(Gx) - Gx = Gx[::-1].zfill(len(Mx))[::-1] - while len(Mx)>initialdifference: - MSB=Mx[0] - if MSB=='1': - result=int(Mx,2)^int(Gx,2) - Mx = str(bin(result)[2:]) - Gx = Gx.rstrip('0')[::-1].zfill(len(Mx))[::-1] + Gx='1111111111111010000001001000' + Mx=DATA[::-1].zfill(len(DATA)+24)[::-1] + initialdifference=len(Mx)-len(Gx) + Gx = Gx[::-1].zfill(len(Mx))[::-1] + while len(Mx)>initialdifference: + MSB=Mx[0] + if MSB=='1': + result=int(Mx,2)^int(Gx,2) + Mx = str(bin(result)[2:]) + Gx = Gx.rstrip('0')[::-1].zfill(len(Mx))[::-1] - else: #If the Mx starts with a 0 - Mx=Mx[1:] - Gx=Gx[:-1] + else: #If the Mx starts with a 0 + Mx=Mx[1:] + Gx=Gx[:-1] - SIcode=(int(Mx,2)^int(PI,2)) #Mx is the parity sequence and PI is the last field of the DF11. - return SIcode + SIcode=bin(int(Mx,2)^int(PI,2)).zfill(7) #Mx is the parity sequence and PI is the last field of the DF11. + if int(SIcode[0:3],2)>0: + return str("SI" + str(int(SIcode[3:],2))) + else: + return str("II" + str(int(SIcode,2))) + else: + raise RuntimeError("Incorrect or inconsistent message types") +def flight_status(msg): + '''returns the flight status''' + if common.df(msg) == 11: # check that the msg is DF11 + binaryraw=common.hex2bin(msg) + CA = (int(binaryraw[5:8],2)) + if CA in [0, 1, 2, 3, 6, 7]: #reserved + return None + if CA == 4:#level 2 transponder, ability to set CA to 7 and on ground + return "on ground" + if CA == 5:#level 2 transponder, ability to set CA to 7 and airborn + return "airborne" + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def transponder_level(msg): + '''returns the transponder level''' + if common.df(msg) == 11:#check that the msg is DF11 + binaryraw=common.hex2bin(msg) + CA = (int(binaryraw[5:8],2)) + if CA in [1, 2, 3, 4, 5, 6, 7]: #reserved + return 2 + if CA == 0: + return 1 + else: + raise RuntimeError("Incorrect or inconsistent message types") def capability(msg): '''return the capability code''' @@ -51,4 +81,7 @@ def capability(msg): def AA(msg): '''returns the icao code of the aircraft''' - return (msg[2:8]) + if common.df(msg) == 11: # check that the msg is DF11 + return (msg[2:8]) + else: + raise RuntimeError("Incorrect or inconsistent message types") From 4ba900f500f91ddfe53c098ed9ece6df8942fe12 Mon Sep 17 00:00:00 2001 From: boringow <62092582+boringow@users.noreply.github.com> Date: Thu, 12 Nov 2020 14:49:52 +0100 Subject: [PATCH 16/64] Update allcall.py --- pyModeS/decoder/allcall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py index 4e9fdf2..9ecd194 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -3,7 +3,7 @@ from pyModeS import common Decoding all call replies DF=11 """ -def ipcode(msg): +def interrogator_code(msg): if common.df(msg) == 11: # check that the msg is DF11 '''Returns the IP code of the Mode S all-call reply (DF11)''' binaryraw=common.hex2bin(msg) From eacc828b75a19ed62cf7df072e610308d3dbc893 Mon Sep 17 00:00:00 2001 From: boringow <62092582+boringow@users.noreply.github.com> Date: Sat, 14 Nov 2020 09:37:08 +0100 Subject: [PATCH 17/64] Update surv.py I have completed the allcall.py and the surv.py, but I can not commit the two files at the same time. Hope you like how it looks, If necessary, tell me what could I change. Have a nice weekend! --- pyModeS/decoder/surv.py | 117 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/pyModeS/decoder/surv.py b/pyModeS/decoder/surv.py index 746f47f..abda10d 100644 --- a/pyModeS/decoder/surv.py +++ b/pyModeS/decoder/surv.py @@ -1,5 +1,120 @@ """ Warpper for short roll call surveillance replies DF=4/5 -[To be implemented] """ + +from pyModeS import common + +def fs(msg): + '''tell the flight status of the aircraft, returns airborne, ground or None''' + if common.df(msg) in [4,5]: + binaryraw=common.hex2bin(msg) + fs=int(binaryraw[5:8],2) + if fs in [0,2]: + return "airborne" + if fs in [1,3]: + return "ground" + else: + return None + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def alert(msg): + '''returns True if there is an alert, False otherwise ''' + #The alert indicates that the TXPPR Mode A code is changed manually by the pilot. Icao Annex10, Vol IV, 3.1.2.6.10.1.1 + if common.df(msg) in [4,5]: + binaryraw = common.hex2bin(msg) + fs = int(binaryraw[5:8], 2) + if fs in [2,3,4]: + return True + if fs in [0,1,5]: + return False + else: + return None + else: + raise RuntimeError("Incorrect or inconsistent message types") + + +def SPI(msg): + '''returns True if there there is Special Pulse Indicator, False otherwise ''' + if common.df(msg) in [4, 5]: + binaryraw = common.hex2bin(msg) + fs = int(binaryraw[5:8], 2) + if fs in [4, 5]: + return True + if fs in [0, 1, 2, 3]: + return False + else: + return None + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def dr(msg): + '''Returns Downlink Request Status. + Args: + msg (str): 14 hexdigits string, + Returns: + str: Downlink Request Status + ''' + if common.df(msg) in [4,5]: + binaryraw = common.hex2bin(msg) + dr = int(binaryraw[8:13], 2) + if dr in [2,3,6,7]: + return "ACAS" + if dr == 0: + return "no downlink request" + if dr == 1: + return "request to send Comm-B message" + if dr in 4: + return "Comm-B broadcast 1 available" + if dr in 5: + return "Comm-B broadcast 2 available" + if dr > 7 and dr < 16: + return None + else: + return str(15-dr)+" downlink ELM segments available" + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def um(msg): + '''returns the Interrogator Identifier and the type of reservation + Args: + msg (str): 14 hexdigits string, + Returns: + int: II code of the identifier that triggered the reply + str: type of reservation made by the interrogator + ''' + if common.df(msg) in [4,5]: + binaryraw = common.hex2bin(msg) + iis = int(binaryraw[13:17], 2) + ds = int(binaryraw[17:19], 2) + if ds == 0: + ds=None + if ds == 1: + ds='Comm-B' + if ds == 2: + ds='Comm-C' + if ds == 3: + ds='Comm-D' + return iis, ds + + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def ac(msg): + '''return the altitude in ft''' + if common.df(msg) == 4: + binaryraw = common.hex2bin(msg) + altcode = binaryraw[19:32] # Here we could also put the common.altcode(msg). + return common.altitude(altcode) + else: + raise RuntimeError("Incorrect or inconsistent message types") + +def id(msg): + '''return the squawk code (identifier)''' + if common.df(msg) == 5: + binaryraw = common.hex2bin(msg) + altcode = binaryraw[19:32] # Here we could also put the common.idcode(msg). + return common.squawk(altcode) + else: + raise RuntimeError("Incorrect or inconsistent message types") From 668d6580d025b2df436baacd62089a19034484df Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 14 Nov 2020 21:57:08 +0100 Subject: [PATCH 18/64] Update allcall.py --- pyModeS/decoder/allcall.py | 140 +++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 76 deletions(-) diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py index 9ecd194..5f352f4 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -1,87 +1,75 @@ +""" +Decode all-call reply messages, with dowlink format 11 +""" + from pyModeS import common -""" -Decoding all call replies DF=11 -""" -def interrogator_code(msg): - if common.df(msg) == 11: # check that the msg is DF11 - '''Returns the IP code of the Mode S all-call reply (DF11)''' - binaryraw=common.hex2bin(msg) - PI=binaryraw[-24:] - DATA=binaryraw[:-24] - Gx='1111111111111010000001001000' - Mx=DATA[::-1].zfill(len(DATA)+24)[::-1] - initialdifference=len(Mx)-len(Gx) - Gx = Gx[::-1].zfill(len(Mx))[::-1] - while len(Mx)>initialdifference: - MSB=Mx[0] - if MSB=='1': - result=int(Mx,2)^int(Gx,2) - Mx = str(bin(result)[2:]) - Gx = Gx.rstrip('0')[::-1].zfill(len(Mx))[::-1] +def _checkdf(func): + """Ensure downlink format is 11.""" - else: #If the Mx starts with a 0 - Mx=Mx[1:] - Gx=Gx[:-1] + def wrapper(msg): + df = common.df(msg) + if df != 11: + raise RuntimeError( + "Incorrect downlink format, expect 11, got {}".format(df) + ) + return func(msg) - SIcode=bin(int(Mx,2)^int(PI,2)).zfill(7) #Mx is the parity sequence and PI is the last field of the DF11. - if int(SIcode[0:3],2)>0: - return str("SI" + str(int(SIcode[3:],2))) - else: - return str("II" + str(int(SIcode,2))) - else: - raise RuntimeError("Incorrect or inconsistent message types") + return wrapper -def flight_status(msg): - '''returns the flight status''' - if common.df(msg) == 11: # check that the msg is DF11 - binaryraw=common.hex2bin(msg) - CA = (int(binaryraw[5:8],2)) - if CA in [0, 1, 2, 3, 6, 7]: #reserved - return None - if CA == 4:#level 2 transponder, ability to set CA to 7 and on ground - return "on ground" - if CA == 5:#level 2 transponder, ability to set CA to 7 and airborn - return "airborne" - else: - raise RuntimeError("Incorrect or inconsistent message types") -def transponder_level(msg): - '''returns the transponder level''' - if common.df(msg) == 11:#check that the msg is DF11 - binaryraw=common.hex2bin(msg) - CA = (int(binaryraw[5:8],2)) - if CA in [1, 2, 3, 4, 5, 6, 7]: #reserved - return 2 - if CA == 0: - return 1 - else: - raise RuntimeError("Incorrect or inconsistent message types") +@_checkdf +def icao(msg): + """Decode transponder code (ICAO address). + Args: + msg (str): 14 hexdigits string + Returns: + string: ICAO address + + """ + return common.icao(msg) + + +@_checkdf +def interrogator(msg): + """Decode interrogator identifier code. + + Args: + msg (str): 14 hexdigits string + Returns: + int: interrogator identifier code + + """ + # simply the CRC reminder + return common.crc(msg) + + +@_checkdf def capability(msg): - '''return the capability code''' - if common.df(msg) == 11:#check that the msg is DF11 - binaryraw=common.hex2bin(msg) - CA = (int(binaryraw[5:8],2)) - if CA == 0: #level 1 transponder - return 0 - if CA in [1, 2, 3]: #reserved - return None - if CA == 4:#level 2 transponder, ability to set CA to 7 and on ground - return 4 - if CA == 5:#level 2 transponder, ability to set CA to 7 and airborn - return 5 - if CA == 6:#level 2 transponder, ability to set CA to 7 and either airborn on ground - return 6 - if CA == 7:# (either DR field is 0 or FS field is 2,3,4 or 5), and either airborn on ground - return 7 - else: - raise RuntimeError("Incorrect or inconsistent message types") + """Decode transponder capability. -def AA(msg): - '''returns the icao code of the aircraft''' - if common.df(msg) == 11: # check that the msg is DF11 - return (msg[2:8]) + 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: - raise RuntimeError("Incorrect or inconsistent message types") + text = None + + return ca, text From a3e5e7e1417295187749b86fba5fd5ee4a2ec34d Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 14 Nov 2020 21:57:34 +0100 Subject: [PATCH 19/64] Update surv.py --- pyModeS/decoder/surv.py | 228 +++++++++++++++++++++------------------- 1 file changed, 120 insertions(+), 108 deletions(-) diff --git a/pyModeS/decoder/surv.py b/pyModeS/decoder/surv.py index abda10d..ceec90c 100644 --- a/pyModeS/decoder/surv.py +++ b/pyModeS/decoder/surv.py @@ -1,120 +1,132 @@ """ -Warpper for short roll call surveillance replies DF=4/5 - +Decode short roll call surveillance replies, with downlink format 4 or 5 """ from pyModeS import common + +def _checkdf(func): + """Ensure downlink format is 4 or 5.""" + + def wrapper(msg): + df = common.df(msg) + if df not in [4, 5]: + raise RuntimeError( + "Incorrect downlink format, expect 4 or 5, got {}".format(df) + ) + return func(msg) + + return wrapper + + +@_checkdf def fs(msg): - '''tell the flight status of the aircraft, returns airborne, ground or None''' - if common.df(msg) in [4,5]: - binaryraw=common.hex2bin(msg) - fs=int(binaryraw[5:8],2) - if fs in [0,2]: - return "airborne" - if fs in [1,3]: - return "ground" - else: - return None - else: - raise RuntimeError("Incorrect or inconsistent message types") + """Decode flight status. -def alert(msg): - '''returns True if there is an alert, False otherwise ''' - #The alert indicates that the TXPPR Mode A code is changed manually by the pilot. Icao Annex10, Vol IV, 3.1.2.6.10.1.1 - if common.df(msg) in [4,5]: - binaryraw = common.hex2bin(msg) - fs = int(binaryraw[5:8], 2) - if fs in [2,3,4]: - return True - if fs in [0,1,5]: - return False - else: - return None - else: - raise RuntimeError("Incorrect or inconsistent message types") - - -def SPI(msg): - '''returns True if there there is Special Pulse Indicator, False otherwise ''' - if common.df(msg) in [4, 5]: - binaryraw = common.hex2bin(msg) - fs = int(binaryraw[5:8], 2) - if fs in [4, 5]: - return True - if fs in [0, 1, 2, 3]: - return False - else: - return None - else: - raise RuntimeError("Incorrect or inconsistent message types") - -def dr(msg): - '''Returns Downlink Request Status. - Args: - msg (str): 14 hexdigits string, - Returns: - str: Downlink Request Status - ''' - if common.df(msg) in [4,5]: - binaryraw = common.hex2bin(msg) - dr = int(binaryraw[8:13], 2) - if dr in [2,3,6,7]: - return "ACAS" - if dr == 0: - return "no downlink request" - if dr == 1: - return "request to send Comm-B message" - if dr in 4: - return "Comm-B broadcast 1 available" - if dr in 5: - return "Comm-B broadcast 2 available" - if dr > 7 and dr < 16: - return None - else: - return str(15-dr)+" downlink ELM segments available" - else: - raise RuntimeError("Incorrect or inconsistent message types") - -def um(msg): - '''returns the Interrogator Identifier and the type of reservation Args: - msg (str): 14 hexdigits string, + msg (str): 14 hexdigits string Returns: - int: II code of the identifier that triggered the reply - str: type of reservation made by the interrogator - ''' - if common.df(msg) in [4,5]: - binaryraw = common.hex2bin(msg) - iis = int(binaryraw[13:17], 2) - ds = int(binaryraw[17:19], 2) - if ds == 0: - ds=None - if ds == 1: - ds='Comm-B' - if ds == 2: - ds='Comm-C' - if ds == 3: - ds='Comm-D' - return iis, ds + int, str: flight status, description - else: - raise RuntimeError("Incorrect or inconsistent message types") + """ + msgbin = common.hex2bin(msg) + fs = common.bin2int(msgbin[5:8]) + text = None -def ac(msg): - '''return the altitude in ft''' - if common.df(msg) == 4: - binaryraw = common.hex2bin(msg) - altcode = binaryraw[19:32] # Here we could also put the common.altcode(msg). - return common.altitude(altcode) - else: - raise RuntimeError("Incorrect or inconsistent message types") + 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" -def id(msg): - '''return the squawk code (identifier)''' - if common.df(msg) == 5: - binaryraw = common.hex2bin(msg) - altcode = binaryraw[19:32] # Here we could also put the common.idcode(msg). - return common.squawk(altcode) - else: - raise RuntimeError("Incorrect or inconsistent message types") + 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. + + Args: + msg (String): 14 hexdigits string + + Returns: + int: altitude in ft + + """ + return common.altcode(msg) + + +@_checkdf +def identity(msg): + """Decode squawk code. + + Args: + msg (String): 14 hexdigits string + + Returns: + string: squawk code + + """ + return common.idcode(msg) From f65ffd13f75e623717011b2311ace359f4e6bf9d Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 14 Nov 2020 22:05:48 +0100 Subject: [PATCH 20/64] add tests --- tests/test_allcall.py | 13 +++++++++++++ tests/test_surv.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/test_allcall.py create mode 100644 tests/test_surv.py diff --git a/tests/test_allcall.py b/tests/test_allcall.py new file mode 100644 index 0000000..e727d4b --- /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") == 22 + + +def test_capability(): + assert allcall.capability("5D484FDEA248F5")[0] == 5 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 From 4a6341bcc03c9886bebef24b65198f6850a2b08e Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 14 Nov 2020 23:04:27 +0100 Subject: [PATCH 21/64] import allcall and surv module --- pyModeS/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py index 1d4a787..60ef606 100644 --- a/pyModeS/__init__.py +++ b/pyModeS/__init__.py @@ -11,6 +11,8 @@ except: from .decoder import tell from .decoder import adsb from .decoder import commb +from .decoder import allcall +from .decoder import surv from .decoder import bds from .extra import aero from .extra import tcpclient From d16484bf721db7f83a796d083a7a8112100dfa30 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Thu, 26 Nov 2020 10:35:16 +0100 Subject: [PATCH 22/64] Update interrogator() in allcall.py (#86) --- pyModeS/decoder/allcall.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py index 5f352f4..05ef31c 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -42,8 +42,15 @@ def interrogator(msg): int: interrogator identifier code """ - # simply the CRC reminder - return common.crc(msg) + # the CRC remainder contains the CL and IC field. top three bits are CL field and last four bits are IC field. + remainder = common.crc(msg) + if remainder > 79: + IC = "corrupt IC" + elif remainder < 16: + IC="II"+str(remainder) + else: + IC="SI"+str(remainder-16) + return IC @_checkdf From 4471c836d9c5d44ad987d9e205f63b0ebdc9c700 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Fri, 27 Nov 2020 10:28:20 +0100 Subject: [PATCH 23/64] Update uplink.py --- pyModeS/decoder/uplink.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pyModeS/decoder/uplink.py b/pyModeS/decoder/uplink.py index bfcfb15..3d179e5 100644 --- a/pyModeS/decoder/uplink.py +++ b/pyModeS/decoder/uplink.py @@ -34,12 +34,8 @@ def bds(msg): mbytes = list(map(common.bin2int, msgbin_split)) if uf(msg) in {4, 5, 20, 21}: - # Decode the SI or II interrogator code - # Decode the DI and get the lockout information - # conveniently (LSS or LOS) - - # DI - Designator Identification - di = mbytes[1] & 0x7 + + di = mbytes[1] & 0x7 # DI - Designator Identification RR = mbytes[1] >> 3 & 0x1F if RR > 15: BDS1 = RR - 16 @@ -50,7 +46,7 @@ def bds(msg): RRS = ((mbytes[2] & 0x1) << 4) | ((mbytes[3] & 0xE0) >> 5) BDS2 = RRS else: - BDS2 = 0 + BDS2 = 0 # for other values of DI, the BDS2 is assumed 0 (as per ICAO Annex 10 Vol IV) return str(BDS1) + str(BDS2) else: @@ -161,13 +157,15 @@ def uplink_fields(msg): BDS1 = "" BDS2 = "" if uf(msg) == 11: - # Decode the SI or II interrogator code - # Get cl and ic bit fields from the data + + # 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 @@ -182,9 +180,7 @@ def uplink_fields(msg): IC = ic_switcher.get(codeLabel, "") if uf(msg) in {4, 5, 20, 21}: - # Decode the SI or II interrogator code - # Decode the DI and get the lockout information - # conveniently (LSS or LOS) + # Decode the DI and get the lockout information conveniently (LSS or LOS) # DI - Designator Identification di = mbytes[1] & 0x7 From 2978329d03a8b008d10477d955c2479f2f3aba33 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Tue, 1 Dec 2020 14:09:39 +0100 Subject: [PATCH 24/64] add the pip install pyModeS[fast] option for the Cython dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2279735..fd25fff 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ details = dict( keywords="Mode-S ADS-B EHS ELS Comm-B", packages=find_packages(exclude=["contrib", "docs", "tests"]), install_requires=["numpy", "pyzmq"], + extras_require={"fast": ["Cython"]}, package_data={"pyModeS": ["*.pyx", "*.pxd"]}, scripts=["pyModeS/streamer/modeslive"], ) From 56d70b03d2087ddd1cc68d85609502f5403541b8 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 6 Jan 2021 12:40:04 +0100 Subject: [PATCH 25/64] Update README.rst --- README.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9390673..5a9a309 100644 --- a/README.rst +++ b/README.rst @@ -76,7 +76,12 @@ Installation examples:: 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) ------------------------------------------ @@ -112,7 +117,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 From 20801883d5ee9937b73afcc4f7f77decf0d55bcb Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Wed, 6 Jan 2021 16:46:20 +0100 Subject: [PATCH 26/64] suggestions for installation --- README.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5a9a309..fce5b9c 100644 --- a/README.rst +++ b/README.rst @@ -72,6 +72,9 @@ 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 @@ -88,10 +91,16 @@ 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) From 50cef7d42493085fa5724b241b896fd8b72f69e3 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 11 Jan 2021 14:45:32 +0100 Subject: [PATCH 27/64] minor packaging additions --- py.typed | 0 setup.cfg | 3 +++ setup.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 py.typed create mode 100644 setup.cfg diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 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 fd25fff..0e0d7a3 100644 --- a/setup.py +++ b/setup.py @@ -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"], ) From 3bb8c361e99d9d8e025f5f26ef804452b261d0fe Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 23 Jan 2021 01:21:54 +0100 Subject: [PATCH 28/64] bug fix: altiude and TAS --- pyModeS/decoder/bds/bds05.py | 31 +++++++++++---------- pyModeS/decoder/bds/bds09.py | 52 +++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/pyModeS/decoder/bds/bds05.py b/pyModeS/decoder/bds/bds05.py index 120c31c..2f620b3 100644 --- a/pyModeS/decoder/bds/bds05.py +++ b/pyModeS/decoder/bds/bds05.py @@ -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 @@ -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 diff --git a/pyModeS/decoder/bds/bds09.py b/pyModeS/decoder/bds/bds09.py index 43acf00..c7302e4 100644 --- a/pyModeS/decoder/bds/bds09.py +++ b/pyModeS/decoder/bds/bds09.py @@ -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,29 +35,35 @@ 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 spd_type = "GS" trk_or_hdg = round(trk, 2) @@ -67,13 +73,15 @@ def airborne_velocity(msg, source=False): 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 +90,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 +104,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 From a4ce3bfaf1206a459b66a8f6ed01953839a9940f Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 23 Jan 2021 01:22:19 +0100 Subject: [PATCH 29/64] number format --- pyModeS/c_common.pyx | 2 +- pyModeS/decoder/bds/bds06.py | 34 +++++++++++++++++----------------- pyModeS/decoder/bds/bds44.py | 4 ++-- pyModeS/decoder/bds/bds50.py | 6 +++--- pyModeS/decoder/bds/bds53.py | 2 +- pyModeS/decoder/bds/bds60.py | 2 +- pyModeS/extra/aero.py | 18 +++++++++--------- pyModeS/py_common.py | 2 +- tests/test_allcall.py | 2 +- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pyModeS/c_common.pyx b/pyModeS/c_common.pyx index 18c1768..ab9f989 100644 --- a/pyModeS/c_common.pyx +++ b/pyModeS/c_common.pyx @@ -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 diff --git a/pyModeS/decoder/bds/bds06.py b/pyModeS/decoder/bds/bds06.py index bd7a8f8..46cee1a 100644 --- a/pyModeS/decoder/bds/bds06.py +++ b/pyModeS/decoder/bds/bds06.py @@ -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] @@ -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 @@ -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 diff --git a/pyModeS/decoder/bds/bds44.py b/pyModeS/decoder/bds/bds44.py index a2198a3..6bc755a 100644 --- a/pyModeS/decoder/bds/bds44.py +++ b/pyModeS/decoder/bds/bds44.py @@ -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,7 +136,7 @@ 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) diff --git a/pyModeS/decoder/bds/bds50.py b/pyModeS/decoder/bds/bds50.py index e20bd38..62dd186 100644 --- a/pyModeS/decoder/bds/bds50.py +++ b/pyModeS/decoder/bds/bds50.py @@ -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) diff --git a/pyModeS/decoder/bds/bds53.py b/pyModeS/decoder/bds/bds53.py index 023f7ef..d7c47b9 100644 --- a/pyModeS/decoder/bds/bds53.py +++ b/pyModeS/decoder/bds/bds53.py @@ -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: diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py index 74255db..5ef19c5 100644 --- a/pyModeS/decoder/bds/bds60.py +++ b/pyModeS/decoder/bds/bds60.py @@ -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: diff --git a/pyModeS/extra/aero.py b/pyModeS/extra/aero.py index 8d6a32c..a27e4cb 100644 --- a/pyModeS/extra/aero.py +++ b/pyModeS/extra/aero.py @@ -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/py_common.py b/pyModeS/py_common.py index 4310ac3..0b299ee 100644 --- a/pyModeS/py_common.py +++ b/pyModeS/py_common.py @@ -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 diff --git a/tests/test_allcall.py b/tests/test_allcall.py index e727d4b..b10a839 100644 --- a/tests/test_allcall.py +++ b/tests/test_allcall.py @@ -6,7 +6,7 @@ def test_icao(): def test_interrogator(): - assert allcall.interrogator("5D484FDEA248F5") == 22 + assert allcall.interrogator("5D484FDEA248F5") == "SI6" def test_capability(): From b8f8f4dbc09d4e474fa3b09ec88b4f819bf2161e Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 23 Jan 2021 01:35:04 +0100 Subject: [PATCH 30/64] add github action for publishing on pypi --- .github/workflows/pypi-publish.yml | 29 +++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pypi-publish.yml 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/setup.py b/setup.py index 0e0d7a3..dd688bf 100644 --- a/setup.py +++ b/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.9", description="Python Mode-S and ADS-B Decoder", long_description=long_description, url="https://github.com/junzis/pyModeS", From a308b9a7e0cb0d4507bb5cdd1fd2c58387e5db77 Mon Sep 17 00:00:00 2001 From: TimA346 <63811111+TimA346@users.noreply.github.com> Date: Sat, 23 Jan 2021 03:05:48 +0100 Subject: [PATCH 31/64] implemented tc29 BDS6,2 --- pyModeS/decoder/__init__.py | 62 +++++ pyModeS/decoder/adsb.py | 17 ++ pyModeS/decoder/bds/__init__.py | 1 + pyModeS/decoder/bds/bds62.py | 474 ++++++++++++++++++++++++++++++++ 4 files changed, 554 insertions(+) create mode 100644 pyModeS/decoder/bds/bds62.py diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py index c961ffc..8a1277c 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -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") diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index 1d65849..2a64faf 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -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): diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py index 84f428e..509a568 100644 --- a/pyModeS/decoder/bds/__init__.py +++ b/pyModeS/decoder/bds/__init__.py @@ -38,6 +38,7 @@ from pyModeS.decoder.bds import ( bds50, bds53, bds60, + bds62 ) diff --git a/pyModeS/decoder/bds/bds62.py b/pyModeS/decoder/bds/bds62.py new file mode 100644 index 0000000..13acaa1 --- /dev/null +++ b/pyModeS/decoder/bds/bds62.py @@ -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]) From 6ab147bffece6856120f74e53cfd6eda1713e477 Mon Sep 17 00:00:00 2001 From: TimA346 <63811111+TimA346@users.noreply.github.com> Date: Sat, 23 Jan 2021 11:51:31 +0100 Subject: [PATCH 32/64] Added test for target state and status message subtype 1 --- tests/test_adsb.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_adsb.py b/tests/test_adsb.py index 2b8b631..18ee41e 100644 --- a/tests/test_adsb.py +++ b/tests/test_adsb.py @@ -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 From b9089d55d2de6f7d40981cb6807e883e22d7bb79 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Wed, 24 Feb 2021 15:28:59 +0100 Subject: [PATCH 33/64] Update uplink.py --- pyModeS/decoder/uplink.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyModeS/decoder/uplink.py b/pyModeS/decoder/uplink.py index 3d179e5..5afe2d1 100644 --- a/pyModeS/decoder/uplink.py +++ b/pyModeS/decoder/uplink.py @@ -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, } From dd1fd596f854aaa96fe0fd24636601b973cd68b9 Mon Sep 17 00:00:00 2001 From: Xander Hirsch Date: Sun, 7 Mar 2021 23:27:08 -0800 Subject: [PATCH 34/64] Fixed spelling of "magnetic_north" in docstring --- pyModeS/decoder/adsb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index 1d65849..0419606 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -154,7 +154,7 @@ def 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') For surface messages, vertical rate and its respective sources are set to None. From 69ce39ab390df8c1fa54b152e3a73c11b77fd4bb Mon Sep 17 00:00:00 2001 From: Daniele Forsi Date: Sun, 9 May 2021 14:27:50 +0200 Subject: [PATCH 35/64] Fix typos --- pyModeS/c_common.pyx | 2 +- pyModeS/decoder/__init__.py | 2 +- pyModeS/decoder/bds/bds06.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyModeS/c_common.pyx b/pyModeS/c_common.pyx index ab9f989..fd82c44 100644 --- a/pyModeS/c_common.pyx +++ b/pyModeS/c_common.pyx @@ -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]) diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py index 8a1277c..b6686de 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -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 diff --git a/pyModeS/decoder/bds/bds06.py b/pyModeS/decoder/bds/bds06.py index 46cee1a..b8c0222 100644 --- a/pyModeS/decoder/bds/bds06.py +++ b/pyModeS/decoder/bds/bds06.py @@ -1,7 +1,7 @@ # ------------------------------------------ # BDS 0,6 # ADS-B TC=5-8 -# Surface movment +# Surface movement # ------------------------------------------ from pyModeS import common From 0b2648bfe0570b84a68c34d5aea2b220e63f61cf Mon Sep 17 00:00:00 2001 From: maranov Date: Mon, 21 Jun 2021 17:25:09 +0200 Subject: [PATCH 36/64] Fix invalid socket disconnect call in TcpClient The testing main function now also stops the Client explicitly --- pyModeS/extra/tcpclient.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyModeS/extra/tcpclient.py b/pyModeS/extra/tcpclient.py index ee66ea0..9c90441 100644 --- a/pyModeS/extra/tcpclient.py +++ b/pyModeS/extra/tcpclient.py @@ -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() From a0a8c9b2f77990668564d9a8c9fe7b1d2a1623b5 Mon Sep 17 00:00:00 2001 From: "Novak, Marek" Date: Mon, 28 Jun 2021 17:21:33 +0200 Subject: [PATCH 37/64] Fix UnboundLocalError in airborne_velocity when velocity is zero --- pyModeS/decoder/bds/bds09.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyModeS/decoder/bds/bds09.py b/pyModeS/decoder/bds/bds09.py index c7302e4..e8d7673 100644 --- a/pyModeS/decoder/bds/bds09.py +++ b/pyModeS/decoder/bds/bds09.py @@ -65,8 +65,9 @@ def airborne_velocity(msg, source=False): 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: From b9471d7fcbeb383b7db95cdf11017c856a5ce0a7 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 9 Oct 2021 00:02:33 +0200 Subject: [PATCH 38/64] move functions to py_common --- pyModeS/decoder/commb.py | 2 + pyModeS/decoder/surv.py | 86 +--------------------------------------- pyModeS/py_common.py | 82 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/pyModeS/decoder/commb.py b/pyModeS/decoder/commb.py index 4957681..d49d293 100644 --- a/pyModeS/decoder/commb.py +++ b/pyModeS/decoder/commb.py @@ -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 diff --git a/pyModeS/decoder/surv.py b/pyModeS/decoder/surv.py index ceec90c..4e54614 100644 --- a/pyModeS/decoder/surv.py +++ b/pyModeS/decoder/surv.py @@ -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. diff --git a/pyModeS/py_common.py b/pyModeS/py_common.py index 0b299ee..b6af728 100644 --- a/pyModeS/py_common.py +++ b/pyModeS/py_common.py @@ -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 From e39baf38edc88d07136f53677d7a9c16bd67711f Mon Sep 17 00:00:00 2001 From: Daniele Forsi Date: Sat, 9 Oct 2021 20:55:10 +0200 Subject: [PATCH 39/64] Fix typos --- README.rst | 2 +- pyModeS/c_common.pyx | 4 ++-- pyModeS/decoder/adsb.py | 8 ++++---- pyModeS/decoder/allcall.py | 2 +- pyModeS/decoder/bds/bds05.py | 2 +- pyModeS/decoder/bds/bds06.py | 6 +++--- pyModeS/decoder/bds/bds08.py | 2 +- pyModeS/decoder/bds/bds09.py | 2 +- pyModeS/decoder/bds/bds17.py | 2 +- pyModeS/decoder/bds/bds44.py | 2 +- pyModeS/decoder/bds/bds60.py | 2 +- pyModeS/py_common.py | 6 +++--- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index fce5b9c..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. diff --git a/pyModeS/c_common.pyx b/pyModeS/c_common.pyx index ab9f989..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]) diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index 50d13f6..285db62 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -109,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: @@ -163,10 +163,10 @@ 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) @@ -341,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 diff --git a/pyModeS/decoder/allcall.py b/pyModeS/decoder/allcall.py index 05ef31c..d5592c8 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -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 diff --git a/pyModeS/decoder/bds/bds05.py b/pyModeS/decoder/bds/bds05.py index 2f620b3..31f6ca2 100644 --- a/pyModeS/decoder/bds/bds05.py +++ b/pyModeS/decoder/bds/bds05.py @@ -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) diff --git a/pyModeS/decoder/bds/bds06.py b/pyModeS/decoder/bds/bds06.py index 46cee1a..4a9f6a9 100644 --- a/pyModeS/decoder/bds/bds06.py +++ b/pyModeS/decoder/bds/bds06.py @@ -1,7 +1,7 @@ # ------------------------------------------ # BDS 0,6 # ADS-B TC=5-8 -# Surface movment +# Surface movement # ------------------------------------------ from pyModeS import common @@ -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) @@ -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: diff --git a/pyModeS/decoder/bds/bds08.py b/pyModeS/decoder/bds/bds08.py index efacab6..b62d909 100644 --- a/pyModeS/decoder/bds/bds08.py +++ b/pyModeS/decoder/bds/bds08.py @@ -1,7 +1,7 @@ # ------------------------------------------ # BDS 0,8 # ADS-B TC=1-4 -# Aircraft identitification and category +# Aircraft identification and category # ------------------------------------------ from pyModeS import common diff --git a/pyModeS/decoder/bds/bds09.py b/pyModeS/decoder/bds/bds09.py index c7302e4..4c4caf9 100644 --- a/pyModeS/decoder/bds/bds09.py +++ b/pyModeS/decoder/bds/bds09.py @@ -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: diff --git a/pyModeS/decoder/bds/bds17.py b/pyModeS/decoder/bds/bds17.py index 80210f1..80e8bbd 100644 --- a/pyModeS/decoder/bds/bds17.py +++ b/pyModeS/decoder/bds/bds17.py @@ -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", diff --git a/pyModeS/decoder/bds/bds44.py b/pyModeS/decoder/bds/bds44.py index 6bc755a..351f13e 100644 --- a/pyModeS/decoder/bds/bds44.py +++ b/pyModeS/decoder/bds/bds44.py @@ -142,7 +142,7 @@ def hum44(msg): def turb44(msg): - """Turblence. + """Turbulence. Args: msg (str): 28 hexdigits string diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py index 5ef19c5..1af3882 100644 --- a/pyModeS/decoder/bds/bds60.py +++ b/pyModeS/decoder/bds/bds60.py @@ -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 diff --git a/pyModeS/py_common.py b/pyModeS/py_common.py index 0b299ee..d44ee61 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)) From b90c2e740d3968f926a3069ace813640e0116c7e Mon Sep 17 00:00:00 2001 From: Daniele Forsi Date: Sat, 9 Oct 2021 23:17:31 +0200 Subject: [PATCH 40/64] Fix typos in print() strings --- pyModeS/decoder/__init__.py | 2 +- pyModeS/extra/rtlreader.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py index 8a1277c..b6686de 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -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 diff --git a/pyModeS/extra/rtlreader.py b/pyModeS/extra/rtlreader.py index a077a7e..5e55938 100644 --- a/pyModeS/extra/rtlreader.py +++ b/pyModeS/extra/rtlreader.py @@ -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 From 0eb2d1f6a2f8ba10b54da65b6809593b1fb0f051 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Thu, 18 Nov 2021 11:24:42 +0100 Subject: [PATCH 41/64] Update bds61.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the Mode A decoder. The ZERO is already part of the binary data inside the message and does not need to be added. See ED-102A, 2.2.3.2.7.8.1.2 b. Starting with ―ME‖ bit 12, the code sequence shall be C1, A1, C2, A2, C4, A4, ZERO, B1, D1, B2, D2, B4, D4. --- pyModeS/decoder/bds/bds61.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/decoder/bds/bds61.py b/pyModeS/decoder/bds/bds61.py index 05dc649..22e7343 100644 --- a/pyModeS/decoder/bds/bds61.py +++ b/pyModeS/decoder/bds/bds61.py @@ -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 From b52431ca51613b38a0205466e81fe9cf51cdf6d7 Mon Sep 17 00:00:00 2001 From: Flyer350 <57186034+Flyer350@users.noreply.github.com> Date: Mon, 24 Jan 2022 10:38:17 +0100 Subject: [PATCH 42/64] Update bds20.py #message with empty callsign can still be BDS2,0 Example messages: a900189f20000000000000d0d739 a100000020000000000000a81785 --- pyModeS/decoder/bds/bds20.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py index 63e10cd..914544e 100644 --- a/pyModeS/decoder/bds/bds20.py +++ b/pyModeS/decoder/bds/bds20.py @@ -26,7 +26,7 @@ def is20(msg): cs = cs20(msg) - if "#" in cs: + if "#" in cs and common.bin2int(d[8:56])>0: #message with empty callsign can still be BDS2,0 return False return True From ce5adbafe5ad74127e5143cb77c0cdf360ae18d9 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Mon, 18 Apr 2022 11:27:53 +0200 Subject: [PATCH 43/64] Update bds20.py --- pyModeS/decoder/bds/bds20.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py index 914544e..d5f4b2f 100644 --- a/pyModeS/decoder/bds/bds20.py +++ b/pyModeS/decoder/bds/bds20.py @@ -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 and common.bin2int(d[8:56])>0: #message with empty callsign can still be BDS2,0 + if "#" in cs20(msg): return False return True From 5dc74a554804799a2b35274d70bac19d3e03c741 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Mon, 18 Apr 2022 11:59:14 +0200 Subject: [PATCH 44/64] new release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dd688bf..26cfa64 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f: details = dict( name="pyModeS", - version="2.9", + version="2.10", description="Python Mode-S and ADS-B Decoder", long_description=long_description, url="https://github.com/junzis/pyModeS", From 65ce1a62c47a46fe78b40055d80f3168700a1000 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 18 Apr 2022 17:59:30 +0200 Subject: [PATCH 45/64] test ci and fix #125 --- .github/workflows/run-tests.yml | 54 +++++++++++++++++++++++++++++++++ pyModeS/decoder/adsb.py | 2 ++ pyModeS/decoder/bds/bds20.py | 2 +- setup.py | 2 +- tests/test_adsb.py | 2 +- 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..684b375 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,54 @@ +name: tests + +on: + push: + pull_request_target: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest # ${{ matrix.os }} + strategy: + matrix: + # os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.7", "3.8", "3.9", "3.10"] + + 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 + # run: | + # mypy pyModeS tests + + - name: Run tests (without Cython) + run: | + pytest tests --cov --cov-report term-missing + + - name: Install with Cython + run: | + pip install -U cython + pip install --force-reinstall . + + - 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/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index 285db62..ee41017 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -1,3 +1,5 @@ +# noqa + """ADS-B module. The ADS-B module also imports functions from the following modules: diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py index d5f4b2f..ce60217 100644 --- a/pyModeS/decoder/bds/bds20.py +++ b/pyModeS/decoder/bds/bds20.py @@ -25,7 +25,7 @@ def is20(msg): return False # allow empty callsign - if common.bin2int(d[8:56]) == 0 + if common.bin2int(d[8:56]) == 0: return True if "#" in cs20(msg): diff --git a/setup.py b/setup.py index 26cfa64..0ee1d00 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f: details = dict( name="pyModeS", - version="2.10", + version="2.10.1", description="Python Mode-S and ADS-B Decoder", long_description=long_description, url="https://github.com/junzis/pyModeS", diff --git a/tests/test_adsb.py b/tests/test_adsb.py index 18ee41e..4a78f7e 100644 --- a/tests/test_adsb.py +++ b/tests/test_adsb.py @@ -90,7 +90,7 @@ 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.selected_heading("8DA05629EA21485CBF3F8CADAEEB") == 66.8 assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") == True assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") == True assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") == False From 6e33ac0288c0d6be88657176c87faa3734762cb7 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 18 Apr 2022 18:32:33 +0200 Subject: [PATCH 46/64] with mypy --- .github/workflows/run-tests.yml | 6 +- pyModeS/__init__.py | 4 +- pyModeS/c_common.pyi | 29 ++++++ pyModeS/decoder/__init__.py | 37 +++++--- pyModeS/decoder/adsb.py | 2 - pyModeS/decoder/bds/bds62.py | 157 +++++++++++++++++++++++--------- pyModeS/decoder/ehs.py | 4 +- pyModeS/extra/rtlreader.py | 2 +- py.typed => pyModeS/py.typed | 0 9 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 pyModeS/c_common.pyi rename py.typed => pyModeS/py.typed (100%) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 684b375..dfef0cb 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -30,9 +30,9 @@ jobs: pip install -U pytest codecov pytest-cov pip install . - # - name: Type checking - # run: | - # mypy pyModeS tests + - name: Type checking + run: | + mypy pyModeS - name: Run tests (without Cython) run: | diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py index 60ef606..9504d45 100644 --- a/pyModeS/__init__.py +++ b/pyModeS/__init__.py @@ -5,8 +5,8 @@ try: from . import c_common as common from .c_common import * except: - from . import py_common as common - from .py_common import * + from . import py_common as common # type: ignore + from .py_common import * # type: ignore from .decoder import tell from .decoder import adsb diff --git a/pyModeS/c_common.pyi b/pyModeS/c_common.pyi new file mode 100644 index 0000000..dd28386 --- /dev/null +++ b/pyModeS/c_common.pyi @@ -0,0 +1,29 @@ +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: ... diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py index b6686de..aa4b901 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -71,12 +71,12 @@ def tell(msg: str) -> None: _print("CPR Longitude", cprlon) _print("Altitude", alt, "feet") - if tc == 29: # target state and status + 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"} + types_29 = {0: "Not Engaged", 1: "Engaged"} tcas_operational_types = {0: "Not Operational", 1: "Operational"} if subtype == 0: emergency_types = { @@ -87,11 +87,11 @@ def tell(msg: str) -> None: 4: "No communications", 5: "Unlawful interference", 6: "Downed aircraft", - 7: "Reserved" + 7: "Reserved", } vertical_horizontal_types = { 1: "Acquiring mode", - 2: "Capturing/Maintaining mode" + 2: "Capturing/Maintaining mode", } tcas_ra_types = {0: "Not active", 1: "Active"} alt, alt_source, alt_ref = adsb.target_altitude(msg) @@ -108,7 +108,12 @@ def tell(msg: str) -> None: _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", + 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: @@ -124,14 +129,20 @@ def tell(msg: str) -> None: _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 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") diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index ee41017..285db62 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -1,5 +1,3 @@ -# noqa - """ADS-B module. The ADS-B module also imports functions from the following modules: diff --git a/pyModeS/decoder/bds/bds62.py b/pyModeS/decoder/bds/bds62.py index 13acaa1..16db8f1 100644 --- a/pyModeS/decoder/bds/bds62.py +++ b/pyModeS/decoder/bds/bds62.py @@ -4,8 +4,10 @@ # Target State and Status # ------------------------------------------ +from __future__ import annotations from pyModeS import common + def selected_altitude(msg): """Decode selected altitude. @@ -19,14 +21,19 @@ def selected_altitude(msg): """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -35,7 +42,6 @@ def selected_altitude(msg): return alt, alt_source - def target_altitude(msg): """Decode target altitude. @@ -50,14 +56,19 @@ def target_altitude(msg): """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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: @@ -94,14 +105,19 @@ def vertical_mode(msg): """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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: @@ -128,14 +144,19 @@ def horizontal_mode(msg): """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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: @@ -152,24 +173,29 @@ def selected_heading(msg): Returns: float: Selected heading (degree) - + """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 = (hdg_sign + 1) * common.bin2int(mb[31:39]) * (180 / 256) hdg = round(hdg, 2) return hdg @@ -185,18 +211,23 @@ def target_angle(msg): 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) + 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) + 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: @@ -210,7 +241,7 @@ def target_angle(msg): angle_source = "Autopilot mode" else: angle_source = "FMS/RNAV" - + angle_type = "Heading" if int(mb[36]) else "Track" return angle, angle_type, angle_source @@ -228,14 +259,19 @@ def baro_pressure_setting(msg): """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -243,7 +279,8 @@ def baro_pressure_setting(msg): return baro -def autopilot(msg) -> bool: + +def autopilot(msg) -> None | bool: """Decode autopilot engagement. Args: @@ -255,14 +292,19 @@ def autopilot(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -271,7 +313,8 @@ def autopilot(msg) -> bool: return autopilot -def vnav_mode(msg) -> bool: + +def vnav_mode(msg) -> None | bool: """Decode VNAV mode. Args: @@ -283,14 +326,19 @@ def vnav_mode(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -300,7 +348,7 @@ def vnav_mode(msg) -> bool: return vnav_mode -def altitude_hold_mode(msg) -> bool: +def altitude_hold_mode(msg) -> None | bool: """Decode altitude hold mode. Args: @@ -312,14 +360,19 @@ def altitude_hold_mode(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -329,8 +382,7 @@ def altitude_hold_mode(msg) -> bool: return alt_hold_mode - -def approach_mode(msg) -> bool: +def approach_mode(msg) -> None | bool: """Decode approach mode. Args: @@ -342,14 +394,19 @@ def approach_mode(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -359,7 +416,7 @@ def approach_mode(msg) -> bool: return app_mode -def lnav_mode(msg) -> bool: +def lnav_mode(msg) -> None | bool: """Decode LNAV mode. Args: @@ -371,14 +428,19 @@ def lnav_mode(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -388,7 +450,7 @@ def lnav_mode(msg) -> bool: return lnav_mode -def tcas_operational(msg) -> bool: +def tcas_operational(msg) -> None | bool: """Decode TCAS/ACAS operational. Args: @@ -400,7 +462,9 @@ def tcas_operational(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + raise RuntimeError( + "%s: Not a target state and status message, expecting TC=29" % msg + ) mb = common.hex2bin(msg)[32:] @@ -413,6 +477,7 @@ def tcas_operational(msg) -> bool: return tcas + def tcas_ra(msg) -> bool: """Decode TCAS/ACAS Resolution advisory. @@ -425,14 +490,19 @@ def tcas_ra(msg) -> bool: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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 @@ -462,13 +532,18 @@ def emergency_status(msg) -> int: """ if common.typecode(msg) != 29: - raise RuntimeError("%s: Not a target state and status message, expecting TC=29" % msg) + 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) + 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/ehs.py b/pyModeS/decoder/ehs.py index bb91a29..8533f7f 100644 --- a/pyModeS/decoder/ehs.py +++ b/pyModeS/decoder/ehs.py @@ -30,6 +30,6 @@ def BDS(msg): def icao(msg): - from pyModeS.decoder.common import icao + from . import common - return icao(msg) + return common.icao(msg) diff --git a/pyModeS/extra/rtlreader.py b/pyModeS/extra/rtlreader.py index 5e55938..2d099a2 100644 --- a/pyModeS/extra/rtlreader.py +++ b/pyModeS/extra/rtlreader.py @@ -4,7 +4,7 @@ import numpy as np import pyModeS as pms try: - import rtlsdr + import rtlsdr # type: ignore except: print("------------------------------------------------------------------------") print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !") diff --git a/py.typed b/pyModeS/py.typed similarity index 100% rename from py.typed rename to pyModeS/py.typed From 06941bac2e9a24d394fba7eeedd3aa61d3edb0a9 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 18 Apr 2022 18:47:12 +0200 Subject: [PATCH 47/64] fix tests --- .github/workflows/run-tests.yml | 3 ++- setup.py | 2 +- tests/test_adsb.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index dfef0cb..8a93978 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -41,7 +41,8 @@ jobs: - name: Install with Cython run: | pip install -U cython - pip install --force-reinstall . + pip uninstall -y pymodes + pip install . - name: Run tests (with Cython) run: | diff --git a/setup.py b/setup.py index 0ee1d00..562b38d 100644 --- a/setup.py +++ b/setup.py @@ -57,5 +57,5 @@ try: setup(**dict(details, ext_modules=cythonize(extensions))) -except: +except ImportError: setup(**details) diff --git a/tests/test_adsb.py b/tests/test_adsb.py index 4a78f7e..a735023 100644 --- a/tests/test_adsb.py +++ b/tests/test_adsb.py @@ -83,7 +83,7 @@ def test_adsb_velocity(): 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(): From 73c3a75234418558fde5d4c59724f6527c903e02 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 18 Apr 2022 18:50:38 +0200 Subject: [PATCH 48/64] more os --- .github/workflows/run-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8a93978..4322ac2 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -7,10 +7,10 @@ on: jobs: deploy: - runs-on: ubuntu-latest # ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: matrix: - # os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.7", "3.8", "3.9", "3.10"] env: From 401f3f5e95ad46cba7b6023a83115d1c1cf9a79a Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 18 Apr 2022 20:28:56 +0200 Subject: [PATCH 49/64] ignore for windows+github actions --- tests/test_bds_inference.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_bds_inference.py b/tests/test_bds_inference.py index c5c849f..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" From 13f4fe77bb652205df6c95b6bba43d949f02e147 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Mon, 18 Apr 2022 20:40:49 +0200 Subject: [PATCH 50/64] tests with coverage --- .coveragerc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .coveragerc 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 From c02414417d2d518a6fb2457b181a85704462df0b Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Mon, 18 Apr 2022 20:50:24 +0200 Subject: [PATCH 51/64] new release 2.11 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 562b38d..f977af5 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f: details = dict( name="pyModeS", - version="2.10.1", + version="2.11", description="Python Mode-S and ADS-B Decoder", long_description=long_description, url="https://github.com/junzis/pyModeS", From b36540e2a17c49bf36bc824bc1e4488306d1a1a0 Mon Sep 17 00:00:00 2001 From: Xavier Olive <1360812+xoolive@users.noreply.github.com> Date: Tue, 31 May 2022 09:55:07 +0200 Subject: [PATCH 52/64] fix corner case --- pyModeS/decoder/bds/bds09.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/bds/bds09.py b/pyModeS/decoder/bds/bds09.py index 90cf95d..737f31a 100644 --- a/pyModeS/decoder/bds/bds09.py +++ b/pyModeS/decoder/bds/bds09.py @@ -81,10 +81,10 @@ def airborne_velocity(msg, source=False): spd = common.bin2int(mb[25:35]) - spd = None if spd == 0 else spd - 1 - if subtype == 4: # Supersonic spd *= 4 + + spd = None if spd == 0 else spd - 1 if mb[24] == "0": spd_type = "IAS" From b90932e51d1e6b2639ed822514e1f2888d659064 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 9 Jul 2022 10:09:41 +0200 Subject: [PATCH 53/64] add missing parameters for dumpto file --- pyModeS/streamer/decode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyModeS/streamer/decode.py b/pyModeS/streamer/decode.py index 5297262..0816d06 100644 --- a/pyModeS/streamer/decode.py +++ b/pyModeS/streamer/decode.py @@ -231,10 +231,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]) From bb8f83e8328e36df203d238db09fe242a4ff0700 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Sat, 9 Jul 2022 12:11:28 +0200 Subject: [PATCH 54/64] fix RCv for position with GNSS height --- pyModeS/decoder/adsb.py | 14 +++++++++----- pyModeS/decoder/uncertainty.py | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pyModeS/decoder/adsb.py b/pyModeS/decoder/adsb.py index 285db62..df35bc9 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -44,7 +44,7 @@ from pyModeS.decoder.bds.bds62 import ( autopilot, altitude_hold_mode, approach_mode, - emergency_status + emergency_status, ) @@ -262,12 +262,16 @@ def nuc_p(msg): NUCp = uncertainty.TC_NUCp_lookup[tc] HPL = uncertainty.NUCp[NUCp]["HPL"] RCu = uncertainty.NUCp[NUCp]["RCu"] - RCv = uncertainty.NUCp[NUCp]["RCv"] except KeyError: - HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA + HPL, RCu = uncertainty.NA, uncertainty.NA - if tc in [20, 21]: - RCv = uncertainty.NA + RCv = uncertainty.NA + + # RCv only available for GNSS height + if tc == 20: + RCv = 4 + elif tc == 21: + RCv = 15 return HPL, RCu, RCv diff --git a/pyModeS/decoder/uncertainty.py b/pyModeS/decoder/uncertainty.py index cb74cb0..2a923ab 100644 --- a/pyModeS/decoder/uncertainty.py +++ b/pyModeS/decoder/uncertainty.py @@ -68,16 +68,16 @@ TC_NICv2_lookup = { NUCp = { - 9: {"HPL": 7.5, "RCu": 3, "RCv": 4}, - 8: {"HPL": 25, "RCu": 10, "RCv": 15}, - 7: {"HPL": 185, "RCu": 93, "RCv": NA}, - 6: {"HPL": 370, "RCu": 185, "RCv": NA}, - 5: {"HPL": 926, "RCu": 463, "RCv": NA}, - 4: {"HPL": 1852, "RCu": 926, "RCv": NA}, - 3: {"HPL": 3704, "RCu": 1852, "RCv": NA}, - 2: {"HPL": 18520, "RCu": 9260, "RCv": NA}, - 1: {"HPL": 37040, "RCu": 18520, "RCv": NA}, - 0: {"HPL": NA, "RCu": NA, "RCv": NA}, + 9: {"HPL": 7.5, "RCu": 3}, + 8: {"HPL": 25, "RCu": 10}, + 7: {"HPL": 185, "RCu": 93}, + 6: {"HPL": 370, "RCu": 185}, + 5: {"HPL": 926, "RCu": 463}, + 4: {"HPL": 1852, "RCu": 926}, + 3: {"HPL": 3704, "RCu": 1852}, + 2: {"HPL": 18520, "RCu": 9260}, + 1: {"HPL": 37040, "RCu": 18520}, + 0: {"HPL": NA, "RCu": NA}, } NUCv = { From b4d4ef8f1fbd8eb4ff48172badab242cb5661fb7 Mon Sep 17 00:00:00 2001 From: Daniele Forsi Date: Mon, 8 Aug 2022 10:21:57 +0200 Subject: [PATCH 55/64] Fix description --- pyModeS/extra/aero.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/extra/aero.py b/pyModeS/extra/aero.py index a27e4cb..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] From 130491e64119a6d2c27a6ee44242907786ef1c8e Mon Sep 17 00:00:00 2001 From: Daniele Forsi Date: Mon, 8 Aug 2022 13:00:22 +0200 Subject: [PATCH 56/64] Add missing import Found with pylint. Fixes: pyModeS/streamer/decode.py:290:21: E0602: Undefined variable 'traceback' (undefined-variable) --- pyModeS/streamer/decode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyModeS/streamer/decode.py b/pyModeS/streamer/decode.py index 0816d06..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 From 6b4a5bdaf33b9460d0e539c37647f33ca0b14f95 Mon Sep 17 00:00:00 2001 From: John Wyman Date: Sun, 14 Aug 2022 00:27:09 -0700 Subject: [PATCH 57/64] move round() call to baro == None else clause --- pyModeS/decoder/bds/bds62.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyModeS/decoder/bds/bds62.py b/pyModeS/decoder/bds/bds62.py index 16db8f1..2d070d7 100644 --- a/pyModeS/decoder/bds/bds62.py +++ b/pyModeS/decoder/bds/bds62.py @@ -274,8 +274,7 @@ def baro_pressure_setting(msg): ) baro = common.bin2int(mb[20:29]) - baro = None if baro == 0 else 800 + (baro - 1) * 0.8 - baro = round(baro, 1) + baro = None if baro == 0 else round(800 + (baro - 1) * 0.8, 1) return baro From afd53af87b0a19f57c9b5d2491604a260b979863 Mon Sep 17 00:00:00 2001 From: John Wyman Date: Sun, 14 Aug 2022 00:34:17 -0700 Subject: [PATCH 58/64] Increase padding on _print to fit baro --- pyModeS/decoder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py index aa4b901..2b80d1f 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -2,7 +2,7 @@ def tell(msg: str) -> None: from pyModeS 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) From 6a427c08b58a654c474f1e340ba01f6aad3401d5 Mon Sep 17 00:00:00 2001 From: John Wyman Date: Sun, 14 Aug 2022 00:45:13 -0700 Subject: [PATCH 59/64] Update display to remove units if baro == none --- pyModeS/decoder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py index 2b80d1f..53ecf72 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -127,7 +127,7 @@ def tell(msg: str) -> None: lnav = adsb.lnav_mode(msg) _print("Selected altitude", alt, "feet") _print("Altitude source", alt_source) - _print("Barometric pressure setting", baro, "millibars") + _print("Barometric pressure setting", baro, "" if baro == 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) From 51222372b1001056bd16cc25c59da4a233597f03 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Tue, 1 Nov 2022 16:32:42 +0900 Subject: [PATCH 60/64] fully annotated code --- pyModeS/__init__.py | 14 +- pyModeS/c_common.pyi | 13 +- pyModeS/common.pyi | 22 +++ pyModeS/decoder/__init__.py | 50 ++++-- pyModeS/decoder/adsb.py | 286 +++++++++++++++++++++----------- pyModeS/decoder/allcall.py | 40 +++-- pyModeS/decoder/bds/__init__.py | 34 ++-- pyModeS/decoder/bds/bds05.py | 33 ++-- pyModeS/decoder/bds/bds06.py | 52 ++++-- pyModeS/decoder/bds/bds08.py | 12 +- pyModeS/decoder/bds/bds09.py | 53 ++++-- pyModeS/decoder/bds/bds10.py | 7 +- pyModeS/decoder/bds/bds17.py | 8 +- pyModeS/decoder/bds/bds20.py | 6 +- pyModeS/decoder/bds/bds30.py | 6 +- pyModeS/decoder/bds/bds40.py | 22 ++- pyModeS/decoder/bds/bds44.py | 16 +- pyModeS/decoder/bds/bds45.py | 22 +-- pyModeS/decoder/bds/bds50.py | 16 +- pyModeS/decoder/bds/bds53.py | 16 +- pyModeS/decoder/bds/bds60.py | 24 +-- pyModeS/decoder/bds/bds61.py | 10 +- pyModeS/decoder/bds/bds62.py | 91 +++++----- pyModeS/decoder/commb.py | 68 ++++++-- pyModeS/decoder/ehs.py | 43 ++++- pyModeS/decoder/els.py | 24 ++- pyModeS/decoder/surv.py | 102 +++++++++++- pyModeS/decoder/uncertainty.py | 83 ++++++--- pyModeS/decoder/uplink.py | 42 ++--- tests/sample_run_adsb.py | 8 +- tests/test_adsb.py | 25 +-- tests/test_commb.py | 25 +-- 32 files changed, 869 insertions(+), 404 deletions(-) create mode 100644 pyModeS/common.pyi diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py index 9504d45..0b11444 100644 --- a/pyModeS/__init__.py +++ b/pyModeS/__init__.py @@ -4,7 +4,7 @@ import warnings try: from . import c_common as common from .c_common import * -except: +except Exception: from . import py_common as common # type: ignore from .py_common import * # type: ignore @@ -17,6 +17,18 @@ 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 index dd28386..2c68584 100644 --- a/pyModeS/c_common.pyi +++ b/pyModeS/c_common.pyi @@ -2,28 +2,17 @@ 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/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 53ecf72..63341df 100644 --- a/pyModeS/decoder/__init__.py +++ b/pyModeS/decoder/__init__.py @@ -1,5 +1,5 @@ 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("%28s: " % label, end="") @@ -20,6 +20,11 @@ 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", "Identification and category") @@ -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)") @@ -106,8 +113,16 @@ def tell(msg: str) -> None: _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]) + 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] @@ -117,7 +132,7 @@ def tell(msg: str) -> 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) + alt, alt_source = adsb.selected_altitude(msg) # type: ignore baro = adsb.baro_pressure_setting(msg) hdg = adsb.selected_heading(msg) autopilot = adsb.autopilot(msg) @@ -127,13 +142,20 @@ def tell(msg: str) -> None: lnav = adsb.lnav_mode(msg) _print("Selected altitude", alt, "feet") _print("Altitude source", alt_source) - _print("Barometric pressure setting", baro, "" if baro == None else "millibars") + _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( + "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 + "Altitude hold mode", + types_29[alt_hold] if alt_hold else None, ) _print("Approach mode", types_29[app] if app else None) _print( @@ -167,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 df35bc9..71bab5b 100644 --- a/pyModeS/decoder/adsb.py +++ b/pyModeS/decoder/adsb.py @@ -2,65 +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 import is_emergency, emergency_state, emergency_squawk -from pyModeS.decoder.bds.bds62 import ( +from .bds.bds08 import callsign, category +from .bds.bds09 import airborne_velocity, altitude_diff +from .bds.bds61 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, - baro_pressure_setting, vertical_mode, - horizontal_mode, vnav_mode, - lnav_mode, - autopilot, - altitude_hold_mode, - approach_mode, - emergency_status, ) +__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. @@ -82,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( @@ -103,7 +161,9 @@ 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 @@ -123,6 +183,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) @@ -133,7 +196,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: @@ -145,7 +208,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: @@ -157,16 +220,20 @@ 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. - If set to True, the function will return six values instead of four. + 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) @@ -174,22 +241,26 @@ def velocity(msg, source=False): - [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) @@ -199,11 +270,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 @@ -214,7 +288,7 @@ def oe_flag(msg): return int(msgbin[53]) -def version(msg): +def version(msg: str) -> int: """ADS-B Version Args: @@ -236,13 +310,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) @@ -250,7 +326,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 """ @@ -295,17 +376,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: @@ -313,10 +395,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: @@ -348,9 +434,11 @@ def nic_v2(msg, NICa, NICbc): 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: @@ -399,7 +486,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: @@ -422,7 +509,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: @@ -433,7 +520,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) @@ -476,18 +566,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) @@ -505,18 +598,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 d5592c8..3023ab3 100644 --- a/pyModeS/decoder/allcall.py +++ b/pyModeS/decoder/allcall.py @@ -2,13 +2,21 @@ Decode all-call reply messages, with downlink format 11 """ -from pyModeS import common + +from __future__ import annotations +from typing import Callable, TypeVar + +from .. import common + +T = TypeVar("T") +F = Callable[[str], T] -def _checkdf(func): +def _checkdf(func: F[T]) -> F[T]: + """Ensure downlink format is 11.""" - def wrapper(msg): + def wrapper(msg: str) -> T: df = common.df(msg) if df != 11: raise RuntimeError( @@ -20,7 +28,7 @@ def _checkdf(func): @_checkdf -def icao(msg): +def icao(msg: str) -> None | str: """Decode transponder code (ICAO address). Args: @@ -33,7 +41,7 @@ def icao(msg): @_checkdf -def interrogator(msg): +def interrogator(msg: str) -> str: """Decode interrogator identifier code. Args: @@ -42,19 +50,20 @@ def interrogator(msg): int: interrogator identifier code """ - # the CRC remainder contains the CL and IC field. top three bits are CL field and last four bits are IC field. + # 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: + if remainder > 79: IC = "corrupt IC" elif remainder < 16: - IC="II"+str(remainder) + IC = "II" + str(remainder) else: - IC="SI"+str(remainder-16) + IC = "SI" + str(remainder - 16) return IC @_checkdf -def capability(msg): +def capability(msg: str) -> tuple[int, None | str]: """Decode transponder capability. Args: @@ -73,9 +82,16 @@ def capability(msg): 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" + 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" + 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 diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py index 509a568..1ee0042 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,13 +33,15 @@ from pyModeS.decoder.bds import ( bds44, bds45, bds50, - bds53, bds60, - bds62 + bds61, + 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: @@ -52,7 +51,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. """ @@ -114,15 +114,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) @@ -133,6 +135,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 31f6ca2..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) @@ -59,7 +65,8 @@ 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) @@ -78,7 +85,9 @@ 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 @@ -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,16 +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:] - alt = common.altitude(altcode) + return common.altitude(altcode) else: - alt = common.bin2int(altbin) * 3.28084 - - 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 4a9f6a9..9077275 100644 --- a/pyModeS/decoder/bds/bds06.py +++ b/pyModeS/decoder/bds/bds06.py @@ -4,10 +4,22 @@ # 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. @@ -55,7 +67,8 @@ 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) @@ -72,17 +85,19 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref): 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 @@ -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. - If set to True, the function will return six values instead of four. + 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: mov_lb = [2, 9, 13, 39, 94, 109, 124] - kts_lb = [0.125, 1, 2, 15, 70, 100, 175] - step = [0.125, 0.25, 0.5, 1, 2, 5] + 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 b62d909..82402b1 100644 --- a/pyModeS/decoder/bds/bds08.py +++ b/pyModeS/decoder/bds/bds08.py @@ -4,10 +4,10 @@ # 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 737f31a..e462feb 100644 --- a/pyModeS/decoder/bds/bds09.py +++ b/pyModeS/decoder/bds/bds09.py @@ -1,25 +1,29 @@ # ------------------------------------------ # 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. - If set to True, the function will return six values instead of four. + 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) @@ -29,12 +33,20 @@ def airborne_velocity(msg, source=False): """ 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:] subtype = common.bin2int(mb[5:8]) + 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 = common.bin2int(mb[14:24]) @@ -80,11 +92,9 @@ def airborne_velocity(msg, source=False): trk_or_hdg = hdg spd = common.bin2int(mb[25:35]) - - if subtype == 4: # Supersonic - spd *= 4 - spd = None if spd == 0 else spd - 1 + if subtype == 4 and spd is not None: # Supersonic + spd *= 4 if mb[24] == "0": spd_type = "IAS" @@ -99,13 +109,20 @@ 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): - """Decode the difference between GNSS and barometric altitude. +def altitude_diff(msg: str) -> None | float: + """Decode the differece between GNSS and barometric altitude. Args: msg (str): 28 hexdigits string, TC=19 @@ -117,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 80e8bbd..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: @@ -38,7 +40,7 @@ def is17(msg): return True -def cap17(msg): +def cap17(msg: str) -> List[str]: """Extract capacities from BDS 1,7 message Args: diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py index ce60217..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: @@ -34,7 +34,7 @@ def is20(msg): 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 5a145e7..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 selalt40fms(msg) diff --git a/pyModeS/decoder/bds/bds44.py b/pyModeS/decoder/bds/bds44.py index 351f13e..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: @@ -73,7 +75,7 @@ def wind44(msg): 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: @@ -141,7 +143,7 @@ def hum44(msg): return round(hm, 1) -def turb44(msg): +def turb44(msg: str) -> Optional[int]: """Turbulence. Args: 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 62dd186..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) @@ -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: @@ -82,7 +84,7 @@ def roll50(msg): return round(angle, 1) -def trk50(msg): +def trk50(msg: str) -> Optional[float]: """True track angle, BDS 5,0 message Args: @@ -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: @@ -155,7 +157,7 @@ def rtrk50(msg): 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 d7c47b9..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: @@ -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 1af3882..98c9a79 100644 --- a/pyModeS/decoder/bds/bds60.py +++ b/pyModeS/decoder/bds/bds60.py @@ -3,11 +3,13 @@ # Heading and speed report # ------------------------------------------ -from pyModeS import common -from pyModeS.extra import aero +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: @@ -66,7 +68,7 @@ def is60(msg): return True -def hdg60(msg): +def hdg60(msg: str) -> Optional[float]: """Megnetic heading of aircraft Args: @@ -92,10 +94,10 @@ def hdg60(msg): if hdg < 0: hdg = 360 + hdg - return round(hdg, 3) + return hdg -def ias60(msg): +def ias60(msg: str) -> Optional[float]: """Indicated airspeed Args: @@ -113,7 +115,7 @@ def ias60(msg): return ias -def mach60(msg): +def mach60(msg: str) -> Optional[float]: """Aircraft MACH number Args: @@ -128,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: @@ -157,8 +159,8 @@ def vr60baro(msg): return roc -def vr60ins(msg): - """Vertical rate measured by onboard equipment (IRS, AHRS) +def vr60ins(msg: str) -> Optional[int]: + """Vertical rate measurd by onbard equiments (IRS, AHRS) Args: msg (str): 28 hexdigits string diff --git a/pyModeS/decoder/bds/bds61.py b/pyModeS/decoder/bds/bds61.py index 22e7343..e71e018 100644 --- a/pyModeS/decoder/bds/bds61.py +++ b/pyModeS/decoder/bds/bds61.py @@ -4,7 +4,7 @@ # Aircraft Airborne status # ------------------------------------------ -from pyModeS import common +from ... import common def is_emergency(msg: str) -> bool: @@ -18,7 +18,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]) @@ -72,7 +74,9 @@ 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) diff --git a/pyModeS/decoder/bds/bds62.py b/pyModeS/decoder/bds/bds62.py index 2d070d7..4b822de 100644 --- a/pyModeS/decoder/bds/bds62.py +++ b/pyModeS/decoder/bds/bds62.py @@ -5,10 +5,11 @@ # ------------------------------------------ from __future__ import annotations -from pyModeS import common + +from ... import common -def selected_altitude(msg): +def selected_altitude(msg: str) -> tuple[None | float, str]: """Decode selected altitude. Args: @@ -31,18 +32,20 @@ def selected_altitude(msg): 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 + "%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 + 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): +def target_altitude(msg: str) -> tuple[None | int, str, str]: """Decode target altitude. Args: @@ -51,7 +54,8 @@ def target_altitude(msg): 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') + string: Altitude reference, either pressure altitude or barometric + corrected altitude ('FL' or 'MSL') """ @@ -66,13 +70,13 @@ def target_altitude(msg): 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 + "%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 + return None, "N/A", "" elif alt_avail == 1: alt_source = "MCP/FCU" elif alt_avail == 2: @@ -87,7 +91,7 @@ def target_altitude(msg): return alt, alt_source, alt_ref -def vertical_mode(msg): +def vertical_mode(msg: str) -> None | int: """Decode vertical mode. Value Meaning @@ -115,8 +119,8 @@ def vertical_mode(msg): 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 + "%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]) @@ -126,7 +130,7 @@ def vertical_mode(msg): return vertical_mode -def horizontal_mode(msg): +def horizontal_mode(msg: str) -> None | int: """Decode horizontal mode. Value Meaning @@ -154,8 +158,8 @@ def horizontal_mode(msg): 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 + "%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]) @@ -165,7 +169,7 @@ def horizontal_mode(msg): return horizontal_mode -def selected_heading(msg): +def selected_heading(msg: str) -> None | float: """Decode selected heading. Args: @@ -187,12 +191,12 @@ def selected_heading(msg): 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 + "%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 + return None else: hdg_sign = int(mb[30]) hdg = (hdg_sign + 1) * common.bin2int(mb[31:39]) * (180 / 256) @@ -201,7 +205,7 @@ def selected_heading(msg): return hdg -def target_angle(msg): +def target_angle(msg: str) -> tuple[None | int, str, str]: """Decode target heading/track angle. Args: @@ -225,13 +229,13 @@ def target_angle(msg): 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 + "%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 + return None, "", "N/A" else: angle = common.bin2int(mb[27:36]) @@ -247,7 +251,7 @@ def target_angle(msg): return angle, angle_type, angle_source -def baro_pressure_setting(msg): +def baro_pressure_setting(msg: str) -> None | float: """Decode barometric pressure setting. Args: @@ -269,14 +273,15 @@ def baro_pressure_setting(msg): if subtype == 0: raise RuntimeError( - "%s: ADS-B version 1 target state and status message does not contain barometric pressure setting" - % msg + "%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 round(800 + (baro - 1) * 0.8, 1) + if baro == 0: + return None - return baro + return 800 + (baro - 1) * 0.8 def autopilot(msg) -> None | bool: @@ -301,8 +306,8 @@ def autopilot(msg) -> None | bool: if subtype == 0: raise RuntimeError( - "%s: ADS-B version 1 target state and status message does not contain autopilot engagement" - % msg + "%s: ADS-B version 1 target state and status message does not " + "contain autopilot engagement" % msg ) if int(mb[46]) == 0: @@ -335,8 +340,8 @@ def vnav_mode(msg) -> None | bool: 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 + "%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: @@ -369,8 +374,8 @@ def altitude_hold_mode(msg) -> None | bool: if subtype == 0: raise RuntimeError( - "%s: ADS-B version 1 target state and status message does not contain altitude hold mode" - % msg + "%s: ADS-B version 1 target state and status message does not " + "contain altitude hold mode" % msg ) if int(mb[46]) == 0: @@ -403,8 +408,8 @@ def approach_mode(msg) -> None | bool: if subtype == 0: raise RuntimeError( - "%s: ADS-B version 1 target state and status message does not contain approach mode" - % msg + "%s: ADS-B version 1 target state and status message does not " + "contain approach mode" % msg ) if int(mb[46]) == 0: @@ -437,8 +442,8 @@ def lnav_mode(msg) -> None | bool: 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 + "%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: @@ -499,8 +504,8 @@ def tcas_ra(msg) -> bool: if subtype == 1: raise RuntimeError( - "%s: ADS-B version 2 target state and status message does not contain TCAS/ACAS RA" - % msg + "%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 @@ -541,8 +546,8 @@ def emergency_status(msg) -> int: if subtype == 1: raise RuntimeError( - "%s: ADS-B version 2 target state and status message does not contain emergency status" - % msg + "%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 d49d293..3a214c6 100644 --- a/pyModeS/decoder/commb.py +++ b/pyModeS/decoder/commb.py @@ -23,18 +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 -from pyModeS.py_common import fs, dr, um +__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 8533f7f..849bef5 100644 --- a/pyModeS/decoder/ehs.py +++ b/pyModeS/decoder/ehs.py @@ -11,20 +11,51 @@ 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) 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/surv.py b/pyModeS/decoder/surv.py index 4e54614..d1dbf3a 100644 --- a/pyModeS/decoder/surv.py +++ b/pyModeS/decoder/surv.py @@ -2,14 +2,19 @@ Decode short roll call surveillance replies, with downlink format 4 or 5 """ -from pyModeS import common -from pyModeS.py_common import fs, dr, um +from __future__ import annotations +from typing import Callable, TypeVar + +from .. import common + +T = TypeVar("T") +F = Callable[[str], T] -def _checkdf(func): +def _checkdf(func: F[T]) -> F[T]: """Ensure downlink format is 4 or 5.""" - def wrapper(msg): + def wrapper(msg: str) -> T: df = common.df(msg) if df not in [4, 5]: raise RuntimeError( @@ -21,7 +26,92 @@ def _checkdf(func): @_checkdf -def altitude(msg): +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: @@ -35,7 +125,7 @@ def altitude(msg): @_checkdf -def identity(msg): +def identity(msg: str) -> str: """Decode squawk code. Args: diff --git a/pyModeS/decoder/uncertainty.py b/pyModeS/decoder/uncertainty.py index 2a923ab..122603a 100644 --- a/pyModeS/decoder/uncertainty.py +++ b/pyModeS/decoder/uncertainty.py @@ -1,8 +1,11 @@ """Uncertainty parameters. -See source code at: https://github.com/junzis/pyModeS/blob/master/pyModeS/decoder/uncertainty.py """ +from __future__ import annotations + +from typing import TypedDict + NA = None TC_NUCp_lookup = { @@ -26,7 +29,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 +49,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,20 +70,32 @@ TC_NICv2_lookup = { } -NUCp = { - 9: {"HPL": 7.5, "RCu": 3}, - 8: {"HPL": 25, "RCu": 10}, - 7: {"HPL": 185, "RCu": 93}, - 6: {"HPL": 370, "RCu": 185}, - 5: {"HPL": 926, "RCu": 463}, - 4: {"HPL": 1852, "RCu": 926}, - 3: {"HPL": 3704, "RCu": 1852}, - 2: {"HPL": 18520, "RCu": 9260}, - 1: {"HPL": 37040, "RCu": 18520}, - 0: {"HPL": NA, "RCu": NA}, +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}, + 6: {"HPL": 370, "RCu": 185, "RCv": NA}, + 5: {"HPL": 926, "RCu": 463, "RCv": NA}, + 4: {"HPL": 1852, "RCu": 926, "RCv": NA}, + 3: {"HPL": 3704, "RCu": 1852, "RCv": NA}, + 2: {"HPL": 18520, "RCu": 9260, "RCv": NA}, + 1: {"HPL": 37040, "RCu": 18520, "RCv": NA}, + 0: {"HPL": NA, "RCu": NA, "RCv": NA}, } -NUCv = { + +class NUCvEntry(TypedDict): + HVE: None | float + VVE: None | float + + +NUCv: dict[int, NUCvEntry] = { 0: {"HVE": NA, "VVE": NA}, 1: {"HVE": 10, "VVE": 15.2}, 2: {"HVE": 3, "VVE": 4.5}, @@ -88,7 +103,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 +124,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 +138,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 +152,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 +173,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 5afe2d1..fa7bf9d 100644 --- a/pyModeS/decoder/uplink.py +++ b/pyModeS/decoder/uplink.py @@ -1,9 +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) @@ -20,22 +21,22 @@ 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): - """Decode requested BDS register from selective (Roll Call) interrogation.""" +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(msg) in {4, 5, 20, 21}: - - di = mbytes[1] & 0x7 # DI - Designator Identification + 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 @@ -46,7 +47,9 @@ def bds(msg): 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) + # 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: @@ -55,7 +58,7 @@ def bds(msg): return None -def pr(msg): +def pr(msg: str) -> Optional[int]: """Decode PR (probability of reply) field from All Call interrogation. Interpretation: 0 signifies reply with probability of 1 @@ -80,7 +83,7 @@ def pr(msg): return None -def ic(msg): +def ic(msg: str) -> Optional[str]: """Decode IC (interrogator code) from a ground-based interrogation.""" UF = uf(msg) @@ -88,8 +91,7 @@ def ic(msg): msgbin_split = wrap(msgbin, 8) mbytes = list(map(common.bin2int, msgbin_split)) IC = None - BDS2 = "" - if uf(msg) == 11: + if UF == 11: codeLabel = mbytes[1] & 0x7 icField = (mbytes[1] >> 3) & 0xF @@ -104,11 +106,11 @@ def ic(msg): } IC = ic_switcher.get(codeLabel, "") - if uf(msg) in {4, 5, 20, 21}: + if UF in {4, 5, 20, 21}: di = mbytes[1] & 0x7 RR = mbytes[1] >> 3 & 0x1F if RR > 15: - BDS1 = RR - 16 + BDS1 = RR - 16 # noqa: F841 if di == 0 or di == 1 or di == 7: # II II = (mbytes[2] >> 4) & 0xF @@ -148,7 +150,6 @@ def uplink_fields(msg): msgbin_split = wrap(msgbin, 8) mbytes = list(map(common.bin2int, msgbin_split)) PR = "" - LOS = "" IC = "" lockout = False di = "" @@ -156,13 +157,11 @@ def uplink_fields(msg): 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 @@ -179,7 +178,8 @@ def uplink_fields(msg): 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) + # Decode the DI and get the lockout information conveniently + # (LSS or LOS) # DI - Designator Identification di = mbytes[1] & 0x7 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/test_adsb.py b/tests/test_adsb.py index a735023..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) @@ -91,13 +97,12 @@ def test_adsb_target_state_status(): 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 - + 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_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 From 9d82f9b9c92d6340c9fe56df3f6d0be8222cf45b Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Tue, 1 Nov 2022 20:38:29 +0900 Subject: [PATCH 61/64] compatibility with Python 3.7 --- pyModeS/decoder/uncertainty.py | 7 ++++++- setup.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyModeS/decoder/uncertainty.py b/pyModeS/decoder/uncertainty.py index 122603a..00cf350 100644 --- a/pyModeS/decoder/uncertainty.py +++ b/pyModeS/decoder/uncertainty.py @@ -4,7 +4,12 @@ from __future__ import annotations -from typing import TypedDict +import sys + +if sys.version_info < (3, 8): + from typing_extensions import TypedDict +else: + from typing import TypedDict NA = None diff --git a/setup.py b/setup.py index f977af5..172cc0b 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,8 @@ details = dict( ], keywords="Mode-S ADS-B EHS ELS Comm-B", packages=find_packages(exclude=["contrib", "docs", "tests"]), - install_requires=["numpy", "pyzmq"], + # 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"]}, scripts=["pyModeS/streamer/modeslive"], From 0667c1b3c377867e2cff9aad0e0a467230844d84 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Tue, 1 Nov 2022 20:45:47 +0900 Subject: [PATCH 62/64] include Python 3.11 --- .github/workflows/run-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4322ac2..6e88061 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] env: PYTHON_VERSION: ${{ matrix.python-version }} @@ -31,6 +31,7 @@ jobs: pip install . - name: Type checking + if: ${{ env.PYTHON_VERSION != '3.7' }} run: | mypy pyModeS From 87e6f5048666418bc281a32cc043ca9a7ada5ec0 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Sun, 4 Dec 2022 00:04:55 +0100 Subject: [PATCH 63/64] FLARM decoder --- pyModeS/decoder/flarm/__init__.py | 25 ++++++ pyModeS/decoder/flarm/core.h | 13 +++ pyModeS/decoder/flarm/core.pxd | 4 + pyModeS/decoder/flarm/decode.pyi | 14 +++ pyModeS/decoder/flarm/decode.pyx | 145 ++++++++++++++++++++++++++++++ setup.py | 47 +++++++++- 6 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 pyModeS/decoder/flarm/__init__.py create mode 100644 pyModeS/decoder/flarm/core.h create mode 100644 pyModeS/decoder/flarm/core.pxd create mode 100644 pyModeS/decoder/flarm/decode.pyi create mode 100644 pyModeS/decoder/flarm/decode.pyx 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/setup.py b/setup.py index 172cc0b..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 @@ -46,17 +48,54 @@ details = dict( # 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"]}, + 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", + ] + + 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) From 90f35a8f294a150e2585a279f91b751d56baccf8 Mon Sep 17 00:00:00 2001 From: Xavier Olive Date: Sun, 4 Dec 2022 00:08:25 +0100 Subject: [PATCH 64/64] FLARM decoder --- pyModeS/decoder/flarm/core.c | 124 +++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 pyModeS/decoder/flarm/core.c diff --git a/pyModeS/decoder/flarm/core.c b/pyModeS/decoder/flarm/core.c new file mode 100644 index 0000000..f3d9089 --- /dev/null +++ b/pyModeS/decoder/flarm/core.c @@ -0,0 +1,124 @@ +#include "core.h" + +/* + * + * https://pastebin.com/YK2f8bfm + * + * NEW ENCRYPTION + * + * Swiss glider anti-colission system moved to a new encryption scheme: XXTEA + * The algorithm encrypts all the packet after the header: total 20 bytes or 5 long int words of data + * + * XXTEA description and code are found here: http://en.wikipedia.org/wiki/XXTEA + * The system uses 6 iterations of the main loop. + * + * The system version 6 sends two type of packets: position and ... some unknown data + * The difference is made by bit 0 of byte 3 of the packet: for position data this bit is zero. + * + * For position data the key used depends on the time and transmitting device address. + * The key is as well obscured by a weird algorithm. + * The code to generate the key is: + * + * */ + +void make_key(int *key, long time, long address) +{ + const long key1[4] = {0xe43276df, 0xdca83759, 0x9802b8ac, 0x4675a56b}; + const long key1b[4] = {0xfc78ea65, 0x804b90ea, 0xb76542cd, 0x329dfa32}; + const long *table = ((((time >> 23) & 255) & 0x01) != 0) ? key1b : key1; + + for (int i = 0; i < 4; i++) + { + key[i] = obscure(table[i] ^ ((time >> 6) ^ address), 0x045D9F3B) ^ 0x87B562F4; + } +} + +long obscure(long key, unsigned long seed) +{ + unsigned int m1 = seed * (key ^ (key >> 16)); + unsigned int m2 = seed * (m1 ^ (m1 >> 16)); + return m2 ^ (m2 >> 16); +} + +/* + * NEW PACKET FORMAT: + * + * Byte Bits + * 0 AAAA AAAA device address + * 1 AAAA AAAA + * 2 AAAA AAAA + * 3 00aa 0000 aa = 10 or 01 + * + * 4 vvvv vvvv vertical speed + * 5 xxxx xxvv + * 6 gggg gggg GPS status + * 7 tttt gggg plane type + * + * 8 LLLL LLLL Latitude + * 9 LLLL LLLL + * 10 aaaa aLLL + * 11 aaaa aaaa Altitude + * + * 12 NNNN NNNN Longitude + * 13 NNNN NNNN + * 14 xxxx NNNN + * 15 FFxx xxxx multiplying factor + * + * 16 SSSS SSSS as in version 4 + * 17 ssss ssss + * 18 KKKK KKKK + * 19 kkkk kkkk + * + * 20 EEEE EEEE + * 21 eeee eeee + * 22 PPPP PPPP + * 24 pppp pppp + * */ + +/* + * https://en.wikipedia.org/wiki/XXTEA + */ + +void btea(uint32_t *v, int n, uint32_t const key[4]) +{ + uint32_t y, z, sum; + unsigned p, rounds, e; + if (n > 1) + { /* Coding Part */ + /* Unused, should remove? */ + rounds = 6 + 52 / n; + sum = 0; + z = v[n - 1]; + do + { + sum += DELTA; + e = (sum >> 2) & 3; + for (p = 0; p < (unsigned)n - 1; p++) + { + y = v[p + 1]; + z = v[p] += MX; + } + y = v[0]; + z = v[n - 1] += MX; + } while (--rounds); + } + else if (n < -1) + { /* Decoding Part */ + n = -n; + rounds = 6; // + 52 / n; + sum = rounds * DELTA; + y = v[0]; + do + { + e = (sum >> 2) & 3; + for (p = n - 1; p > 0; p--) + { + z = v[p - 1]; + y = v[p] -= MX; + } + z = v[n - 1]; + y = v[0] -= MX; + sum -= DELTA; + } while (--rounds); + } +} \ No newline at end of file