diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py
index c43679b..b8a4cca 100644
--- a/pyModeS/__init__.py
+++ b/pyModeS/__init__.py
@@ -2,8 +2,10 @@ from __future__ import absolute_import, print_function, division
from .decoder.util import *
from .decoder import adsb
+from .decoder import els
from .decoder import ehs
from .decoder import util
from .decoder import modes
+from .decoder import bds
from .extra import aero
from .extra import beastclient
diff --git a/pyModeS/decoder/bds/__init__.py b/pyModeS/decoder/bds/__init__.py
new file mode 100644
index 0000000..bf10e0a
--- /dev/null
+++ b/pyModeS/decoder/bds/__init__.py
@@ -0,0 +1,101 @@
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+from pyModeS.extra import aero
+from pyModeS.decoder.modes import allzeros
+from pyModeS.decoder.bds.bds10 import isBDS10
+from pyModeS.decoder.bds.bds17 import isBDS17
+from pyModeS.decoder.bds.bds20 import isBDS20
+from pyModeS.decoder.bds.bds30 import isBDS30
+from pyModeS.decoder.bds.bds40 import isBDS40
+from pyModeS.decoder.bds.bds50 import isBDS50, trk50, gs50
+from pyModeS.decoder.bds.bds60 import isBDS60, hdg60, mach60, ias60
+
+
+def is50or60(msg, spd_ref, trk_ref, alt_ref):
+ """Use reference ground speed and trk to determine BDS50 and DBS60
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+ spd_ref (float): reference speed (ADS-B ground speed), kts
+ trk_ref (float): reference track (ADS-B track angle), deg
+ alt_ref (float): reference altitude (ADS-B altitude), ft
+
+ Returns:
+ String or None: BDS version, or possible versions, or None if nothing matches.
+ """
+ def vxy(v, angle):
+ vx = v * np.sin(np.deg2rad(angle))
+ vy = v * np.cos(np.deg2rad(angle))
+ return vx, vy
+
+ if not (isBDS50(msg) and isBDS60(msg)):
+ return None
+
+ h50 = trk50(msg)
+ v50 = gs50(msg)
+ h50 = np.nan if h50 is None else h50
+ v50 = np.nan if v50 is None else v50
+
+ h60 = hdg60(msg)
+ m60 = mach60(msg)
+ i60 = ias60(msg)
+ h60 = np.nan if h60 is None else h60
+ m60 = np.nan if m60 is None else m60
+ i60 = np.nan if i60 is None else i60
+
+ 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)
+
+ allbds = ['BDS50', 'BDS60', 'BDS60']
+
+ X = np.array([XY5, XY6m, XY6i])
+ Mu = np.array(vxy(spd_ref*aero.kts, trk_ref*aero.kts))
+
+ # compute Mahalanobis distance matrix
+ # Cov = [[20**2, 0], [0, 20**2]]
+ # mmatrix = np.sqrt(np.dot(np.dot(X-Mu, np.linalg.inv(Cov)), (X-Mu).T))
+ # dist = np.diag(mmatrix)
+
+ # since the covariance matrix is identity matrix,
+ # M-dist is same as eculidian distance
+ dist = np.linalg.norm(X-Mu, axis=1)
+ BDS = allbds[np.nanargmin(dist)]
+
+ return BDS
+
+
+def infer(msg):
+ """Estimate the most likely BDS code of an message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ String or None: BDS version, or possible versions, or None if nothing matches.
+ """
+
+ if allzeros(msg):
+ return None
+
+ is10 = isBDS10(msg)
+ is17 = isBDS17(msg)
+ is20 = isBDS20(msg)
+ is30 = isBDS30(msg)
+ is40 = isBDS40(msg)
+ is50 = isBDS50(msg)
+ is60 = isBDS60(msg)
+
+ allbds = np.array([
+ "BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60"
+ ])
+
+ isBDS = [is10, is17, is20, is30, is40, is50, is60]
+
+ bds = ','.join(sorted(allbds[isBDS]))
+
+ if len(bds) == 0:
+ return None
+ else:
+ return bds
diff --git a/pyModeS/decoder/bds/bds10.py b/pyModeS/decoder/bds/bds10.py
new file mode 100644
index 0000000..26b28bf
--- /dev/null
+++ b/pyModeS/decoder/bds/bds10.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros
+
+# ------------------------------------------
+# BDS 1,0
+# Data link capability report
+# ------------------------------------------
+
+def isBDS10(msg):
+ """Check if a message is likely to be BDS code 1,0
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ # first 8 bits must be 0x10
+ if d[0:8] != '00010000':
+ return False
+
+ # bit 10 to 14 are reserved
+ if bin2int(d[9:14]) != 0:
+ return False
+
+ # overlay capabilty conflict
+ if d[14] == '1' and bin2int(d[16:23]) < 5:
+ return False
+ if d[14] == '0' and bin2int(d[16:23]) > 4:
+ return False
+
+ return True
+
+def ovc10(msg):
+ """Return the overlay control capability
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ int: Whether the transponder is OVC capable
+ """
+ d = hex2bin(data(msg))
+
+ return int(d[14])
diff --git a/pyModeS/decoder/bds/bds17.py b/pyModeS/decoder/bds/bds17.py
new file mode 100644
index 0000000..5c6c5dd
--- /dev/null
+++ b/pyModeS/decoder/bds/bds17.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros
+
+# ------------------------------------------
+# BDS 1,7
+# Common usage GICB capability report
+# ------------------------------------------
+
+def isBDS17(msg):
+ """Check if a message is likely to be BDS code 1,7
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ if bin2int(d[28:56]) != 0:
+ return False
+
+ caps = cap17(msg)
+
+ # basic BDS codes for ADS-B shall be supported
+ # assuming ADS-B out is installed (2017EU/2020US mandate)
+ if not set(['BDS05', 'BDS06', 'BDS09', 'BDS20']).issubset(caps):
+ return False
+
+ return True
+
+def cap17(msg):
+ """Extract capacities from BDS 1,7 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ list: list of suport BDS codes
+ """
+ allbds = ['05', '06', '07', '08', '09', '0A', '20', '21', '40', '41',
+ '42', '43', '44', '45', '48', '50', '51', '52', '53', '54',
+ '55', '56', '5F', '60', 'NA', 'NA', 'E1', 'E2']
+
+ d = hex2bin(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']
+
+ return capacity
diff --git a/pyModeS/decoder/bds/bds20.py b/pyModeS/decoder/bds/bds20.py
new file mode 100644
index 0000000..7d40a55
--- /dev/null
+++ b/pyModeS/decoder/bds/bds20.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros
+
+# ------------------------------------------
+# BDS 2,0
+# Aircraft identification
+# ------------------------------------------
+
+def isBDS20(msg):
+ """Check if a message is likely to be BDS code 2,0
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ # status bit 1, 14, and 27
+
+ if bin2int(d[0:4]) != 2 or bin2int(d[4:8]) != 0:
+ return False
+
+ cs = callsign(msg)
+
+ if '#' in cs:
+ return False
+
+ return True
+
+
+def callsign(msg):
+ """Aircraft callsign
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS40) string
+
+ Returns:
+ string: callsign, max. 8 chars
+ """
+ chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
+
+ d = hex2bin(data(msg))
+
+ cs = ''
+ cs += chars[bin2int(d[8:14])]
+ cs += chars[bin2int(d[14:20])]
+ cs += chars[bin2int(d[20:26])]
+ cs += chars[bin2int(d[26:32])]
+ cs += chars[bin2int(d[32:38])]
+ cs += chars[bin2int(d[38:44])]
+ cs += chars[bin2int(d[44:50])]
+ cs += chars[bin2int(d[50:56])]
+
+ return cs
diff --git a/pyModeS/decoder/bds/bds30.py b/pyModeS/decoder/bds/bds30.py
new file mode 100644
index 0000000..c075130
--- /dev/null
+++ b/pyModeS/decoder/bds/bds30.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros
+
+# ------------------------------------------
+# BDS 3,0
+# ACAS active resolution advisory
+# ------------------------------------------
+
+def isBDS30(msg):
+ """Check if a message is likely to be BDS code 2,0
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ if d[0:8] != '00110000':
+ return False
+
+ # threat type 3 not assigned
+ if d[28:30] == '11':
+ return False
+
+ # reserved for ACAS III, in far future
+ if bin2int(d[15:22]) >= 48:
+ return False
+
+ return True
diff --git a/pyModeS/decoder/bds/bds40.py b/pyModeS/decoder/bds/bds40.py
new file mode 100644
index 0000000..00c719e
--- /dev/null
+++ b/pyModeS/decoder/bds/bds40.py
@@ -0,0 +1,114 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros, wrongstatus
+
+# ------------------------------------------
+# BDS 4,0
+# Selected vertical intention
+# ------------------------------------------
+
+def isBDS40(msg):
+ """Check if a message is likely to be BDS code 4,0
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ # status bit 1, 14, and 27
+
+ if wrongstatus(d, 1, 2, 13):
+ return False
+
+ if wrongstatus(d, 14, 15, 26):
+ return False
+
+ if wrongstatus(d, 27, 28, 39):
+ return False
+
+ # bits 40-47 and 52-53 shall all be zero
+
+ if bin2int(d[39:47]) != 0:
+ return False
+
+ if bin2int(d[51:53]) != 0:
+ return False
+
+ return True
+
+
+def alt40mcp(msg):
+ """Selected altitude, MCP/FCU
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS40) string
+
+ Returns:
+ int: altitude in feet
+ """
+ d = hex2bin(data(msg))
+
+ if d[0] == '0':
+ return None
+
+ alt = bin2int(d[1:13]) * 16 # ft
+ return alt
+
+
+def alt40fms(msg):
+ """Selected altitude, FMS
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS40) string
+
+ Returns:
+ int: altitude in feet
+ """
+ d = hex2bin(data(msg))
+
+ if d[13] == '0':
+ return None
+
+ alt = bin2int(d[14:26]) * 16 # ft
+ return alt
+
+
+def p40baro(msg):
+ """Barometric pressure setting
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS40) string
+
+ Returns:
+ float: pressure in millibar
+ """
+ d = hex2bin(data(msg))
+
+ if d[26] == '0':
+ return None
+
+ p = bin2int(d[27:39]) * 0.1 + 800 # millibar
+ return p
diff --git a/pyModeS/decoder/bds/bds44.py b/pyModeS/decoder/bds/bds44.py
new file mode 100644
index 0000000..d3a9ef2
--- /dev/null
+++ b/pyModeS/decoder/bds/bds44.py
@@ -0,0 +1,219 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros, wrongstatus
+
+# ------------------------------------------
+# BDS 4,4
+# Meteorological routine air report
+# ------------------------------------------
+
+def isBDS44(msg, rev=False):
+ """Check if a message is likely to be BDS code 4,4
+ Meteorological routine air report
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+ rev (bool): using revised version
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+
+ if not rev:
+ # status bit 5, 35, 47, 50
+ if wrongstatus(d, 5, 6, 23):
+ return False
+
+ if wrongstatus(d, 35, 36, 46):
+ return False
+
+ if wrongstatus(d, 47, 48, 49):
+ return False
+
+ if wrongstatus(d, 50, 51, 56):
+ return False
+
+ # Bits 1-4 indicate source, values > 4 reserved and should not occur
+ if bin2int(d[0:4]) > 4:
+ return False
+ else:
+ # status bit 5, 15, 24, 36, 49
+ if wrongstatus(d, 5, 6, 14):
+ return False
+
+ if wrongstatus(d, 15, 16, 23):
+ return False
+
+ if wrongstatus(d, 24, 25, 35):
+ return False
+
+ if wrongstatus(d, 36, 37, 47):
+ return False
+
+ if wrongstatus(d, 49, 50, 56):
+ return False
+
+ # Bits 1-4 are reserved and should be zero
+ if bin2int(d[0:4]) != 0:
+ return False
+
+ vw = wind44(msg, rev=rev)
+ if vw is not None and vw[0] > 250:
+ return False
+
+ if temp44(msg):
+ if temp44(msg) > 60 or temp44(msg) < -80:
+ return False
+
+ elif temp44(msg) == 0:
+ return False
+
+ return True
+
+
+def wind44(msg, rev=False):
+ """reported wind speed and direction
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS44) string
+ rev (bool): using revised version
+
+ Returns:
+ (int, float): speed (kt), direction (degree)
+ """
+ d = hex2bin(data(msg))
+
+ if not rev:
+ status = int(d[4])
+ if not status:
+ return None
+
+ speed = bin2int(d[5:14]) # knots
+ direction = bin2int(d[14:23]) * 180.0 / 256.0 # degree
+
+ else:
+ spd_status = int(d[4])
+ dir_status = int(d[14])
+
+ if (not spd_status) or (not dir_status):
+ return None
+
+ speed = bin2int(d[5:14]) # knots
+ direction = bin2int(d[15:23]) * 180.0 / 128.0 # degree
+
+ return round(speed, 0), round(direction, 1)
+
+
+def temp44(msg, rev=False):
+ """reported air temperature
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS44) string
+ rev (bool): using revised version
+
+ Returns:
+ float: tmeperature in Celsius degree
+ """
+ d = hex2bin(data(msg))
+
+ if not rev:
+ # if d[22] == '0':
+ # return None
+
+ sign = int(d[23])
+ value = bin2int(d[24:34])
+
+ if sign:
+ value = value - 1024
+
+ temp = value * 0.125 # celsius
+ temp = round(temp, 1)
+ else:
+ # if d[23] == '0':
+ # return None
+
+ sign = int(d[24])
+ value = bin2int(d[25:35])
+
+ if sign:
+ value = value - 1024
+
+ temp = value * 0.125 # celsius
+ temp = round(temp, 1)
+
+ return -1*temp if sign else temp
+
+
+def p44(msg, rev=False):
+ """reported average static pressure
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS44) string
+ rev (bool): using revised version
+
+ Returns:
+ int: static pressure in hPa
+ """
+ d = hex2bin(data(msg))
+
+ if not rev:
+ if d[34] == '0':
+ return None
+
+ p = bin2int(d[35:46]) # hPa
+
+ else:
+ if d[35] == '0':
+ return None
+
+ p = bin2int(d[36:47]) # hPa
+
+ return p
+
+
+def hum44(msg, rev=False):
+ """reported humidity
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS44) string
+ rev (bool): using revised version
+
+ Returns:
+ float: percentage of humidity, [0 - 100] %
+ """
+ d = hex2bin(data(msg))
+
+ if not rev:
+ if d[49] == '0':
+ return None
+
+ hm = bin2int(d[50:56]) * 100.0 / 64 # %
+
+ else:
+ if d[48] == '0':
+ return None
+
+ hm = bin2int(d[49:56]) # %
+
+ return round(hm, 1)
diff --git a/pyModeS/decoder/bds/bds50.py b/pyModeS/decoder/bds/bds50.py
new file mode 100644
index 0000000..7d759bd
--- /dev/null
+++ b/pyModeS/decoder/bds/bds50.py
@@ -0,0 +1,190 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros, wrongstatus
+
+# ------------------------------------------
+# 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
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ # status bit 1, 12, 24, 35, 46
+
+ if wrongstatus(d, 1, 3, 11):
+ return False
+
+ if wrongstatus(d, 12, 13, 23):
+ return False
+
+ if wrongstatus(d, 24, 25, 34):
+ return False
+
+ if wrongstatus(d, 35, 36, 45):
+ return False
+
+ if wrongstatus(d, 46, 47, 56):
+ return False
+
+ if d[2:11] != "000000000":
+ roll = abs(roll50(msg))
+ if roll and roll > 60:
+ return False
+
+ gs = gs50(msg)
+ if gs is not None and gs > 600:
+ return False
+
+ tas = tas50(msg)
+ if tas is not None and tas > 500:
+ return False
+
+ if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200):
+ return False
+
+ return True
+
+
+def roll50(msg):
+ """Roll angle, BDS 5,0 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS50) string
+
+ Returns:
+ float: angle in degrees,
+ negative->left wing down, positive->right wing down
+ """
+ d = hex2bin(data(msg))
+
+ if d[0] == '0':
+ return None
+
+ sign = int(d[1]) # 1 -> left wing down
+ value = bin2int(d[2:11])
+
+ if sign:
+ value = value - 512
+
+ angle = value * 45.0 / 256.0 # degree
+ return round(angle, 1)
+
+
+def trk50(msg):
+ """True track angle, BDS 5,0 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS50) string
+
+ Returns:
+ float: angle in degrees to true north (from 0 to 360)
+ """
+ d = hex2bin(data(msg))
+
+ if d[11] == '0':
+ return None
+
+ sign = int(d[12]) # 1 -> west
+ value = bin2int(d[13:23])
+
+ if sign:
+ value = value - 1024
+
+ trk = value * 90.0 / 512.0
+
+ # convert from [-180, 180] to [0, 360]
+ if trk < 0:
+ trk = 360 + trk
+
+ return round(trk, 3)
+
+
+def gs50(msg):
+ """Ground speed, BDS 5,0 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS50) string
+
+ Returns:
+ int: ground speed in knots
+ """
+ d = hex2bin(data(msg))
+
+ if d[23] == '0':
+ return None
+
+ spd = bin2int(d[24:34]) * 2 # kts
+ return spd
+
+
+def rtrk50(msg):
+ """Track angle rate, BDS 5,0 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS50) string
+
+ Returns:
+ float: angle rate in degrees/second
+ """
+ d = hex2bin(data(msg))
+
+ if d[34] == '0':
+ return None
+
+ if d[36:45] == "111111111":
+ return None
+
+ sign = int(d[35]) # 1 -> negative value, two's complement
+ value = bin2int(d[36:45])
+ if sign:
+ value = value - 512
+
+ angle = value * 8.0 / 256.0 # degree / sec
+ return round(angle, 3)
+
+
+def tas50(msg):
+ """Aircraft true airspeed, BDS 5,0 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS50) string
+
+ Returns:
+ int: true airspeed in knots
+ """
+ d = hex2bin(data(msg))
+
+ if d[45] == '0':
+ return None
+
+ tas = bin2int(d[46:56]) * 2 # kts
+ return tas
diff --git a/pyModeS/decoder/bds/bds53.py b/pyModeS/decoder/bds/bds53.py
new file mode 100644
index 0000000..6275548
--- /dev/null
+++ b/pyModeS/decoder/bds/bds53.py
@@ -0,0 +1,182 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros, wrongstatus
+
+# ------------------------------------------
+# 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
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ # status bit 1, 13, 24, 34, 47
+
+ if wrongstatus(d, 1, 3, 12):
+ return False
+
+ if wrongstatus(d, 13, 14, 23):
+ return False
+
+ if wrongstatus(d, 24, 25, 33):
+ return False
+
+ if wrongstatus(d, 34, 35, 46):
+ return False
+
+ if wrongstatus(d, 47, 49, 56):
+ return False
+
+ ias = ias53(msg)
+ if ias is not None and ias > 500:
+ return False
+
+ mach = mach53(msg)
+ if mach is not None and mach > 1:
+ return False
+
+ tas = tas53(msg)
+ if tas is not None and tas > 500:
+ return False
+
+ vr = vr53(msg)
+ if vr is not None and abs(vr) > 8000:
+ return False
+
+ return True
+
+
+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 = hex2bin(data(msg))
+
+ if d[0] == '0':
+ return None
+
+ sign = int(d[1]) # 1 -> west
+ value = bin2int(d[2:12])
+
+ if sign:
+ value = value - 1024
+
+ hdg = value * 90.0 / 512.0 # degree
+
+ # convert from [-180, 180] to [0, 360]
+ if hdg < 0:
+ hdg = 360 + hdg
+
+ return round(hdg, 3)
+
+
+def ias53(msg):
+ """Indicated airspeed, DBS 5,3 message
+
+ Args:
+ msg (String): 28 bytes hexadecimal message
+
+ Returns:
+ int: indicated arispeed in knots
+ """
+ d = hex2bin(data(msg))
+
+ if d[12] == '0':
+ return None
+
+ ias = 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 = hex2bin(data(msg))
+
+ if d[23] == '0':
+ return None
+
+ mach = 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 = hex2bin(data(msg))
+
+ if d[33] == '0':
+ return None
+
+ tas = 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 = hex2bin(data(msg))
+
+ if d[46] == '0':
+ return None
+
+ sign = int(d[47]) # 1 -> negative value, two's complement
+ value = bin2int(d[48:56])
+
+ if value == 0 or value == 255: # all zeros or all ones
+ return 0
+
+ value = value - 256 if sign else value
+ roc = value * 64 # feet/min
+
+ return roc
diff --git a/pyModeS/decoder/bds/bds60.py b/pyModeS/decoder/bds/bds60.py
new file mode 100644
index 0000000..9836942
--- /dev/null
+++ b/pyModeS/decoder/bds/bds60.py
@@ -0,0 +1,190 @@
+# Copyright (C) 2018 Junzi Sun (TU Delft)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import absolute_import, print_function, division
+from pyModeS.decoder.util import hex2bin, bin2int
+from pyModeS.decoder.modes import data, allzeros, wrongstatus
+
+# ------------------------------------------
+# BDS 6,0
+# Heading and speed report
+# ------------------------------------------
+
+def isBDS60(msg):
+ """Check if a message is likely to be BDS code 6,0
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+
+ if allzeros(msg):
+ return False
+
+ d = hex2bin(data(msg))
+
+ # status bit 1, 13, 24, 35, 46
+
+ if wrongstatus(d, 1, 2, 12):
+ return False
+
+ if wrongstatus(d, 13, 14, 23):
+ return False
+
+ if wrongstatus(d, 24, 25, 34):
+ return False
+
+ if wrongstatus(d, 35, 36, 45):
+ return False
+
+ if wrongstatus(d, 46, 47, 56):
+ return False
+
+ ias = ias60(msg)
+ if ias is not None and ias > 500:
+ return False
+
+ mach = mach60(msg)
+ if mach is not None and mach > 1:
+ return False
+
+ vr_baro = vr60baro(msg)
+ if vr_baro is not None and abs(vr_baro) > 6000:
+ return False
+
+ vr_ins = vr60ins(msg)
+ if vr_ins is not None and abs(vr_ins) > 6000:
+ return False
+
+ return True
+
+
+def hdg60(msg):
+ """Megnetic heading of aircraft
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS60) string
+
+ Returns:
+ float: heading in degrees to megnetic north (from 0 to 360)
+ """
+ d = hex2bin(data(msg))
+
+ if d[0] == '0':
+ return None
+
+ sign = int(d[1]) # 1 -> west
+ value = bin2int(d[2:12])
+
+ if sign:
+ value = value - 1024
+
+ hdg = value * 90 / 512.0 # degree
+
+ # convert from [-180, 180] to [0, 360]
+ if hdg < 0:
+ hdg = 360 + hdg
+
+ return round(hdg, 3)
+
+
+def ias60(msg):
+ """Indicated airspeed
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS60) string
+
+ Returns:
+ int: indicated airspeed in knots
+ """
+ d = hex2bin(data(msg))
+
+ if d[12] == '0':
+ return None
+
+ ias = bin2int(d[13:23]) # kts
+ return ias
+
+
+def mach60(msg):
+ """Aircraft MACH number
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS60) string
+
+ Returns:
+ float: MACH number
+ """
+ d = hex2bin(data(msg))
+
+ if d[23] == '0':
+ return None
+
+ mach = bin2int(d[24:34]) * 2.048 / 512.0
+ return round(mach, 3)
+
+
+def vr60baro(msg):
+ """Vertical rate from barometric measurement, this value may be very noisy.
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS60) string
+
+ Returns:
+ int: vertical rate in feet/minutes
+ """
+ d = hex2bin(data(msg))
+
+ if d[34] == '0':
+ return None
+
+ sign = int(d[35]) # 1 -> negative value, two's complement
+ value = bin2int(d[36:45])
+
+ if value == 0 or value == 511: # all zeros or all ones
+ return 0
+
+ value = value - 512 if sign else value
+
+ roc = value * 32 # feet/min
+ return roc
+
+
+def vr60ins(msg):
+ """Vertical rate messured by onbard equiments (IRS, AHRS)
+
+ Args:
+ msg (String): 28 bytes hexadecimal message (BDS60) string
+
+ Returns:
+ int: vertical rate in feet/minutes
+ """
+ d = hex2bin(data(msg))
+
+ if d[45] == '0':
+ return None
+
+ sign = int(d[46]) # 1 -> negative value, two's complement
+ value = bin2int(d[47:56])
+
+ if value == 0 or value == 511: # all zeros or all ones
+ return 0
+
+ value = value - 512 if sign else value
+
+ roc = value * 32 # feet/min
+ return roc
diff --git a/pyModeS/decoder/ehs.py b/pyModeS/decoder/ehs.py
index 0d5e2f7..56e8386 100644
--- a/pyModeS/decoder/ehs.py
+++ b/pyModeS/decoder/ehs.py
@@ -1,1122 +1,7 @@
-# Copyright (C) 2016 Junzi Sun (TU Delft)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-"""
-A python package for decoding ModeS (DF20, DF21) messages.
-"""
-
from __future__ import absolute_import, print_function, division
-import numpy as np
-from pyModeS.decoder import util, modes
-from pyModeS.extra import aero
-def icao(msg):
- return modes.icao(msg)
-
-def data(msg):
- """Return the data frame in the message, bytes 9 to 22"""
- return msg[8:22]
-
-def allzeros(msg):
- """check if the data bits are all zeros
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- bool: True or False
- """
- d = util.hex2bin(data(msg))
-
- if util.bin2int(d) > 0:
- return False
- else:
- return True
-
-def wrongstatus(data, sb, msb, lsb):
- """Check if the status bit and field bits are consistency. This Function
- is used for checking BDS code versions.
- """
-
- # status bit, most significant bit, least significant bit
- status = int(data[sb-1])
- value = util.bin2int(data[msb-1:lsb])
-
- if not status:
- if value != 0:
- return True
-
- return False
-
-
-# ------------------------------------------
-# BDS 1,0
-# Data link capability report
-# ------------------------------------------
-
-def isBDS10(msg):
- """Check if a message is likely to be BDS code 1,0
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- # first 8 bits must be 0x10
- if d[0:8] != '00010000':
- return False
-
- # bit 10 to 14 are reserved
- if util.bin2int(d[9:14]) != 0:
- return False
-
- # overlay capabilty conflict
- if d[14] == '1' and util.bin2int(d[16:23]) < 5:
- return False
- if d[14] == '0' and util.bin2int(d[16:23]) > 4:
- return False
-
- return True
-
-def ovc10(msg):
- """Return the overlay control capability
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- int: Whether the transponder is OVC capable
- """
- d = util.hex2bin(data(msg))
-
- return int(d[14])
-
-# ------------------------------------------
-# BDS 1,7
-# Common usage GICB capability report
-# ------------------------------------------
-
-def isBDS17(msg):
- """Check if a message is likely to be BDS code 1,7
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- if util.bin2int(d[28:56]) != 0:
- return False
-
- caps = cap17(msg)
-
- # basic BDS codes for ADS-B shall be supported
- # assuming ADS-B out is installed (2017EU/2020US mandate)
- if not set(['BDS05', 'BDS06', 'BDS09', 'BDS20']).issubset(caps):
- return False
-
- return True
-
-def cap17(msg):
- """Extract capacities from BDS 1,7 message
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- list: list of suport BDS codes
- """
- allbds = ['05', '06', '07', '08', '09', '0A', '20', '21', '40', '41',
- '42', '43', '44', '45', '48', '50', '51', '52', '53', '54',
- '55', '56', '5F', '60', 'NA', 'NA', 'E1', 'E2']
-
- d = util.hex2bin(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']
-
- return capacity
-
-
-# ------------------------------------------
-# BDS 2,0
-# Aircraft identification
-# ------------------------------------------
-
-def isBDS20(msg):
- """Check if a message is likely to be BDS code 2,0
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- # status bit 1, 14, and 27
-
- if util.bin2int(d[0:4]) != 2 or util.bin2int(d[4:8]) != 0:
- return False
-
- cs = callsign(msg)
-
- if '#' in cs:
- return False
-
- return True
-
-
-def callsign(msg):
- """Aircraft callsign
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS40) string
-
- Returns:
- string: callsign, max. 8 chars
- """
- chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
-
- d = util.hex2bin(data(msg))
-
- cs = ''
- cs += chars[util.bin2int(d[8:14])]
- cs += chars[util.bin2int(d[14:20])]
- cs += chars[util.bin2int(d[20:26])]
- cs += chars[util.bin2int(d[26:32])]
- cs += chars[util.bin2int(d[32:38])]
- cs += chars[util.bin2int(d[38:44])]
- cs += chars[util.bin2int(d[44:50])]
- cs += chars[util.bin2int(d[50:56])]
-
- return cs
-
-
-# ------------------------------------------
-# BDS 4,0
-# Selected vertical intention
-# ------------------------------------------
-
-def isBDS40(msg):
- """Check if a message is likely to be BDS code 4,0
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- # status bit 1, 14, and 27
-
- if wrongstatus(d, 1, 2, 13):
- return False
-
- if wrongstatus(d, 14, 15, 26):
- return False
-
- if wrongstatus(d, 27, 28, 39):
- return False
-
- # bits 40-47 and 52-53 shall all be zero
-
- if util.bin2int(d[39:47]) != 0:
- return False
-
- if util.bin2int(d[51:53]) != 0:
- return False
-
- return True
-
-
-def alt40mcp(msg):
- """Selected altitude, MCP/FCU
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS40) string
-
- Returns:
- int: altitude in feet
- """
- d = util.hex2bin(data(msg))
-
- if d[0] == '0':
- return None
-
- alt = util.bin2int(d[1:13]) * 16 # ft
- return alt
-
-
-def alt40fms(msg):
- """Selected altitude, FMS
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS40) string
-
- Returns:
- int: altitude in feet
- """
- d = util.hex2bin(data(msg))
-
- if d[13] == '0':
- return None
-
- alt = util.bin2int(d[14:26]) * 16 # ft
- return alt
-
-
-def p40baro(msg):
- """Barometric pressure setting
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS40) string
-
- Returns:
- float: pressure in millibar
- """
- d = util.hex2bin(data(msg))
-
- if d[26] == '0':
- return None
-
- p = util.bin2int(d[27:39]) * 0.1 + 800 # millibar
- return p
-
-
-# ------------------------------------------
-# BDS 4,4
-# Meteorological routine air report
-# ------------------------------------------
-
-def isBDS44(msg, rev=False):
- """Check if a message is likely to be BDS code 4,4
- Meteorological routine air report
-
- Args:
- msg (String): 28 bytes hexadecimal message string
- rev (bool): using revised version
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
-
- if not rev:
- # status bit 5, 35, 47, 50
- if wrongstatus(d, 5, 6, 23):
- return False
-
- if wrongstatus(d, 35, 36, 46):
- return False
-
- if wrongstatus(d, 47, 48, 49):
- return False
-
- if wrongstatus(d, 50, 51, 56):
- return False
-
- # Bits 1-4 indicate source, values > 4 reserved and should not occur
- if util.bin2int(d[0:4]) > 4:
- return False
- else:
- # status bit 5, 15, 24, 36, 49
- if wrongstatus(d, 5, 6, 14):
- return False
-
- if wrongstatus(d, 15, 16, 23):
- return False
-
- if wrongstatus(d, 24, 25, 35):
- return False
-
- if wrongstatus(d, 36, 37, 47):
- return False
-
- if wrongstatus(d, 49, 50, 56):
- return False
-
- # Bits 1-4 are reserved and should be zero
- if util.bin2int(d[0:4]) != 0:
- return False
-
- vw = wind44(msg, rev=rev)
- if vw is not None and vw[0] > 250:
- return False
-
- if temp44(msg):
- if temp44(msg) > 60 or temp44(msg) < -80:
- return False
-
- elif temp44(msg) == 0:
- return False
-
- return True
-
-
-def wind44(msg, rev=False):
- """reported wind speed and direction
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS44) string
- rev (bool): using revised version
-
- Returns:
- (int, float): speed (kt), direction (degree)
- """
- d = util.hex2bin(data(msg))
-
- if not rev:
- status = int(d[4])
- if not status:
- return None
-
- speed = util.bin2int(d[5:14]) # knots
- direction = util.bin2int(d[14:23]) * 180.0 / 256.0 # degree
-
- else:
- spd_status = int(d[4])
- dir_status = int(d[14])
-
- if (not spd_status) or (not dir_status):
- return None
-
- speed = util.bin2int(d[5:14]) # knots
- direction = util.bin2int(d[15:23]) * 180.0 / 128.0 # degree
-
- return round(speed, 0), round(direction, 1)
-
-
-def temp44(msg, rev=False):
- """reported air temperature
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS44) string
- rev (bool): using revised version
-
- Returns:
- float: tmeperature in Celsius degree
- """
- d = util.hex2bin(data(msg))
-
- if not rev:
- # if d[22] == '0':
- # return None
-
- sign = int(d[23])
- value = util.bin2int(d[24:34])
-
- if sign:
- value = value - 1024
-
- temp = value * 0.125 # celsius
- temp = round(temp, 1)
- else:
- # if d[23] == '0':
- # return None
-
- sign = int(d[24])
- value = util.bin2int(d[25:35])
-
- if sign:
- value = value - 1024
-
- temp = value * 0.125 # celsius
- temp = round(temp, 1)
-
- return -1*temp if sign else temp
-
-
-def p44(msg, rev=False):
- """reported average static pressure
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS44) string
- rev (bool): using revised version
-
- Returns:
- int: static pressure in hPa
- """
- d = util.hex2bin(data(msg))
-
- if not rev:
- if d[34] == '0':
- return None
-
- p = util.bin2int(d[35:46]) # hPa
-
- else:
- if d[35] == '0':
- return None
-
- p = util.bin2int(d[36:47]) # hPa
-
- return p
-
-
-def hum44(msg, rev=False):
- """reported humidity
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS44) string
- rev (bool): using revised version
-
- Returns:
- float: percentage of humidity, [0 - 100] %
- """
- d = util.hex2bin(data(msg))
-
- if not rev:
- if d[49] == '0':
- return None
-
- hm = util.bin2int(d[50:56]) * 100.0 / 64 # %
-
- else:
- if d[48] == '0':
- return None
-
- hm = util.bin2int(d[49:56]) # %
-
- return round(hm, 1)
-
-
-# ------------------------------------------
-# 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
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- # status bit 1, 12, 24, 35, 46
-
- if wrongstatus(d, 1, 3, 11):
- return False
-
- if wrongstatus(d, 12, 13, 23):
- return False
-
- if wrongstatus(d, 24, 25, 34):
- return False
-
- if wrongstatus(d, 35, 36, 45):
- return False
-
- if wrongstatus(d, 46, 47, 56):
- return False
-
- if d[2:11] != "000000000":
- roll = abs(roll50(msg))
- if roll and roll > 60:
- return False
-
- gs = gs50(msg)
- if gs is not None and gs > 600:
- return False
-
- tas = tas50(msg)
- if tas is not None and tas > 500:
- return False
-
- if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200):
- return False
-
- return True
-
-
-def roll50(msg):
- """Roll angle, BDS 5,0 message
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS50) string
-
- Returns:
- float: angle in degrees,
- negative->left wing down, positive->right wing down
- """
- d = util.hex2bin(data(msg))
-
- if d[0] == '0':
- return None
-
- sign = int(d[1]) # 1 -> left wing down
- value = util.bin2int(d[2:11])
-
- if sign:
- value = value - 512
-
- angle = value * 45.0 / 256.0 # degree
- return round(angle, 1)
-
-
-def trk50(msg):
- """True track angle, BDS 5,0 message
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS50) string
-
- Returns:
- float: angle in degrees to true north (from 0 to 360)
- """
- d = util.hex2bin(data(msg))
-
- if d[11] == '0':
- return None
-
- sign = int(d[12]) # 1 -> west
- value = util.bin2int(d[13:23])
-
- if sign:
- value = value - 1024
-
- trk = value * 90.0 / 512.0
-
- # convert from [-180, 180] to [0, 360]
- if trk < 0:
- trk = 360 + trk
-
- return round(trk, 3)
-
-
-def gs50(msg):
- """Ground speed, BDS 5,0 message
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS50) string
-
- Returns:
- int: ground speed in knots
- """
- d = util.hex2bin(data(msg))
-
- if d[23] == '0':
- return None
-
- spd = util.bin2int(d[24:34]) * 2 # kts
- return spd
-
-
-def rtrk50(msg):
- """Track angle rate, BDS 5,0 message
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS50) string
-
- Returns:
- float: angle rate in degrees/second
- """
- d = util.hex2bin(data(msg))
-
- if d[34] == '0':
- return None
-
- if d[36:45] == "111111111":
- return None
-
- sign = int(d[35]) # 1 -> negative value, two's complement
- value = util.bin2int(d[36:45])
- if sign:
- value = value - 512
-
- angle = value * 8.0 / 256.0 # degree / sec
- return round(angle, 3)
-
-
-def tas50(msg):
- """Aircraft true airspeed, BDS 5,0 message
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS50) string
-
- Returns:
- int: true airspeed in knots
- """
- d = util.hex2bin(data(msg))
-
- if d[45] == '0':
- return None
-
- tas = util.bin2int(d[46:56]) * 2 # kts
- return tas
-
-
-# ------------------------------------------
-# 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
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- # status bit 1, 13, 24, 34, 47
-
- if wrongstatus(d, 1, 3, 12):
- return False
-
- if wrongstatus(d, 13, 14, 23):
- return False
-
- if wrongstatus(d, 24, 25, 33):
- return False
-
- if wrongstatus(d, 34, 35, 46):
- return False
-
- if wrongstatus(d, 47, 49, 56):
- return False
-
- ias = ias53(msg)
- if ias is not None and ias > 500:
- return False
-
- mach = mach53(msg)
- if mach is not None and mach > 1:
- return False
-
- tas = tas53(msg)
- if tas is not None and tas > 500:
- return False
-
- vr = vr53(msg)
- if vr is not None and abs(vr) > 8000:
- return False
-
- return True
-
-
-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))
-
- if d[0] == '0':
- return None
-
- sign = int(d[1]) # 1 -> west
- value = util.bin2int(d[2:12])
-
- if sign:
- value = value - 1024
-
- hdg = value * 90.0 / 512.0 # degree
-
- # convert from [-180, 180] to [0, 360]
- if hdg < 0:
- hdg = 360 + hdg
-
- return round(hdg, 3)
-
-
-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))
-
- if d[12] == '0':
- return None
-
- 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))
-
- if d[23] == '0':
- return None
-
- 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))
-
- if d[33] == '0':
- return None
-
- 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))
-
- if d[46] == '0':
- return None
-
- sign = int(d[47]) # 1 -> negative value, two's complement
- value = util.bin2int(d[48:56])
-
- if value == 0 or value == 255: # all zeros or all ones
- return 0
-
- value = value - 256 if sign else value
- roc = value * 64 # feet/min
-
- return roc
-
-
-# ------------------------------------------
-# BDS 6,0
-# ------------------------------------------
-
-def isBDS60(msg):
- """Check if a message is likely to be BDS code 6,0
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- bool: True or False
- """
-
- if allzeros(msg):
- return False
-
- d = util.hex2bin(data(msg))
-
- # status bit 1, 13, 24, 35, 46
-
- if wrongstatus(d, 1, 2, 12):
- return False
-
- if wrongstatus(d, 13, 14, 23):
- return False
-
- if wrongstatus(d, 24, 25, 34):
- return False
-
- if wrongstatus(d, 35, 36, 45):
- return False
-
- if wrongstatus(d, 46, 47, 56):
- return False
-
- ias = ias60(msg)
- if ias is not None and ias > 500:
- return False
-
- mach = mach60(msg)
- if mach is not None and mach > 1:
- return False
-
- vr_baro = vr60baro(msg)
- if vr_baro is not None and abs(vr_baro) > 6000:
- return False
-
- vr_ins = vr60ins(msg)
- if vr_ins is not None and abs(vr_ins) > 6000:
- return False
-
- return True
-
-
-def hdg60(msg):
- """Megnetic heading of aircraft
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS60) string
-
- Returns:
- float: heading in degrees to megnetic north (from 0 to 360)
- """
- d = util.hex2bin(data(msg))
-
- if d[0] == '0':
- return None
-
- sign = int(d[1]) # 1 -> west
- value = util.bin2int(d[2:12])
-
- if sign:
- value = value - 1024
-
- hdg = value * 90 / 512.0 # degree
-
- # convert from [-180, 180] to [0, 360]
- if hdg < 0:
- hdg = 360 + hdg
-
- return round(hdg, 3)
-
-
-def ias60(msg):
- """Indicated airspeed
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS60) string
-
- Returns:
- int: indicated airspeed in knots
- """
- d = util.hex2bin(data(msg))
-
- if d[12] == '0':
- return None
-
- ias = util.bin2int(d[13:23]) # kts
- return ias
-
-
-def mach60(msg):
- """Aircraft MACH number
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS60) string
-
- Returns:
- float: MACH number
- """
- d = util.hex2bin(data(msg))
-
- if d[23] == '0':
- return None
-
- mach = util.bin2int(d[24:34]) * 2.048 / 512.0
- return round(mach, 3)
-
-
-def vr60baro(msg):
- """Vertical rate from barometric measurement, this value may be very noisy.
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS60) string
-
- Returns:
- int: vertical rate in feet/minutes
- """
- d = util.hex2bin(data(msg))
-
- if d[34] == '0':
- return None
-
- sign = int(d[35]) # 1 -> negative value, two's complement
- value = util.bin2int(d[36:45])
-
- if value == 0 or value == 511: # all zeros or all ones
- return 0
-
- value = value - 512 if sign else value
-
- roc = value * 32 # feet/min
- return roc
-
-
-def vr60ins(msg):
- """Vertical rate messured by onbard equiments (IRS, AHRS)
-
- Args:
- msg (String): 28 bytes hexadecimal message (BDS60) string
-
- Returns:
- int: vertical rate in feet/minutes
- """
- d = util.hex2bin(data(msg))
-
- if d[45] == '0':
- return None
-
- sign = int(d[46]) # 1 -> negative value, two's complement
- value = util.bin2int(d[47:56])
-
- if value == 0 or value == 511: # all zeros or all ones
- return 0
-
- value = value - 512 if sign else value
-
- roc = value * 32 # feet/min
- return roc
-
-
-def isBDS50or60(msg, spd_ref, trk_ref, alt_ref):
- """Use reference ground speed and trk to determine BDS50 and DBS60
-
- Args:
- msg (String): 28 bytes hexadecimal message string
- spd_ref (float): reference speed (ADS-B ground speed), kts
- trk_ref (float): reference track (ADS-B track angle), deg
- alt_ref (float): reference altitude (ADS-B altitude), ft
-
- Returns:
- String or None: BDS version, or possible versions, or None if nothing matches.
- """
- def vxy(v, angle):
- vx = v * np.sin(np.deg2rad(angle))
- vy = v * np.cos(np.deg2rad(angle))
- return vx, vy
-
- if not (isBDS50(msg) and isBDS60(msg)):
- return None
-
- h50 = trk50(msg)
- v50 = gs50(msg)
- h50 = np.nan if h50 is None else h50
- v50 = np.nan if v50 is None else v50
-
-
- h60 = hdg60(msg)
- m60 = mach60(msg)
- i60 = ias60(msg)
- h60 = np.nan if h60 is None else h60
- m60 = np.nan if m60 is None else m60
- i60 = np.nan if i60 is None else i60
-
- 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)
-
- allbds = ['BDS50', 'BDS60', 'BDS60']
-
- X = np.array([XY5, XY6m, XY6i])
- Mu = np.array(vxy(spd_ref*aero.kts, trk_ref*aero.kts))
-
- # compute Mahalanobis distance matrix
- # Cov = [[20**2, 0], [0, 20**2]]
- # mmatrix = np.sqrt(np.dot(np.dot(X-Mu, np.linalg.inv(Cov)), (X-Mu).T))
- # dist = np.diag(mmatrix)
-
- # since the covariance matrix is identity matrix,
- # M-dist is same as eculidian distance
- dist = np.linalg.norm(X-Mu, axis=1)
- BDS = allbds[np.nanargmin(dist)]
-
- return BDS
-
-
-def BDS(msg):
- """Estimate the most likely BDS code of an message
-
- Args:
- msg (String): 28 bytes hexadecimal message string
-
- Returns:
- String or None: BDS version, or possible versions, or None if nothing matches.
- """
-
- if allzeros(msg):
- return None
-
- is10 = isBDS10(msg)
- is17 = isBDS17(msg)
- is20 = isBDS20(msg)
- is40 = isBDS40(msg)
- is44 = isBDS44(msg)
- is44rev = isBDS44(msg, rev=True)
- is50 = isBDS50(msg)
- is53 = isBDS53(msg)
- is60 = isBDS60(msg)
-
- allbds = np.array([
- "BDS10", "BDS17", "BDS20", "BDS40", "BDS44", "BDS44REV",
- "BDS50", "BDS53", "BDS60"
- ])
-
- isBDS = [is10, is17, is20, is40, is44, is44rev, is50, is53, is60]
-
- bds = ','.join(sorted(allbds[isBDS]))
-
- if len(bds) == 0:
- return None
- else:
- return bds
+from pyModeS.decoder.bds.bds40 import *
+from pyModeS.decoder.bds.bds44 import *
+from pyModeS.decoder.bds.bds50 import *
+from pyModeS.decoder.bds.bds53 import *
+from pyModeS.decoder.bds.bds60 import *
diff --git a/pyModeS/decoder/els.py b/pyModeS/decoder/els.py
new file mode 100644
index 0000000..1e9f0b0
--- /dev/null
+++ b/pyModeS/decoder/els.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, print_function, division
+
+from pyModeS.decoder.bds.bds10 import *
+from pyModeS.decoder.bds.bds17 import *
+from pyModeS.decoder.bds.bds20 import *
+from pyModeS.decoder.bds.bds30 import *
diff --git a/pyModeS/decoder/modes.py b/pyModeS/decoder/modes.py
index 379f986..e61776f 100644
--- a/pyModeS/decoder/modes.py
+++ b/pyModeS/decoder/modes.py
@@ -140,3 +140,41 @@ def gray2int(graystr):
num ^= (num >> 2)
num ^= (num >> 1)
return num
+
+
+def data(msg):
+ """Return the data frame in the message, bytes 9 to 22"""
+ return msg[8:-6]
+
+
+def allzeros(msg):
+ """check if the data bits are all zeros
+
+ Args:
+ msg (String): 28 bytes hexadecimal message string
+
+ Returns:
+ bool: True or False
+ """
+ d = util.hex2bin(data(msg))
+
+ if util.bin2int(d) > 0:
+ return False
+ else:
+ return True
+
+
+def wrongstatus(data, sb, msb, lsb):
+ """Check if the status bit and field bits are consistency. This Function
+ is used for checking BDS code versions.
+ """
+
+ # status bit, most significant bit, least significant bit
+ status = int(data[sb-1])
+ value = util.bin2int(data[msb-1:lsb])
+
+ if not status:
+ if value != 0:
+ return True
+
+ return False
diff --git a/tests/test_ehs.py b/tests/test_ehs.py
deleted file mode 100644
index ce9e60b..0000000
--- a/tests/test_ehs.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from pyModeS import ehs
-from pyModeS import modes
-
-def test_ehs_icao():
- assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
- assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
- assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
-
-
-def test_modes_altcode():
- assert ehs.modes.altcode("A02014B400000000000000F9D514") == 32300
-
-def test_modes_idcode():
- assert ehs.modes.idcode("A800292DFFBBA9383FFCEB903D01") == '1346'
-
-
-def test_ehs_BDS():
- assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
- assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
- assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
- assert ehs.BDS("A00004128F39F91A7E27C46ADC21") == 'BDS60'
-
-def test_ehs_isBDS50or60():
- assert ehs.isBDS50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
- assert ehs.isBDS50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == 'BDS50'
- assert ehs.isBDS50or60("A0000000919A5927E23444000000", 413, 54, 18700) == 'BDS60'
-
-def test_ehs_BDS20_callsign():
- assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
- assert ehs.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
-
-
-def test_ehs_BDS40_functions():
- assert ehs.alt40mcp("A000029C85E42F313000007047D3") == 3008
- assert ehs.alt40fms("A000029C85E42F313000007047D3") == 3008
- assert ehs.p40baro("A000029C85E42F313000007047D3") == 1020.0
-
-def test_ehs_BDS50_functions():
- 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
- # signed values
- assert ehs.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4
-
-def test_ehs_BDS60_functions():
- assert ehs.hdg60("A00004128F39F91A7E27C46ADC21") == 42.7
- assert ehs.ias60("A00004128F39F91A7E27C46ADC21") == 252
- assert ehs.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
- assert ehs.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
- assert ehs.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
-
-def test_graycode_to_altitude():
- assert modes.gray2alt('00000000010') == -1000
- assert modes.gray2alt('00000001010') == -500
- assert modes.gray2alt('00000011011') == -100
- assert modes.gray2alt('00000011010') == 0
- assert modes.gray2alt('00000011110') == 100
- assert modes.gray2alt('00000010011') == 600
- assert modes.gray2alt('00000110010') == 1000
- assert modes.gray2alt('00001001001') == 5800
- assert modes.gray2alt('00011100100') == 10300
- assert modes.gray2alt('01100011010') == 32000
- assert modes.gray2alt('01110000100') == 46300
- assert modes.gray2alt('01010101100') == 50200
- assert modes.gray2alt('11011110100') == 73200
- assert modes.gray2alt('10000000011') == 126600
- assert modes.gray2alt('10000000001') == 126700
diff --git a/tests/test_modes.py b/tests/test_modes.py
new file mode 100644
index 0000000..e3772a3
--- /dev/null
+++ b/tests/test_modes.py
@@ -0,0 +1,66 @@
+from pyModeS import bds, modes
+
+def test_ehs_icao():
+ assert modes.icao("A0001839CA3800315800007448D9") == '400940'
+ assert modes.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
+ assert modes.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
+
+
+def test_modes_altcode():
+ assert modes.altcode("A02014B400000000000000F9D514") == 32300
+
+def test_modes_idcode():
+ assert modes.idcode("A800292DFFBBA9383FFCEB903D01") == '1346'
+
+
+def test_bds_infer():
+ assert bds.infer("A0001838201584F23468207CDFA5") == 'BDS20'
+ assert bds.infer("A0001839CA3800315800007448D9") == 'BDS40'
+ assert bds.infer("A000139381951536E024D4CCF6B5") == 'BDS50'
+ assert bds.infer("A00004128F39F91A7E27C46ADC21") == 'BDS60'
+
+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'
+
+def test_els_BDS20_callsign():
+ assert bds.bds20.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
+ assert bds.bds20.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
+
+def test_ehs_BDS40_functions():
+ assert bds.bds40.alt40mcp("A000029C85E42F313000007047D3") == 3008
+ assert bds.bds40.alt40fms("A000029C85E42F313000007047D3") == 3008
+ assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0
+
+def test_ehs_BDS50_functions():
+ assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1
+ assert bds.bds50.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
+ assert bds.bds50.trk50("A000139381951536E024D4CCF6B5") == 114.258
+ assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438
+ assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
+ assert bds.bds50.tas50("A000139381951536E024D4CCF6B5") == 424
+
+def test_ehs_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
+
+def test_graycode_to_altitude():
+ assert modes.gray2alt('00000000010') == -1000
+ assert modes.gray2alt('00000001010') == -500
+ assert modes.gray2alt('00000011011') == -100
+ assert modes.gray2alt('00000011010') == 0
+ assert modes.gray2alt('00000011110') == 100
+ assert modes.gray2alt('00000010011') == 600
+ assert modes.gray2alt('00000110010') == 1000
+ assert modes.gray2alt('00001001001') == 5800
+ assert modes.gray2alt('00011100100') == 10300
+ assert modes.gray2alt('01100011010') == 32000
+ assert modes.gray2alt('01110000100') == 46300
+ assert modes.gray2alt('01010101100') == 50200
+ assert modes.gray2alt('11011110100') == 73200
+ assert modes.gray2alt('10000000011') == 126600
+ assert modes.gray2alt('10000000001') == 126700