55 Commits

Author SHA1 Message Date
Junzi Sun
7c52db318d release version 1.2.2 2018-03-28 13:49:46 +02:00
Junzi Sun
e692d30d66 fix vertical rate 2018-03-09 13:19:12 +01:00
Junzi Sun
fd8bb8386f fix vertical rate bug in BDS60 2018-03-08 16:36:09 +01:00
Junzi Sun
2b1f2a5878 rename spd to trk 2017-12-12 23:06:03 +01:00
Junzi Sun
3538645e22 Update README.rst 2017-12-12 21:48:25 +01:00
Junzi Sun
a9887d6238 minor fix in bit sequence 2017-11-18 22:14:09 +01:00
Junzi Sun
7c8fd74db7 fix velocity message with no data; and update some comments 2017-11-15 22:33:06 +01:00
Junzi Sun
35b0d63fa9 update readme 2017-11-01 11:53:10 +01:00
Junzi Sun
2ae7bf4c19 Merge pull request #13 from hv92/patch-6
update identification function for BDS44
2017-10-24 17:59:17 +02:00
Huy Vû
e8449154ca Update ehs.py
Additional checks BDS4,4
2017-10-24 13:02:08 +02:00
Junzi Sun
fbe5b63286 Merge pull request #12 from hv92/patch-2
Temperature requirement
2017-10-11 20:34:39 +02:00
Huy Vû
45c32cd7aa Temperature requirement
Temperature range [-80,60] is a requirement in Annex 3
2017-10-11 11:38:26 +02:00
Junzi Sun
c708d57fcc Update README.rst 2017-09-19 11:19:54 +02:00
junzis
53a258bd35 version 1.2.0 2017-09-18 16:00:58 +02:00
junzis
d9e277dc54 set zero altitude when decoding surface message 2017-08-03 17:18:07 +02:00
junzis
854386fbd4 update EHS sample data 2017-07-31 14:28:26 +02:00
junzis
3117febac0 update EHS sample data 2017-07-31 14:23:59 +02:00
junzis
8693c51998 update sample run data and scripts 2017-07-27 13:41:21 +02:00
junzis
8c90371111 update BDS60 check 2017-07-26 12:02:49 +02:00
Junzi Sun
c6952e4e63 major bug fix, signed values in ehs (two's complement) 2017-07-25 23:29:03 +02:00
junzis
98e5d81ae1 update test altcode test function 2017-07-25 13:05:48 +02:00
junzis
fd557d1c40 fix DF4,20 altitude decoding 2017-07-25 12:28:06 +02:00
junzis
cdbcf47bc2 fix DF4,20 altitude decoding 2017-07-25 12:27:07 +02:00
junzis
5f7e28950c work on altitude code 2017-07-24 17:06:20 +02:00
junzis
c1d0a925d5 work on altitude code 2017-07-24 17:01:10 +02:00
junzis
b0a71717f0 work on altitude code 2017-07-24 16:59:46 +02:00
Junzi Sun
25e5a4e412 increase roll angle limit 2017-07-22 11:58:29 +02:00
junzis
d3022c6fe5 update readme 2017-07-21 17:45:08 +02:00
junzis
aa9f49b470 add DF4/20 altitude and DF5/21 squawk decoding 2017-07-21 17:40:10 +02:00
junzis
27daf52850 minor update 2017-07-21 16:02:40 +02:00
junzis
0e29a4d18a minor update 2017-07-21 15:57:32 +02:00
junzis
1e842e4789 add altitude difference function in adsb, fix bug. 2017-07-21 15:53:50 +02:00
junzis
1220368ada Merge branch 'master' of github.com:junzis/pyModeS 2017-07-21 15:17:13 +02:00
junzis
fb32ace095 update BDS 1,7 decoding 2017-07-21 15:17:02 +02:00
Junzi Sun
abafd97b3f Merge pull request #10 from hv92/patch-1
Fix altitude
2017-07-21 05:58:49 -07:00
hv92
0a38231713 Fix altitude 2017-07-21 13:58:14 +02:00
junzis
2fd822d275 update BDS 6,0 checks 2017-03-31 17:24:55 +02:00
Junzi Sun
8de58bb01f Fixed what seems to be an ICAO documentation error: sign reversed in vertical speed in BDS60 messages. 2017-03-29 21:32:40 +02:00
junzis
140f312c11 new release v1.1.1 2017-03-23 16:15:15 +01:00
junzis
fefd26a787 fix import error in Python3 2017-03-23 16:10:22 +01:00
junzis
1e82b5c59c update EHS module 2017-03-23 13:12:33 +01:00
junzis
a1615767b6 Merge branch 'master' of github.com:junzis/pyModeS 2017-03-23 12:08:36 +01:00
junzis
03a62dc68c update EHS module 2017-03-23 12:08:15 +01:00
junzis
46fee6b7dc update EHS module 2017-03-23 11:48:51 +01:00
Junzi Sun
8aa821c8dd Update requirements.txt 2017-03-22 21:25:07 +01:00
Junzi Sun
621d3e7580 new release version 2017-03-22 21:14:41 +01:00
Junzi Sun
3b3609bf2b Update README.rst 2017-03-22 21:07:59 +01:00
Junzi Sun
ef2268127c Major funciton renaming in EHS for suppporting more BDS types. Bug fixes. 2017-03-22 21:06:51 +01:00
junzis
cc66e2f4e4 version 1.0.9 2017-03-21 18:14:30 +01:00
junzis
128163b41d update BDS 4,4 decoder, default to revised version 2017-03-17 18:03:54 +01:00
Junzi Sun
8933afb1c1 Update ehs.py 2017-03-17 15:24:18 +01:00
junzis
15f2833aee add surface velocity decoding from msg TC:5-8 2017-03-07 13:41:07 +01:00
junzis
2b3e2c62d0 update package version 2017-03-07 10:35:45 +01:00
junzis
cadcbb1756 major bug fix for function adsb.position_with_ref() 2017-03-07 10:22:53 +01:00
Junzi Sun
b9c6db6f65 fix error in surface position with reference 2016-11-10 13:45:36 +01:00
20 changed files with 11155 additions and 2318 deletions

View File

@@ -1,21 +1,30 @@
A Python Mode-S Decoder
=======================
The Python Mode-S Decoder
=========================
Python library for Mode-S message decoding. Two separate methods are
implemented to decode the following messages:
Python library for Mode-S message decoding. Support Downlink Formats (DF) are:
- Automatic Dependent Surveillance - Broadcast (ADS-B) (DF17)
- aircraft information that contains: ICAO address, position,
altitude, velocity (ground speed), callsign, etc.
- aircraft information that contains: ICAO address, position, altitude, velocity (ground speed), callsign, etc.
- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21)
- Mode-S Elementary Surveillance (ELS) (DF4 and DF5).
- additional information in response to SSR interrogation, such as:
true airspeed, indicated airspeed, mach number, track angle,
heading, roll angle, etc.
- DF4: Altitude
- DF5: Squawk code
A detailed manual on Mode-S decoding is published by the author, at:
- 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.
- DF20: Altitude
- DF21: Squawk code
- 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
Detailed manual on Mode-S decoding is published by the author, at:
http://adsb-decode-guide.readthedocs.io
@@ -27,92 +36,147 @@ https://github.com/junzis/pyModeS
API documentation at:
http://pymodes.readthedocs.io
Install
-------
Checkout source code, or install using pip:
The easiest installation is to use pip:
::
pip install pyModeS
pip install pyModeS
Usage
-----
To install latest devlopment version from the GitHub:
::
pip install git+https://github.com/junzis/pyModeS
Use the library
---------------
.. code:: python
import pyModeS as pms
import pyModeS as pms
Common function for Mode-S message:
Common functions:
*****************
.. code:: python
pms.df(msg) # Downlink Format
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
pms.df(msg) # Downlink Format
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer
pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer
pms.gray2int(str) # Convert grey code to interger
Core functions for ADS-B decoding:
**********************************
.. code:: python
pms.adsb.icao(msg)
pms.adsb.callsign(msg)
pms.adsb.icao(msg)
pms.adsb.typecode(msg)
pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None)
pms.adsb.airborne_position(msg_even, msg_odd, t_even, t_odd)
pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref)
# typecode 1-4
pms.adsb.callsign(msg)
pms.adsb.position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.surface_position_with_ref(msg, lat_ref, lon_ref)
# typecode 5-8 (surface) and 9-18 (airborne)
pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None)
pms.adsb.airborne_position(msg_even, msg_odd, t_even, t_odd)
pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref)
pms.adsb.altitude(msg)
pms.adsb.velocity(msg)
pms.adsb.speed_heading(msg)
pms.adsb.position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.surface_position_with_ref(msg, lat_ref, lon_ref)
**Hint: When you have a fix position of the aircraft, it is convenient to
pms.adsb.altitude(msg)
# typecode: 19
pms.adsb.velocity(msg) # handles both surface & airborne messages
pms.adsb.speed_heading(msg) # handles both surface & airborne messages
pms.adsb.surface_velocity(msg)
pms.adsb.airborne_velocity(msg)
Note: When you have a fix position of the aircraft, it is convenient to
use `position_with_ref()` method to decode with only one position message
(either odd or even). This works with both airborne and surface position
messages. But the reference position shall be with in 180NM (airborne)
or 45NM (surface) of the true position.**
or 45NM (surface) of the true position.
Core functions for EHS decoding:
Core functions for ELS decoding:
********************************
.. code:: python
pms.ehs.icao(msg) # ICAO address
pms.ehs.BDS(msg) # Comm-B Data Selector Version
pms.els.icao(msg) # ICAO address
pms.els.df4alt(msg) # Altitude from any DF4 message
pms.ehs.df5id(msg) # Squawk code from any DF5 message
# for BDS version 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)
Core functions for EHS decoding:
********************************
# 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)
.. code:: python
# 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.icao(msg) # ICAO address
pms.ehs.df20alt(msg) # Altitude from any DF20 message
pms.ehs.df21id(msg) # Squawk code from any DF21 message
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.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.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.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
------------
To run tests, run the following commands:
```
$ tox
```
To perform unit tests. First install ``tox`` through pip, Then, run the following commands:
.. code:: bash
$ tox

