From ef2268127c9922b9aada40385f133abe57733adc Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 22 Mar 2017 21:06:51 +0100 Subject: [PATCH] Major funciton renaming in EHS for suppporting more BDS types. Bug fixes. --- README.rst | 58 +++++++---- pyModeS/adsb.py | 6 +- pyModeS/ehs.py | 211 +++++++++++++++++++++++++++++--------- tests/sample_data_test.py | 31 +++--- tests/test_adsb.py | 5 +- tests/test_ehs.py | 32 +++--- tests/test_util.py | 6 +- 7 files changed, 250 insertions(+), 99 deletions(-) diff --git a/README.rst b/README.rst index a0e823f..944be9a 100644 --- a/README.rst +++ b/README.rst @@ -9,11 +9,14 @@ implemented to decode the following messages: - aircraft information that contains: ICAO address, position, altitude, velocity (ground speed), callsign, etc. -- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21) - - - additional information in response to SSR interrogation, such as: - true airspeed, indicated airspeed, mach number, track angle, - heading, roll angle, etc. +- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21). Additional information in response to SSR interrogation, such as: true airspeed, indicated airspeed, mach number, wind, temperature, etc. + - BDS 2,0 Aircraft identification + - BDS 2,1 Aircraft and airline registration markings + - BDS 4,0 Selected vertical intention + - BDS 4,4 Meteorological routine air report + - BDS 5,0 Track and turn report + - BDS 5,3 Air-referenced state vector + - BDS 6,0 Heading and speed report A detailed manual on Mode-S decoding is published by the author, at: http://adsb-decode-guide.readthedocs.io @@ -93,26 +96,45 @@ Core functions for EHS decoding: pms.ehs.BDS(msg) # Comm-B Data Selector Version # for BDS version 2,0 + pms.ehs.isBDS20(msg) # Check if message is BDS 2,0 pms.ehs.callsign(msg) # Aircraft callsign # for BDS version 4,0 - pms.ehs.alt_mcp(msg) # MCP/FCU selected altitude (ft) - pms.ehs.alt_fms(msg) # FMS selected altitude (ft) - pms.ehs.alt_pbaro(msg) # Barometric pressure (mb) + pms.ehs.isBDS40(msg) # Check if message is BDS 4,0 + pms.ehs.alt40mcp(msg) # MCP/FCU selected altitude (ft) + pms.ehs.alt40fms(msg) # FMS selected altitude (ft) + pms.ehs.p40baro(msg) # Barometric pressure (mb) + + # for BDS version 4,4 + pms.ehs.isBDS44(msg, rev=False) # Check if message is BDS 4,4 + pms.ehs.wind44(msg, rev=False) # wind speed (kt) and heading (deg) + pms.ehs.temp44(msg, rev=False) # temperature (C) + pms.ehs.p44(msg, rev=False) # pressure (hPa) + pms.ehs.hum44(msg, rev=False) # humidity (%) # for BDS version 5,0 - pms.ehs.roll(msg) # roll angle (deg) - pms.ehs.track(msg) # track angle (deg) - pms.ehs.gs(msg) # ground speed (kt) - pms.ehs.rtrack(msg) # track angle rate (deg/sec) - pms.ehs.tas(msg) # true airspeed (kt) + pms.ehs.isBDS50(msg) # Check if message is BDS 5,0 + pms.ehs.roll50(msg) # roll angle (deg) + pms.ehs.trk50(msg) # track angle (deg) + pms.ehs.gs50(msg) # ground speed (kt) + pms.ehs.rtrk50(msg) # track angle rate (deg/sec) + pms.ehs.tas50(msg) # true airspeed (kt) + + # for BDS version 5,3 + pms.ehs.isBDS53(msg) # Check if message is BDS 5,3 + pms.ehs.hdg53(msg) # magnetic heading (deg) + pms.ehs.ias53(msg) # indicated airspeed (kt) + pms.ehs.mach53(msg) # MACH number + pms.ehs.tas53(msg) # true airspeed (kt) + pms.ehs.vr53(msg) # vertical rate (fpm) # for BDS version 6,0 - pms.ehs.heading(msg) # heading (deg) - pms.ehs.ias(msg) # indicated airspeed (kt) - pms.ehs.mach(msg) # MACH number - pms.ehs.baro_vr(msg) # barometric altitude rate (ft/min) - pms.ehs.ins_vr(msg) # inertial vertical speed (ft/min) + pms.ehs.isBDS60(msg) # Check if message is BDS 6,0 + pms.ehs.hdg60(msg) # heading (deg) + pms.ehs.ias60(msg) # indicated airspeed (kt) + pms.ehs.mach60(msg) # MACH number + pms.ehs.vr60baro(msg) # barometric altitude rate (ft/min) + pms.ehs.vr60ins(msg) # inertial vertical speed (ft/min) Developement ------------ diff --git a/pyModeS/adsb.py b/pyModeS/adsb.py index 1391ffa..a777c72 100644 --- a/pyModeS/adsb.py +++ b/pyModeS/adsb.py @@ -18,7 +18,7 @@ A python package for decoding ABS-D messages. """ import math -from . import util +import util def df(msg): @@ -638,6 +638,7 @@ def surface_velocity(msg): hdg_status = int(msgbin[44]) if hdg_status == 1: hdg = util.bin2int(msgbin[45:52]) * 360.0 / 128.0 + hdg = round(hdg, 1) else: hdg = None @@ -656,5 +657,6 @@ def surface_velocity(msg): 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) - return round(spd, 2), round(hdg, 1), 0, 'GS' + return spd, hdg, 0, 'GS' diff --git a/pyModeS/ehs.py b/pyModeS/ehs.py index a868c12..459b6e5 100644 --- a/pyModeS/ehs.py +++ b/pyModeS/ehs.py @@ -17,8 +17,8 @@ A python package for decoding ModeS (DF20, DF21) messages. """ -from . import util -from .util import crc +import util +from util import crc def df(msg): @@ -163,7 +163,7 @@ def isBDS40(msg): return result -def alt_mcp(msg): +def alt40mcp(msg): """Selected altitude, MCP/FCU Args: @@ -177,7 +177,7 @@ def alt_mcp(msg): return alt -def alt_fms(msg): +def alt40fms(msg): """Selected altitude, FMS Args: @@ -191,7 +191,7 @@ def alt_fms(msg): return alt -def pbaro(msg): +def p40baro(msg): """Barometric pressure setting Args: @@ -209,7 +209,7 @@ def pbaro(msg): # DF 20/21, BDS 4,4 # ------------------------------------------ -def isBDS44(msg, rev=True): +def isBDS44(msg, rev=False): """Check if a message is likely to be BDS code 4,4 Meteorological routine air report @@ -238,18 +238,18 @@ def isBDS44(msg, rev=True): & checkbits(d, 15, 16, 23) & checkbits(d, 24, 25, 35) \ & checkbits(d, 36, 37, 47) & checkbits(d, 49, 50, 56) - vw = wind(msg, rev=rev) + vw = wind44(msg, rev=rev) if vw and vw[0] > 250: result &= False - # if temperature(msg): - # if temperature(msg) > 60 or temperature(msg) < -80: + # if temp44(msg): + # if temp44(msg) > 60 or temp44(msg) < -80: # result &= False return result -def wind(msg, rev=True): +def wind44(msg, rev=False): """reported wind speed and direction Args: @@ -282,7 +282,7 @@ def wind(msg, rev=True): return round(speed, 0), round(direction, 1) -def temperature(msg, rev=True): +def temp44(msg, rev=False): """reported air temperature Args: @@ -308,10 +308,10 @@ def temperature(msg, rev=True): temp = util.bin2int(d[25:35]) * 0.125 # celsius temp = round(temp, 1) - return temp if sign else -1*temp + return -1*temp if sign else temp -def pressure(msg, rev=True): +def p44(msg, rev=False): """reported average static pressure Args: @@ -340,7 +340,7 @@ def pressure(msg, rev=True): return p -def humidity(msg, rev=True): +def hum44(msg, rev=False): """reported humidity Args: @@ -371,10 +371,12 @@ def humidity(msg, rev=True): # ------------------------------------------ # DF 20/21, BDS 5,0 +# Track and turn report # ------------------------------------------ def isBDS50(msg): """Check if a message is likely to be BDS code 5,0 + (Track and turn report) Args: msg (String): 28 bytes hexadecimal message string @@ -395,23 +397,23 @@ def isBDS50(msg): if d[2:11] == "000000000": result &= True else: - if abs(roll(msg)) > 30: + if abs(roll50(msg)) > 30: result &= False - if gs(msg) > 600: + if gs50(msg) > 600: result &= False - if tas(msg) > 500: + if tas50(msg) > 500: result &= False - if abs(tas(msg) - gs(msg)) > 100: + if abs(tas50(msg) - gs50(msg)) > 100: result &= False return result -def roll(msg): - """Aircraft roll angle +def roll50(msg): + """Roll angle, BDS 5,0 message Args: msg (String): 28 bytes hexadecimal message (BDS50) string @@ -422,13 +424,13 @@ def roll(msg): """ d = util.hex2bin(data(msg)) sign = int(d[1]) # 1 -> left wing down - value = util.bin2int(d[2:11]) * 45 / 256.0 # degree + value = util.bin2int(d[2:11]) * 45.0 / 256.0 # degree angle = -1 * value if sign else value return round(angle, 1) -def track(msg): - """True track angle +def trk50(msg): + """True track angle, BDS 5,0 message Args: msg (String): 28 bytes hexadecimal message (BDS50) string @@ -438,13 +440,13 @@ def track(msg): """ d = util.hex2bin(data(msg)) sign = int(d[12]) # 1 -> west - value = util.bin2int(d[13:23]) * 90 / 512.0 # degree + value = util.bin2int(d[13:23]) * 90.0 / 512.0 # degree angle = 360 - value if sign else value return round(angle, 1) -def gs(msg): - """Aircraft ground speed +def gs50(msg): + """Ground speed, BDS 5,0 message Args: msg (String): 28 bytes hexadecimal message (BDS50) string @@ -457,8 +459,8 @@ def gs(msg): return spd -def rtrack(msg): - """Track angle rate +def rtrk50(msg): + """Track angle rate, BDS 5,0 message Args: msg (String): 28 bytes hexadecimal message (BDS50) string @@ -468,13 +470,13 @@ def rtrack(msg): """ d = util.hex2bin(data(msg)) sign = int(d[35]) # 1 -> minus - value = util.bin2int(d[36:45]) * 8 / 256.0 # degree / sec + value = util.bin2int(d[36:45]) * 8.0 / 256.0 # degree / sec angle = -1 * value if sign else value return round(angle, 3) -def tas(msg): - """Aircraft true airspeed +def tas50(msg): + """Aircraft true airspeed, BDS 5,0 message Args: msg (String): 28 bytes hexadecimal message (BDS50) string @@ -483,8 +485,121 @@ def tas(msg): int: true airspeed in knots """ d = util.hex2bin(data(msg)) - spd = util.bin2int(d[46:56]) * 2 # kts - return spd + tas = util.bin2int(d[46:56]) * 2 # kts + return tas + + +# ------------------------------------------ +# DF 20/21, BDS 5,3 +# Air-referenced state vector +# ------------------------------------------ + +def isBDS53(msg): + """Check if a message is likely to be BDS code 5,3 + (Air-referenced state vector) + + Args: + msg (String): 28 bytes hexadecimal message string + + Returns: + bool: True or False + """ + + # status bit 1, 13, 24, 34, 47 + d = util.hex2bin(data(msg)) + + result = True + + result = result & checkbits(d, 1, 3, 12) & checkbits(d, 13, 14, 23) \ + & checkbits(d, 24, 25, 33) & checkbits(d, 34, 35, 46) \ + & checkbits(d, 47, 49, 56) + + if ias53(msg) > 500: + result &= False + + if mach53(msg) > 1: + result &= False + + if tas53(msg) > 500: + result &= False + + if abs(vr53(msg)) > 8000: + result &= False + + return result + + +def hdg53(msg): + """Magnetic heading, BDS 5,3 message + + Args: + msg (String): 28 bytes hexadecimal message (BDS53) string + + Returns: + float: angle in degrees to true north (from 0 to 360) + """ + d = util.hex2bin(data(msg)) + sign = int(d[1]) # 1 -> west + value = util.bin2int(d[2:12]) * 90.0 / 512.0 # degree + angle = 360 - value if sign else value + return round(angle, 1) + + +def ias53(msg): + """Indicated airspeed, DBS 5,3 message + + Args: + msg (String): 28 bytes hexadecimal message + + Returns: + int: indicated arispeed in knots + """ + d = util.hex2bin(data(msg)) + ias = util.bin2int(d[13:23]) # knots + return ias + + +def mach53(msg): + """MACH number, DBS 5,3 message + + Args: + msg (String): 28 bytes hexadecimal message + + Returns: + float: MACH number + """ + d = util.hex2bin(data(msg)) + mach = util.bin2int(d[24:33]) * 0.008 + return round(mach, 3) + + +def tas53(msg): + """Aircraft true airspeed, BDS 5,3 message + + Args: + msg (String): 28 bytes hexadecimal message + + Returns: + float: true airspeed in knots + """ + d = util.hex2bin(data(msg)) + tas = util.bin2int(d[34:46]) * 0.5 # kts + return round(tas, 1) + +def vr53(msg): + """Vertical rate + + Args: + msg (String): 28 bytes hexadecimal message (BDS60) string + + Returns: + int: vertical rate in feet/minutes + """ + d = util.hex2bin(data(msg)) + sign = d[47] # 1 -> minus + value = util.bin2int(d[48:56]) * 64 # feet/min + roc = -1*value if sign else value + return roc # ------------------------------------------ @@ -509,22 +624,22 @@ def isBDS60(msg): & checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \ & checkbits(d, 46, 47, 56) - if not (1 < ias(msg) < 500): + if not (1 < ias60(msg) < 500): result &= False - if not (0.0 < mach(msg) < 1.0): + if not (0.0 < mach60(msg) < 1.0): result &= False - if abs(baro_vr(msg)) > 5000: + if abs(vr60baro(msg)) > 5000: result &= False - if abs(ins_vr(msg)) > 5000: + if abs(vr60ins(msg)) > 5000: result &= False return result -def heading(msg): +def hdg60(msg): """Megnetic heading of aircraft Args: @@ -540,7 +655,7 @@ def heading(msg): return round(hdg, 1) -def ias(msg): +def ias60(msg): """Indicated airspeed Args: @@ -554,7 +669,7 @@ def ias(msg): return ias -def mach(msg): +def mach60(msg): """Aircraft MACH number Args: @@ -568,7 +683,7 @@ def mach(msg): return round(mach, 3) -def baro_vr(msg): +def vr60baro(msg): """Vertical rate from barometric measurement Args: @@ -584,7 +699,7 @@ def baro_vr(msg): return roc -def ins_vr(msg): +def vr60ins(msg): """Vertical rate messured by onbard equiments (IRS, AHRS) Args: @@ -610,15 +725,19 @@ def BDS(msg): String or None: Version: "BDS20", "BDS40", "BDS50", or "BDS60". Or None, if nothing matched """ is20 = isBDS20(msg) - is44 = isBDS44(msg) is40 = isBDS40(msg) + is44 = isBDS44(msg) + is44rev = isBDS44(msg, rev=True) is50 = isBDS50(msg) + is53 = isBDS53(msg) is60 = isBDS60(msg) - BDS = ["BDS20", "BDS40", "BDS44", "BDS50", "BDS60"] - isBDS = [is20, is40, is44, is50, is60] + BDS = ["BDS20", "BDS40", "BDS44", "BDS44REV", "BDS50", "BDS53", "BDS60"] + isBDS = [is20, is40, is44, is44rev, is50, is53, is60] + if sum(isBDS) == 0: + return None if sum(isBDS) == 1: return BDS[isBDS.index(True)] else: - return None + return [bds for (bds, i) in zip(BDS, isBDS) if i] diff --git a/tests/sample_data_test.py b/tests/sample_data_test.py index 83da4d2..5a2a67b 100644 --- a/tests/sample_data_test.py +++ b/tests/sample_data_test.py @@ -1,7 +1,6 @@ -# If you get import error run with ipython -from pyModeS import adsb -from pyModeS import ehs -from pyModeS import util +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+'/pyModeS') +import adsb, ehs, util # === Decode sample data file === @@ -54,27 +53,33 @@ def ehs_decode_all(n=None): vBDS = ehs.BDS(m) if vBDS: + if isinstance(vBDS, list): + print(ts, m, icao, vBDS) if vBDS == "BDS20": print(ts, m, icao, vBDS, ehs.callsign(m)) if vBDS == "BDS40": - print(ts, m, icao, vBDS, ehs.alt_mcp(m), - ehs.alt_fms(m), ehs.pbaro(m)) + print(ts, m, icao, vBDS, ehs.alt40mcp(m), + ehs.alt40fms(m), ehs.p40baro(m)) if vBDS == "BDS44": - print(ts, m, icao, vBDS, ehs.wind(m), - ehs.temperature(m), ehs.pressure(m)) + print(ts, m, icao, vBDS, ehs.wind44(m), + ehs.temp44(m), ehs.p44(m)) if vBDS == "BDS50": - print(ts, m, icao, vBDS, ehs.roll(m), ehs.track(m), - ehs.gs(m), ehs.rtrack(m), ehs.tas(m)) + print(ts, m, icao, vBDS, ehs.roll50(m), ehs.trk50(m), + ehs.gs50(m), ehs.rtrk50(m), ehs.tas50(m)) + + if vBDS == "BDS53": + print(ts, m, icao, vBDS, ehs.hdg53(m), ehs.ias53(m), + ehs.mach53(m), ehs.tas53(m), ehs.vr53(m)) if vBDS == "BDS60": - print(ts, m, icao, vBDS, ehs.heading(m), ehs.ias(m), - ehs.mach(m), ehs.baro_vr(m), ehs.ins_vr(m)) + print(ts, m, icao, vBDS, ehs.hdg60(m), ehs.ias60(m), + ehs.mach60(m), ehs.vr60baro(m), ehs.vr60ins(m)) else: print(ts, m, icao, 'UNKNOWN') if __name__ == '__main__': - adsb_decode_all(100) + # adsb_decode_all(100) ehs_decode_all(100) diff --git a/tests/test_adsb.py b/tests/test_adsb.py index 22bbb81..3fb0f82 100644 --- a/tests/test_adsb.py +++ b/tests/test_adsb.py @@ -1,5 +1,6 @@ -from pyModeS import adsb - +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+'/pyModeS') +import adsb # === TEST ADS-B package === diff --git a/tests/test_ehs.py b/tests/test_ehs.py index e549295..3422ff8 100644 --- a/tests/test_ehs.py +++ b/tests/test_ehs.py @@ -1,4 +1,6 @@ -from pyModeS import ehs +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+'/pyModeS') +import ehs def test_ehs_icao(): @@ -12,8 +14,6 @@ def test_ehs_BDS(): assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40' assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50' assert ehs.BDS("A000029CFFBAA11E2004727281F1") == 'BDS60' - assert ehs.BDS("A0281838CAE9E12FA03FFF2DDDE5") == 'BDS44' - assert ehs.BDS("A00017B0C8480030A4000024512F") is None def test_ehs_BDS20_callsign(): @@ -22,22 +22,22 @@ def test_ehs_BDS20_callsign(): def test_ehs_BDS40_functions(): - assert ehs.alt_mcp("A000029C85E42F313000007047D3") == 3008 - assert ehs.alt_fms("A000029C85E42F313000007047D3") == 3008 - assert ehs.pbaro("A000029C85E42F313000007047D3") == 1020.0 + assert ehs.alt40mcp("A000029C85E42F313000007047D3") == 3008 + assert ehs.alt40fms("A000029C85E42F313000007047D3") == 3008 + assert ehs.p40baro("A000029C85E42F313000007047D3") == 1020.0 def test_ehs_BDS50_functions(): - assert ehs.roll("A000139381951536E024D4CCF6B5") == 2.1 - assert ehs.track("A000139381951536E024D4CCF6B5") == 114.3 - assert ehs.gs("A000139381951536E024D4CCF6B5") == 438 - assert ehs.rtrack("A000139381951536E024D4CCF6B5") == 0.125 - assert ehs.tas("A000139381951536E024D4CCF6B5") == 424 + assert ehs.roll50("A000139381951536E024D4CCF6B5") == 2.1 + assert ehs.trk50("A000139381951536E024D4CCF6B5") == 114.3 + assert ehs.gs50("A000139381951536E024D4CCF6B5") == 438 + assert ehs.rtrk50("A000139381951536E024D4CCF6B5") == 0.125 + assert ehs.tas50("A000139381951536E024D4CCF6B5") == 424 def test_ehs_BDS60_functions(): - assert ehs.heading("A000029CFFBAA11E2004727281F1") == 180.9 - assert ehs.ias("A000029CFFBAA11E2004727281F1") == 336 - assert ehs.mach("A000029CFFBAA11E2004727281F1") == 0.48 - assert ehs.baro_vr("A000029CFFBAA11E2004727281F1") == 0 - assert ehs.ins_vr("A000029CFFBAA11E2004727281F1") == -3648 + assert ehs.hdg60("A000029CFFBAA11E2004727281F1") == 180.9 + assert ehs.ias60("A000029CFFBAA11E2004727281F1") == 336 + assert ehs.mach60("A000029CFFBAA11E2004727281F1") == 0.48 + assert ehs.vr60baro("A000029CFFBAA11E2004727281F1") == 0 + assert ehs.vr60ins("A000029CFFBAA11E2004727281F1") == -3648 diff --git a/tests/test_util.py b/tests/test_util.py index 758bf4e..d2a6704 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,4 +1,6 @@ -from pyModeS import util +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+'/pyModeS') +import util def test_hex2bin(): @@ -12,4 +14,4 @@ def test_crc_decode(): def test_crc_encode(): parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True) - assert util.hex2bin("AA4BDA") == parity \ No newline at end of file + assert util.hex2bin("AA4BDA") == parity