38 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
18 changed files with 10714 additions and 2327 deletions

View File

@@ -1,16 +1,21 @@
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 Elementary Surveillance (ELS) (DF4 and DF5).
- DF4: Altitude
- DF5: Squawk code
- 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
@@ -19,7 +24,7 @@ implemented to decode the following messages:
- BDS 5,3 Air-referenced state vector
- BDS 6,0 Heading and speed report
A detailed manual on Mode-S decoding is published by the author, at:
Detailed manual on Mode-S decoding is published by the author, at:
http://adsb-decode-guide.readthedocs.io
@@ -31,115 +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.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)
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)
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)
**Hint: When you have a fix position of the aircraft, it is convenient to
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.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)
Core functions for EHS decoding:
********************************
# 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 (%)
.. code:: python
# 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)
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
# 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)
pms.ehs.BDS(msg) # Comm-B Data Selector Version
# 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)
# 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

@@ -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

@@ -266,10 +266,9 @@ 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
@@ -469,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:
@@ -492,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)
@@ -541,7 +545,7 @@ def velocity(msg):
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)
"""
@@ -553,70 +557,76 @@ def velocity(msg):
return airborne_velocity(msg)
else:
raise RuntimeError("incorrect or inconsistant message types")
raise RuntimeError("incorrect or inconsistant message types, expecting 4<TC<9 or TC=19")
def speed_heading(msg):
"""Get speed and heading only from the velocity message
"""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), heading (degree)
(int, float): speed (kt), ground track or heading (degree)
"""
spd, hdg, rocd, tag = velocity(msg)
return spd, hdg
spd, trk_or_hdg, rocd, tag = velocity(msg)
return spd, trk_or_hdg
def airborne_velocity(msg):
"""Calculate the speed, heading, and vertical rate
"""Calculate the speed, track (or heading), and vertical rate
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 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 surface_velocity(msg):
@@ -625,23 +635,23 @@ def surface_velocity(msg):
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): 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)
"""
if typecode(msg) < 5 or typecode(msg) > 8:
raise RuntimeError("%s: Not a surface message" % msg)
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
msgbin = util.hex2bin(msg)
# heading
hdg_status = int(msgbin[44])
if hdg_status == 1:
hdg = util.bin2int(msgbin[45:52]) * 360.0 / 128.0
hdg = round(hdg, 1)
# 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:
hdg = None
trk = None
# ground movment / speed
mov = util.bin2int(msgbin[37:44])
@@ -660,4 +670,27 @@ def surface_velocity(msg):
spd = kts[i-1] + (mov-movs[i-1]) * step
spd = round(spd, 2)
return spd, hdg, 0, 'GS'
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

@@ -18,65 +18,15 @@ A python package for decoding ModeS (DF20, DF21) messages.
"""
from __future__ import absolute_import, print_function, division
from . import util
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 . 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
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
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(util.crc(msg, encode=True))
c1 = util.hex2int(msg[-6:])
icao = '%06X' % (c0 ^ c1)
return icao
def checkbits(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 False
return True
# ------------------------------------------
# Common functions
# ------------------------------------------
def isnull(msg):
"""check if the data bits are all zeros
@@ -93,9 +43,27 @@ def isnull(msg):
else:
return True
def checkbits(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 False
return True
# ------------------------------------------
# Common functions
# ------------------------------------------
def df20alt(msg):
"""Computes the altitude from DF20 bit 20-32
"""Computes the altitude from DF20 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
@@ -104,31 +72,83 @@ def df20alt(msg):
int: altitude in ft
"""
if df(msg) != 20:
if util.df(msg) != 20:
raise RuntimeError("Message must be Downlink Format 20.")
# Altitude code, bit 20-32
mbin = util.hex2bin(msg)
mbit = mbin[25] # M bit: 26
qbit = mbin[27] # Q bit: 28
return modes_common.altcode(msg)
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
if qbit == '0': # 100ft interval
# to be implemented
alt = None
if mbit == '1': # unit in meter
vbin = mbin[19:25] + mbin[26:32]
alt = int(util.bin2int(vbin) * 3.28084) # convert to ft
def df21id(msg):
"""Computes identity (squawk code) from DF21, bit 20-32
return alt
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):
@@ -187,7 +207,8 @@ def callsign(msg):
# ------------------------------------------
# DF 20/21, BDS 4,0
# BDS 4,0
# Selected vertical intention
# ------------------------------------------
def isBDS40(msg):
@@ -276,7 +297,8 @@ def p40baro(msg):
# ------------------------------------------
# DF 20/21, BDS 4,4
# BDS 4,4
# Meteorological routine air report
# ------------------------------------------
def isBDS44(msg, rev=False):
@@ -303,12 +325,17 @@ def isBDS44(msg, rev=False):
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
if not result:
return False
@@ -317,9 +344,11 @@ def isBDS44(msg, rev=False):
if vw is not None and vw[0] > 250:
result &= False
# if temp44(msg):
# if temp44(msg) > 60 or temp44(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
@@ -370,18 +399,28 @@ def temp44(msg, rev=False):
d = util.hex2bin(data(msg))
if not rev:
if d[22] == '0':
return None
# if d[22] == '0':
# return None
sign = int(d[23])
temp = util.bin2int(d[24:34]) * 0.125 # celsius
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
# if d[23] == '0':
# return None
sign = int(d[24])
temp = util.bin2int(d[25:35]) * 0.125 # celsius
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
@@ -442,7 +481,7 @@ def hum44(msg, rev=False):
# ------------------------------------------
# DF 20/21, BDS 5,0
# BDS 5,0
# Track and turn report
# ------------------------------------------
@@ -476,7 +515,7 @@ def isBDS50(msg):
result &= True
else:
roll = abs(roll50(msg))
if roll and roll > 30:
if roll and roll > 60:
result &= False
gs = gs50(msg)
@@ -509,8 +548,12 @@ def roll50(msg):
return None
sign = int(d[1]) # 1 -> left wing down
value = util.bin2int(d[2:11]) * 45.0 / 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)
@@ -529,9 +572,18 @@ def trk50(msg):
return None
sign = int(d[12]) # 1 -> west
value = util.bin2int(d[13:23]) * 90.0 / 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 gs50(msg):
@@ -569,9 +621,12 @@ def rtrk50(msg):
if d[36:45] == "111111111":
return None
sign = int(d[35]) # 1 -> minus
value = util.bin2int(d[36:45]) * 8.0 / 256.0 # degree / sec
angle = -1 * value if sign else value
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)
@@ -594,7 +649,7 @@ def tas50(msg):
# ------------------------------------------
# DF 20/21, BDS 5,3
# BDS 5,3
# Air-referenced state vector
# ------------------------------------------
@@ -657,10 +712,19 @@ def hdg53(msg):
if d[0] == '0':
return None
sign = int(d[1]) # 1 -> west
value = util.bin2int(d[2:12]) * 90.0 / 512.0 # degree
angle = 360 - value if sign else value
return round(angle, 1)
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):
@@ -730,14 +794,20 @@ def vr53(msg):
if d[46] == '0':
return None
sign = d[47] # 1 -> minus
value = util.bin2int(d[48:56]) * 64 # feet/min
roc = -1*value if sign else value
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
# ------------------------------------------
# DF 20/21, BDS 6,0
# BDS 6,0
# ------------------------------------------
def isBDS60(msg):
@@ -773,13 +843,8 @@ def isBDS60(msg):
if mach is not None and mach > 1:
result &= False
vrb = vr60baro(msg)
if vrb is not None and abs(vrb) > 5000:
result &= False
vri = vr60ins(msg)
if vri is not None and abs(vri) > 5000:
result &= False
# leave out the check from vertical rates,
# due to very noisy measurement
return result
@@ -799,8 +864,17 @@ def hdg60(msg):
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)
@@ -841,7 +915,7 @@ def mach60(msg):
def vr60baro(msg):
"""Vertical rate from barometric measurement
"""Vertical rate from barometric measurement, this value may be very noisy.
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
@@ -854,9 +928,15 @@ def vr60baro(msg):
if d[34] == '0':
return None
sign = d[35] # 1 -> minus
value = util.bin2int(d[36:45]) * 32 # feet/min
roc = -1*value if sign else value
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
@@ -874,9 +954,15 @@ def vr60ins(msg):
if d[45] == '0':
return None
sign = d[46] # 1 -> minus
value = util.bin2int(d[47:56]) * 32 # feet/min
roc = -1*value if sign else value
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
@@ -893,6 +979,7 @@ def BDS(msg):
if isnull(msg):
return None
is17 = isBDS17(msg)
is20 = isBDS20(msg)
is40 = isBDS40(msg)
is44 = isBDS44(msg)
@@ -901,12 +988,12 @@ def BDS(msg):
is53 = isBDS53(msg)
is60 = isBDS60(msg)
BDS = ["BDS20", "BDS40", "BDS44", "BDS44REV", "BDS50", "BDS53", "BDS60"]
isBDS = [is20, is40, is44, is44rev, is50, is53, is60]
BDS = ["BDS17", "BDS20", "BDS40", "BDS44", "BDS44REV", "BDS50", "BDS53", "BDS60"]
isBDS = [is17, is20, is40, is44, is44rev, is50, is53, is60]
if sum(isBDS) == 0:
return None
if sum(isBDS) == 1:
elif sum(isBDS) == 1:
return BDS[isBDS.index(True)]
else:
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

@@ -30,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.1.1',
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,83 +0,0 @@
from pyModeS import adsb, ehs, 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 isinstance(vBDS, list):
print(ts, m, icao, vBDS)
if vBDS == "BDS20":
print(ts, m, icao, vBDS, ehs.callsign(m))
if vBDS == "BDS40":
print(ts, m, icao, vBDS, ehs.alt40mcp(m),
ehs.alt40fms(m), ehs.p40baro(m))
if vBDS == "BDS44":
print(ts, m, icao, vBDS, ehs.wind44(m),
ehs.temp44(m), ehs.p44(m))
if vBDS == "BDS50":
print(ts, m, icao, vBDS, ehs.roll50(m), ehs.trk50(m),
ehs.gs50(m), ehs.rtrk50(m), ehs.tas50(m))
if vBDS == "BDS53":
print(ts, m, icao, vBDS, ehs.hdg53(m), ehs.ias53(m),
ehs.mach53(m), ehs.tas53(m), ehs.vr53(m))
if vBDS == "BDS60":
print(ts, m, icao, vBDS, ehs.hdg60(m), ehs.ias60(m),
ehs.mach60(m), ehs.vr60baro(m), ehs.vr60ins(m))
else:
print(ts, m, icao, 'UNKNOWN')
if __name__ == '__main__':
# adsb_decode_all(100)
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

@@ -61,6 +61,7 @@ def test_adsb_velocity():
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'
@@ -8,15 +8,15 @@ def test_ehs_icao():
def test_df20alt():
assert ehs.df20alt("A02014B400000000000000F9D514") == 33300
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("A00004128F39F91A7E27C46ADC21") == 'BDS60'
def test_ehs_BDS20_callsign():
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
@@ -28,18 +28,35 @@ def test_ehs_BDS40_functions():
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("A000029CFFBAA11E2004727281F1") == 180.9
assert ehs.ias60("A000029CFFBAA11E2004727281F1") == 336
assert ehs.mach60("A000029CFFBAA11E2004727281F1") == 0.48
assert ehs.vr60baro("A000029CFFBAA11E2004727281F1") == 0
assert ehs.vr60ins("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

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