View File

@@ -1 +1 @@
pyModeS==1.0.5
pyModeS==1.1.0

View File

@@ -3,3 +3,4 @@ from __future__ import absolute_import, print_function, division
from .util import *
from . import adsb
from . import ehs
from . import els

View File

@@ -17,6 +17,7 @@
A python package for decoding ABS-D messages.
"""
from __future__ import absolute_import, print_function, division
import math
from . import util
@@ -265,19 +266,18 @@ def position_with_ref(msg, lat_ref, lon_ref):
of the true position.
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
if 5 <= typecode(msg) <= 8:
return airborne_position_with_ref(msg, lat_ref, lon_ref)
return surface_position_with_ref(msg, lat_ref, lon_ref)
elif 9 <= typecode(msg) <= 18:
return surface_position_with_ref(msg, lat_ref, lon_ref)
return airborne_position_with_ref(msg, lat_ref, lon_ref)
else:
raise RuntimeError("incorrect or inconsistant message types")
@@ -427,9 +427,9 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
ni = _cprNL(lat) - i
if ni > 0:
d_lon = 360.0 / ni
d_lon = 90.0 / ni
else:
d_lon = 360.0
d_lon = 90.0
m = util.floor(lon_ref / d_lon) \
+ util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
@@ -468,9 +468,14 @@ def altitude(msg):
Returns:
int: altitude in feet
"""
if typecode(msg) < 9 or typecode(msg) > 18:
if typecode(msg) < 5 or typecode(msg) > 18:
raise RuntimeError("%s: Not a position message" % msg)
if typecode(msg) >=5 and typecode(msg) <=8:
# surface position, altitude 0
return 0
msgbin = util.hex2bin(msg)
q = msgbin[47]
if q:
@@ -491,7 +496,7 @@ def nic(msg):
int: NIC number (from 0 to 11), -1 if not applicable
"""
if typecode(msg) < 9 or typecode(msg) > 18:
raise RuntimeError("%s: Not a airborne position message" % msg)
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
msgbin = util.hex2bin(msg)
tc = typecode(msg)
@@ -534,62 +539,158 @@ def nic(msg):
def velocity(msg):
"""Calculate the speed, heading, and vertical rate
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): speed (kt), heading (degree),
(int, float, int, string): speed (kt), ground track or heading (degree),
rate of climb/descend (ft/min), and speed type
('GS' for ground speed, 'AS' for airspeed)
"""
if 5 <= typecode(msg) <= 8:
return surface_velocity(msg)
elif typecode(msg) == 19:
return airborne_velocity(msg)
else:
raise RuntimeError("incorrect or inconsistant message types, expecting 4<TC<9 or TC=19")
def speed_heading(msg):
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), ground track or heading (degree)
"""
spd, trk_or_hdg, rocd, tag = velocity(msg)
return spd, trk_or_hdg
def airborne_velocity(msg):
"""Calculate the speed, track (or heading), and vertical rate
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): speed (kt), ground track or heading (degree),
rate of climb/descend (ft/min), and speed type
('GS' for ground speed, 'AS' for airspeed)
"""
if typecode(msg) != 19:
raise RuntimeError("%s: Not a airborne velocity message" % msg)
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
msgbin = util.hex2bin(msg)
subtype = util.bin2int(msgbin[37:40])
if util.bin2int(msgbin[46:56]) == 0 or util.bin2int(msgbin[57:67]) == 0:
return None
if subtype in (1, 2):
v_ew_sign = util.bin2int(msgbin[45])
v_ew_sign = -1 if int(msgbin[45]) else 1
v_ew = util.bin2int(msgbin[46:56]) - 1 # east-west velocity
v_ns_sign = util.bin2int(msgbin[56])
v_ns_sign = -1 if int(msgbin[56]) else 1
v_ns = util.bin2int(msgbin[57:67]) - 1 # north-south velocity
v_we = -1*v_ew if v_ew_sign else v_ew
v_sn = -1*v_ns if v_ns_sign else v_ns
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
hdg = math.atan2(v_we, v_sn)
hdg = math.degrees(hdg) # convert to degrees
hdg = hdg if hdg >= 0 else hdg + 360 # no negative val
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
tag = 'GS'
trk_or_hdg = trk
else:
hdg = util.bin2int(msgbin[46:56]) / 1024.0 * 360.0
spd = util.bin2int(msgbin[57:67])
tag = 'AS'
trk_or_hdg = hdg
vr_sign = util.bin2int(msgbin[68])
vr_sign = -1 if int(msgbin[68]) else 1
vr = (util.bin2int(msgbin[69:78]) - 1) * 64 # vertical rate, fpm
rocd = -1*vr if vr_sign else vr # rate of climb/descend
rocd = vr_sign * vr
return int(spd), round(hdg, 1), int(rocd), tag
return int(spd), round(trk_or_hdg, 1), int(rocd), tag
def speed_heading(msg):
"""Get speed and heading only from the velocity message
def surface_velocity(msg):
"""Decode surface velocity from from a surface position message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), heading (degree)
(int, float, int, string): speed (kt), ground track (degree),
rate of climb/descend (ft/min), and speed type
('GS' for ground speed, 'AS' for airspeed)
"""
spd, hdg, rocd, tag = velocity(msg)
return spd, hdg
if typecode(msg) < 5 or typecode(msg) > 8:
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
msgbin = util.hex2bin(msg)
# ground track
trk_status = int(msgbin[44])
if trk_status == 1:
trk = util.bin2int(msgbin[45:52]) * 360.0 / 128.0
trk = round(trk, 1)
else:
trk = None
# ground movment / speed
mov = util.bin2int(msgbin[37:44])
if mov == 0 or mov > 124:
spd = None
elif mov == 1:
spd = 0
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)
return spd, trk, 0, 'GS'
def altitude_diff(msg):
"""Decode the differece between GNSS and barometric altitude
Args:
msg (string): 28 bytes hexadecimal message string, TC=19
Returns:
int: Altitude difference in ft. Negative value indicates GNSS altitude
below barometric altitude.
"""
if typecode(msg) != 19:
raise RuntimeError("incorrect message types, expecting TC=19")
msgbin = util.hex2bin(msg)
sign = -1 if int(msgbin[80]) else 1
value = util.bin2int(msgbin[81:88])
if value == 0 or value == 127:
return None
else:
return sign * (value - 1) * 25 # in ft.

View File

@@ -17,47 +17,31 @@
A python package for decoding ModeS (DF20, DF21) messages.
"""
from . import util
from .util import crc
def df(msg):
"""Get the downlink format (DF) number
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: DF number
"""
return util.df(msg)
from __future__ import absolute_import, print_function, division
from . import util, modes_common
def icao(msg):
return modes_common.icao(msg)
def data(msg):
"""Return the data frame in the message, bytes 9 to 22"""
return msg[8:22]
def icao(msg):
"""Calculate the ICAO address from an Mode-S message
with DF4, DF5, DF20, DF21
def isnull(msg):
"""check if the data bits are all zeros
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
bool: True or False
"""
d = util.hex2bin(data(msg))
if df(msg) not in (4, 5, 20, 21):
# raise RuntimeError("Message DF must be in (4, 5, 20, 21)")
return None
c0 = util.bin2int(crc(msg, encode=True))
c1 = util.hex2int(msg[-6:])
icao = '%06X' % (c0 ^ c1)
return icao
if util.bin2int(d) > 0:
return False
else:
return True
def checkbits(data, sb, msb, lsb):
"""Check if the status bit and field bits are consistency. This Function
@@ -74,9 +58,97 @@ def checkbits(data, sb, msb, lsb):
return True
# ------------------------------------------
# Common functions
# ------------------------------------------
def df20alt(msg):
"""Computes the altitude from DF20 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if util.df(msg) != 20:
raise RuntimeError("Message must be Downlink Format 20.")
return modes_common.altcode(msg)
def df21id(msg):
"""Computes identity (squawk code) from DF21, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if util.df(msg) != 21:
raise RuntimeError("Message must be Downlink Format 21.")
return modes_common.idcode(msg)
# ------------------------------------------
# DF 20/21, BDS 2,0
# 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 isnull(msg):
return False
d = util.hex2bin(data(msg))
result = True
if util.bin2int(d[28:56]) != 0:
result &= 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):
result &= False
return result
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):
@@ -89,6 +161,9 @@ def isBDS20(msg):
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 14, and 27
d = util.hex2bin(data(msg))
@@ -132,7 +207,8 @@ def callsign(msg):
# ------------------------------------------
# DF 20/21, BDS 4,0
# BDS 4,0
# Selected vertical intention
# ------------------------------------------
def isBDS40(msg):
@@ -145,6 +221,9 @@ def isBDS40(msg):
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 14, and 27
d = util.hex2bin(data(msg))
@@ -163,7 +242,7 @@ def isBDS40(msg):
return result
def alt_mcp(msg):
def alt40mcp(msg):
"""Selected altitude, MCP/FCU
Args:
@@ -173,11 +252,15 @@ def alt_mcp(msg):
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 alt_fms(msg):
def alt40fms(msg):
"""Selected altitude, FMS
Args:
@@ -187,11 +270,15 @@ def alt_fms(msg):
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 pbaro(msg):
def p40baro(msg):
"""Barometric pressure setting
Args:
@@ -201,134 +288,206 @@ def pbaro(msg):
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
# ------------------------------------------
# DF 20/21, BDS 4,4
# BDS 4,4
# Meteorological routine air report
# ------------------------------------------
def isBDS44(msg):
def isBDS44(msg, rev=False):
"""Check if a message is likely to be BDS code 4,4
Meteorological routine air report
WARNING: there is two different definition for BDS4,4. This part of the
decoder is likely to be wrong...
Args:
msg (String): 28 bytes hexadecimal message string
rev (bool): using revised version
Returns:
bool: True or False
"""
# status bit 5, 15, 24, 36, 49
if isnull(msg):
return False
d = util.hex2bin(data(msg))
result = True
# --- current version ---
result = result & checkbits(d, 5, 6, 23) \
& checkbits(d, 35, 36, 46) & checkbits(d, 47, 48, 49) \
& checkbits(d, 50, 51, 56)
if not rev:
# status bit 5, 35, 47, 50
result = result & checkbits(d, 5, 6, 23) \
& checkbits(d, 35, 36, 46) & checkbits(d, 47, 48, 49) \
& checkbits(d, 50, 51, 56)
# Bits 1-4 indicate source, values > 4 reserved and should not occur
if util.bin2int(d[0:4]) > 4:
result &= False
else:
# status bit 5, 15, 24, 36, 49
result = result & checkbits(d, 5, 6, 14) \
& checkbits(d, 15, 16, 23) & checkbits(d, 24, 25, 35) \
& checkbits(d, 36, 37, 47) & checkbits(d, 49, 50, 56)
# Bits 1-4 are reserved and should be zero
if util.bin2int(d[0:4]) != 0:
result &= False
# # --- revised future version ---
# result = result & checkbits(d, 5, 6, 14) \
# & checkbits(d, 15, 16, 23) & checkbits(d, 24, 25, 35) \
# & checkbits(d, 36, 37, 47) & checkbits(d, 49, 50, 56)
if not result:
return False
if wind(msg) and wind(msg)[0] > 250:
vw = wind44(msg, rev=rev)
if vw is not None and vw[0] > 250:
result &= False
# if temperature(msg):
# if temperature(msg) > 60 or temperature(msg) < -80:
# result &= False
if temp44(msg):
if temp44(msg) > 60 or temp44(msg) < -80:
result &= False
elif temp44(msg) == 0:
result &= False
return result
def wind(msg):
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))
status = int(d[4])
if not status:
return None
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
speed = util.bin2int(d[5:14]) # knots
direction = util.bin2int(d[14:23]) * 180.0 / 256.0 # degree
return round(speed, 0), round(direction, 1)
def temperature(msg):
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))
sign = int(d[23]) # 1 -> minus
temp = util.bin2int(d[24:34]) * 0.25 # celsius
temp = round(temp, 1)
return -1 * temp if sign else temp
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 pressure(msg):
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))
status = int(d[34])
if not status:
return None
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
p = util.bin2int(d[35:46]) # hPa
return p
def humidity(msg):
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))
status = int(d[49])
if not status:
return None
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]) # %
hm = util.bin2int(d[50:56]) * 100.0 / 64 # %
return round(hm, 1)
# ------------------------------------------
# DF 20/21, BDS 5,0
# 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
@@ -337,6 +496,9 @@ def isBDS50(msg):
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 12, 24, 35, 46
d = util.hex2bin(data(msg))
@@ -346,26 +508,32 @@ def isBDS50(msg):
& checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \
& checkbits(d, 46, 47, 56)
if not result:
return False
if d[2:11] == "000000000":
result &= True
else:
if abs(roll(msg)) > 30:
roll = abs(roll50(msg))
if roll and roll > 60:
result &= False
if gs(msg) > 500:
gs = gs50(msg)
if gs is not None and gs > 600:
result &= False
if tas(msg) > 500:
tas = tas50(msg)
if tas is not None and tas > 500:
result &= False
if abs(tas(msg) - gs(msg)) > 100:
if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200):
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
@@ -375,14 +543,22 @@ def roll(msg):
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]) * 45 / 256.0 # degree
angle = -1 * value if sign else value
value = util.bin2int(d[2:11])
if sign:
value = value - 512
angle = value * 45.0 / 256.0 # degree
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
@@ -391,14 +567,27 @@ def track(msg):
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]) * 90 / 512.0 # degree
angle = 360 - value if sign else value
return round(angle, 1)
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, 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
@@ -407,12 +596,16 @@ def gs(msg):
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 rtrack(msg):
"""Track angle rate
def rtrk50(msg):
"""Track angle rate, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
@@ -421,14 +614,24 @@ def rtrack(msg):
float: angle rate in degrees/second
"""
d = util.hex2bin(data(msg))
sign = int(d[35]) # 1 -> minus
value = util.bin2int(d[36:45]) * 8 / 256.0 # degree / sec
angle = -1 * value if sign else value
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 tas(msg):
"""Aircraft true airspeed
def tas50(msg):
"""Aircraft true airspeed, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
@@ -437,12 +640,174 @@ def tas(msg):
int: true airspeed in knots
"""
d = util.hex2bin(data(msg))
spd = util.bin2int(d[46:56]) * 2 # kts
return spd
if d[45] == '0':
return None
tas = util.bin2int(d[46:56]) * 2 # kts
return tas
# ------------------------------------------
# DF 20/21, BDS 6,0
# 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 isnull(msg):
return 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 not result:
return False
ias = ias53(msg)
if ias is not None and ias > 500:
result &= False
mach = mach53(msg)
if mach is not None and mach > 1:
result &= False
tas = tas53(msg)
if tas is not None and tas > 500:
result &= False
vr = vr53(msg)
if vr is not None and abs(vr) > 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))
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, 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))
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):
@@ -454,6 +819,10 @@ def isBDS60(msg):
Returns:
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 13, 24, 35, 46
d = util.hex2bin(data(msg))
@@ -463,22 +832,24 @@ 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 result:
return False
ias = ias60(msg)
if ias is not None and ias > 500:
result &= False
if not (0.0 < mach(msg) < 1.0):
mach = mach60(msg)
if mach is not None and mach > 1:
result &= False
if abs(baro_vr(msg)) > 5000:
result &= False
if abs(ins_vr(msg)) > 5000:
result &= False
# leave out the check from vertical rates,
# due to very noisy measurement
return result
def heading(msg):
def hdg60(msg):
"""Megnetic heading of aircraft
Args:
@@ -488,13 +859,26 @@ def heading(msg):
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]) * 90 / 512.0 # degree
hdg = 360 - value if sign else value
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, 1)
def ias(msg):
def ias60(msg):
"""Indicated airspeed
Args:
@@ -504,11 +888,15 @@ def ias(msg):
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 mach(msg):
def mach60(msg):
"""Aircraft MACH number
Args:
@@ -518,12 +906,16 @@ def mach(msg):
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 baro_vr(msg):
"""Vertical rate from barometric measurement
def vr60baro(msg):
"""Vertical rate from barometric measurement, this value may be very noisy.
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
@@ -532,13 +924,23 @@ def baro_vr(msg):
int: vertical rate in feet/minutes
"""
d = util.hex2bin(data(msg))
sign = d[35] # 1 -> minus
value = util.bin2int(d[36:45]) * 32 # feet/min
roc = -1*value if sign else value
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 ins_vr(msg):
def vr60ins(msg):
"""Vertical rate messured by onbard equiments (IRS, AHRS)
Args:
@@ -548,9 +950,19 @@ def ins_vr(msg):
int: vertical rate in feet/minutes
"""
d = util.hex2bin(data(msg))
sign = d[46] # 1 -> minus
value = util.bin2int(d[47:56]) * 32 # feet/min
roc = -1*value if sign else value
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
@@ -563,16 +975,25 @@ def BDS(msg):
Returns:
String or None: Version: "BDS20", "BDS40", "BDS50", or "BDS60". Or None, if nothing matched
"""
if isnull(msg):
return None
is17 = isBDS17(msg)
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 = ["BDS17", "BDS20", "BDS40", "BDS44", "BDS44REV", "BDS50", "BDS53", "BDS60"]
isBDS = [is17, is20, is40, is44, is44rev, is50, is53, is60]
if sum(isBDS) == 1:
if sum(isBDS) == 0:
return None
elif sum(isBDS) == 1:
return BDS[isBDS.index(True)]
else:
return None
return [bds for (bds, i) in zip(BDS, isBDS) if i]

37
pyModeS/els.py Normal file
View File

@@ -0,0 +1,37 @@
from __future__ import absolute_import, print_function, division
from . import util, modes_common
def icao(msg):
return modes_common.icao(msg)
def df4alt(msg):
"""Computes the altitude from DF4 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if util.df(msg) != 4:
raise RuntimeError("Message must be Downlink Format 4.")
return modes_common.altcode(msg)
def df5id(msg):
"""Computes identity (squawk code) from DF5 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if util.df(msg) != 5:
raise RuntimeError("Message must be Downlink Format 5.")
return modes_common.idcode(msg)

131
pyModeS/modes_common.py Normal file
View File

@@ -0,0 +1,131 @@
from __future__ import absolute_import, print_function, division
from . import util
def icao(msg):
"""Calculate the ICAO address from an Mode-S message
with DF4, DF5, DF20, DF21
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
if util.df(msg) not in (4, 5, 20, 21):
# raise RuntimeError("Message DF must be in (4, 5, 20, 21)")
return None
c0 = util.bin2int(util.crc(msg, encode=True))
c1 = util.hex2int(msg[-6:])
addr = '%06X' % (c0 ^ c1)
return addr
def idcode(msg):
"""Computes identity (squawk code) from DF5 or DF21 message, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if util.df(msg) not in [5, 21]:
raise RuntimeError("Message must be Downlink Format 5 or 21.")
mbin = util.hex2bin(msg)
C1 = mbin[19]
A1 = mbin[20]
C2 = mbin[21]
A2 = mbin[22]
C4 = mbin[23]
A4 = mbin[24]
# _ = mbin[25]
B1 = mbin[26]
D1 = mbin[27]
B2 = mbin[28]
D2 = mbin[29]
B4 = mbin[30]
D4 = mbin[31]
byte1 = int(A4+A2+A1, 2)
byte2 = int(B4+B2+B1, 2)
byte3 = int(C4+C2+C1, 2)
byte4 = int(D4+D2+D1, 2)
return str(byte1) + str(byte2) + str(byte3) + str(byte4)
def altcode(msg):
"""Computes the altitude from DF4 or DF20 message, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if util.df(msg) not in [4, 20]:
raise RuntimeError("Message must be Downlink Format 4 or 20.")
# Altitude code, bit 20-32
mbin = util.hex2bin(msg)
mbit = mbin[25] # M bit: 26
qbit = mbin[27] # Q bit: 28
if mbit == '0': # unit in ft
if qbit == '1': # 25ft interval
vbin = mbin[19:25] + mbin[26] + mbin[28:32]
alt = util.bin2int(vbin) * 25 - 1000
if qbit == '0': # 100ft interval, above 50175ft
C1 = mbin[19]
A1 = mbin[20]
C2 = mbin[21]
A2 = mbin[22]
C4 = mbin[23]
A4 = mbin[24]
# _ = mbin[25]
B1 = mbin[26]
# D1 = mbin[27] # always zero
B2 = mbin[28]
D2 = mbin[29]
B4 = mbin[30]
D4 = mbin[31]
graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
alt = gray2alt(graystr)
if mbit == '1': # unit in meter
vbin = mbin[19:25] + mbin[26:31]
alt = int(util.bin2int(vbin) * 3.28084) # convert to ft
return alt
def gray2alt(codestr):
gc500 = codestr[:8]
n500 = util.gray2int(gc500)
# in 100-ft step must be converted first
gc100 = codestr[8:]
n100 = util.gray2int(gc100)
if n100 in [0, 5, 6]:
return None
if n100 == 7:
n100 = 5
if n500%2:
n100 = 6 - n100
alt = (n500*500 + n100*100) - 1300
return alt

View File

@@ -85,3 +85,13 @@ def floor(x):
eg.: floor(3.6) = 3, while floor(-3.6) = -4
"""
return int(math.floor(x))
def gray2int(graystr):
"""Convert greycode to binary (DF4, 20 altitude coding)"""
num = bin2int(graystr)
num ^= (num >> 8)
num ^= (num >> 4)
num ^= (num >> 2)
num ^= (num >> 1)
return num

View File

@@ -3,6 +3,13 @@
See:
https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject
Steps for deploying a new verison:
1. Increase the version number
2. remove the old deployment under [dist] folder
3. run: python setup.py sdist
run: python setup.py bdist_wheel --universal
4. twine upload dist/*
"""
# Always prefer setuptools over distutils
@@ -23,7 +30,7 @@ setup(
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version='1.0.7',
version='1.2.2',
description='Python Mode-S Decoder',
long_description=long_description,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +0,0 @@
# If you get import error run with ipython
from pyModeS import adsb
from pyModeS import ehs
from pyModeS import util
# === Decode sample data file ===
def adsb_decode_all(n=None):
print("===== Decode all ADS-B sample data=====")
import csv
f = open('tests/sample_data_adsb.csv', 'rt')
msg0 = None
msg1 = None
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[0]
m = r[1]
icao = adsb.icao(m)
tc = adsb.typecode(m)
if 1 <= tc <= 4:
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
if tc == 19:
print(ts, m, icao, tc, adsb.velocity(m))
if 5 <= tc <= 18:
if adsb.oe_flag(m):
msg1 = m
t1 = ts
else:
msg0 = m
t0 = ts
if msg0 and msg1:
pos = adsb.position(msg0, msg1, t0, t1)
alt = adsb.altitude(m)
print(ts, m, icao, tc, pos, alt)
def ehs_decode_all(n=None):
print("===== Decode all Mode-S EHS sample data=====")
import csv
f = open('tests/sample_data_ehs.csv', 'rt')
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[1]
m = r[2]
icao = ehs.icao(m)
vBDS = ehs.BDS(m)
if 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))
if vBDS == "BDS44":
print(ts, m, icao, vBDS, ehs.wind(m))
if vBDS == "BDS50":
print(ts, m, icao, vBDS, ehs.roll(m), ehs.track(m),
ehs.gs(m), ehs.rtrack(m), ehs.tas(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))
else:
print(ts, m, icao, 'UNKNOWN')
if __name__ == '__main__':
adsb_decode_all(100)
ehs_decode_all(100)

41
tests/sample_run_adsb.py Normal file
View File

@@ -0,0 +1,41 @@
from __future__ import print_function
from pyModeS import adsb, ehs, util
# === Decode sample data file ===
def adsb_decode_all(n=None):
print("===== Decode ADS-B sample data=====")
import csv
f = open('tests/data/sample_data_adsb.csv', 'rt')
msg0 = None
msg1 = None
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[0]
m = r[1]
icao = adsb.icao(m)
tc = adsb.typecode(m)
if 1 <= tc <= 4:
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
if tc == 19:
print(ts, m, icao, tc, adsb.velocity(m))
if 5 <= tc <= 18:
if adsb.oe_flag(m):
msg1 = m
t1 = ts
else:
msg0 = m
t0 = ts
if msg0 and msg1:
pos = adsb.position(msg0, msg1, t0, t1)
alt = adsb.altitude(m)
print(ts, m, icao, tc, pos, alt)
if __name__ == '__main__':
adsb_decode_all(n=100)

75
tests/sample_run_ehs.py Normal file
View File

@@ -0,0 +1,75 @@
from __future__ import print_function
from pyModeS import adsb, ehs, util
# === Decode sample data file ===
def bds_info(BDS, m):
if BDS == "BDS17":
info = ([i[-2:] for i in ehs.cap17(m)])
elif BDS == "BDS20":
info = ehs.callsign(m)
elif BDS == "BDS40":
info = (ehs.alt40mcp(m), ehs.alt40fms(m), ehs.p40baro(m))
elif BDS == "BDS44":
info = (ehs.wind44(m), ehs.temp44(m), ehs.p44(m), ehs.hum44(m))
elif BDS == "BDS44REV":
info = (ehs.wind44(m, rev=True), ehs.temp44(m, rev=True), ehs.p44(m, rev=True), ehs.hum44(m, rev=True))
elif BDS == "BDS50":
info = (ehs.roll50(m), ehs.trk50(m), ehs.gs50(m), ehs.rtrk50(m), ehs.tas50(m))
elif BDS == "BDS53":
info = (ehs.hdg53(m), ehs.ias53(m), ehs.mach53(m), ehs.tas53(m), ehs.vr53(m))
elif BDS == "BDS60":
info = (ehs.hdg60(m), ehs.ias60(m), ehs.mach60(m), ehs.vr60baro(m), ehs.vr60ins(m))
else:
info = None
return info
def ehs_decode_all(df, n=None):
import csv
print("===== Decode EHS sample data (DF=%s)=====" % df)
f = open('tests/data/sample_data_ehs_df%s.csv' % df, 'rt')
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[0]
m = r[2]
df = util.df(m)
icao = ehs.icao(m)
BDS = ehs.BDS(m)
code = ehs.df20alt(m) if df==20 else ehs.df21id(m)
if not BDS:
print(ts, m, icao, df, '%5s'%code, 'UNKNOWN')
continue
if isinstance(BDS, list):
print(ts, m, icao, df, '%5s'%code, end=' ')
for i, bds in enumerate(BDS):
if i == 0:
print(bds, *bds_info(bds, m))
else:
print(' '*55, bds, *bds_info(bds, m))
else:
print(ts, m, icao, df, '%5s'%code, BDS, *bds_info(BDS, m))
if __name__ == '__main__':
ehs_decode_all(df=20, n=100)
ehs_decode_all(df=21, n=100)

View File

@@ -1,6 +1,5 @@
from pyModeS import adsb
# === TEST ADS-B package ===
def test_adsb_icao():
@@ -22,6 +21,13 @@ def test_adsb_position():
assert pos == (49.81755, 6.08442)
def test_adsb_position_with_ref():
pos = adsb.position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0)
assert pos == (49.82410, 6.06785)
pos = adsb.position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5)
assert pos == (-43.48564, 172.53942)
def test_adsb_airborne_position_with_ref():
pos = adsb.airborne_position_with_ref("8D40058B58C901375147EFD09357",
49.0, 6.0)
@@ -32,9 +38,9 @@ def test_adsb_airborne_position_with_ref():
def test_adsb_surface_position_with_ref():
pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B22B4A8",
pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B000000",
-43.5, 172.5)
assert pos == (-43.48564, 175.87195)
assert pos == (-43.48564, 172.53942)
def test_adsb_surface_position():
@@ -51,8 +57,11 @@ def test_adsb_alt():
def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, 182.9, -832, 'GS')
assert vas == (376, 244.0, -2304, 'AS')
assert vgs_surface == (19.0, 42.2, 0 , 'GS')
assert adsb.altitude_diff('8D485020994409940838175B284F') == 550
def test_nic():

View File

@@ -1,5 +1,5 @@
from pyModeS import ehs
from pyModeS import modes_common
def test_ehs_icao():
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
@@ -7,14 +7,16 @@ def test_ehs_icao():
assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
def test_df20alt():
assert ehs.df20alt("A02014B400000000000000F9D514") == 32300
def test_ehs_BDS():
assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
# assert ehs.BDS("A000031DBAA9DD18622C441330E9") == 'BDS44'
assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
assert ehs.BDS("A000029CFFBAA11E2004727281F1") == 'BDS60'
assert ehs.BDS("A0281838CAE9E12FA03FFF2DDDE5") == 'BDS44'
assert ehs.BDS("A00017B0C8480030A4000024512F") is None
assert ehs.BDS("A00004128F39F91A7E27C46ADC21") == 'BDS60'
def test_ehs_BDS20_callsign():
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
@@ -22,22 +24,39 @@ 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
# signed values
assert ehs.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4
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("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_common.gray2alt('00000000010') == -1000
assert modes_common.gray2alt('00000001010') == -500
assert modes_common.gray2alt('00000011011') == -100
assert modes_common.gray2alt('00000011010') == 0
assert modes_common.gray2alt('00000011110') == 100
assert modes_common.gray2alt('00000010011') == 600
assert modes_common.gray2alt('00000110010') == 1000
assert modes_common.gray2alt('00001001001') == 5800
assert modes_common.gray2alt('00011100100') == 10300
assert modes_common.gray2alt('01100011010') == 32000
assert modes_common.gray2alt('01110000100') == 46300
assert modes_common.gray2alt('01010101100') == 50200
assert modes_common.gray2alt('11011110100') == 73200
assert modes_common.gray2alt('10000000011') == 126600
assert modes_common.gray2alt('10000000001') == 126700

View File

@@ -12,4 +12,4 @@ def test_crc_decode():
def test_crc_encode():
parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert util.hex2bin("AA4BDA") == parity
assert util.hex2bin("AA4BDA") == parity

View File

@@ -1,6 +1,6 @@
[tox]
toxworkdir=/tmp/tox
envlist = py26,py27,py35
envlist = py2,py3
[testenv]
deps=pytest
commands=py.test