Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72fe72c16c | ||
|
|
31df5c2042 | ||
|
|
10e9b234ad | ||
|
|
546d69c129 | ||
|
|
c4c285266c | ||
|
|
ebc11e5e84 | ||
|
|
f5c2b36209 | ||
|
|
759380b5a9 | ||
|
|
3f6389a67d | ||
|
|
c252647e77 | ||
|
|
1795d2f3c0 | ||
|
|
fb4cef7085 | ||
|
|
b24c1101b6 | ||
|
|
8a43d711aa | ||
|
|
495f320988 | ||
|
|
44a9c8d2aa | ||
|
|
ecfc4037a1 | ||
|
|
61f2191feb | ||
|
|
9e1cc2c5a2 | ||
|
|
500c3ca9bd | ||
|
|
78f1ca77c7 | ||
|
|
576713f13b | ||
|
|
3502fedf02 | ||
|
|
de9ec43912 | ||
|
|
16c83d1505 | ||
|
|
e5d5633535 | ||
|
|
8adacd8e91 | ||
|
|
fd15b13c17 | ||
|
|
405d8ed108 | ||
|
|
0085d03d4a | ||
|
|
6f139d4ae9 | ||
|
|
e5ca76ac0d | ||
|
|
ef9d2cfd16 | ||
|
|
ea1ccc0c70 | ||
|
|
648f4660b7 | ||
|
|
0df6a664a3 | ||
|
|
44b277f0ad | ||
|
|
715d0a3c66 | ||
|
|
6db5ea8023 | ||
|
|
7685f1590f | ||
|
|
5fa090b95f | ||
|
|
6c5ae2141b | ||
|
|
24f3658673 | ||
|
|
205725872a | ||
|
|
01a573a1af | ||
|
|
c0476f5e16 | ||
|
|
140b68afbc | ||
|
|
70b3af2c8b | ||
|
|
0ef64be934 | ||
|
|
bbe6e50fb2 | ||
|
|
a3e44b5626 | ||
|
|
457a948879 | ||
|
|
54b2038a41 | ||
|
|
82b912bc05 | ||
|
|
6d5869a9e0 | ||
|
|
36840e0225 | ||
|
|
d4ca81e0ca | ||
|
|
9fa475ab9a | ||
|
|
c91bd4bb03 | ||
|
|
4f0946c4da | ||
|
|
46a99852d6 | ||
|
|
2389c12b98 | ||
|
|
6128c5a18d | ||
|
|
72c1a9f645 | ||
|
|
f221c67295 | ||
|
|
14a537030d | ||
|
|
b7afd841ff | ||
|
|
11b85b6959 | ||
|
|
8a54f927f6 | ||
|
|
9bfc116516 | ||
|
|
be38acabfd | ||
|
|
0b6d5576d3 | ||
|
|
dfdddb77f2 | ||
|
|
a872cd253e | ||
|
|
edbfdc68de | ||
|
|
f246c88dd6 | ||
|
|
e821b8fa77 | ||
|
|
1b3dcef659 | ||
|
|
7653b2459d | ||
|
|
362b92de7a | ||
|
|
ef8f4b3b7f | ||
|
|
773efff9bc | ||
|
|
05e34e7516 | ||
|
|
161ea31ee1 | ||
|
|
12506e7c7f | ||
|
|
fcd98d6bcc | ||
|
|
ef40acdbfd | ||
|
|
8b48fabf5a | ||
|
|
d91ad261cd | ||
|
|
4911e69171 | ||
|
|
5697e5b88e | ||
|
|
3073187d24 | ||
|
|
441cd27761 | ||
|
|
c4406ba276 | ||
|
|
de6238f5e9 | ||
|
|
711fd889e6 | ||
|
|
d6a04865dc | ||
|
|
6baa218596 | ||
|
|
8a9045e730 | ||
|
|
d13e1bd12e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
244
README.rst
244
README.rst
@@ -1,53 +1,107 @@
|
||||
The Python Mode-S Decoder
|
||||
=========================
|
||||
The Python ADS-B/Mode-S Decoder
|
||||
==========================================
|
||||
|
||||
Python library for Mode-S message decoding. Support Downlink Formats (DF) are:
|
||||
Python library for ADS-B/Mode-S message decoding. Supported Downlink Formats (DF) are:
|
||||
|
||||
- Automatic Dependent Surveillance - Broadcast (ADS-B) (DF17)
|
||||
**DF17 / DF18: Automatic Dependent Surveillance - Broadcast (ADS-B)**
|
||||
|
||||
- aircraft information that contains: ICAO address, position, altitude, velocity (ground speed), callsign, etc.
|
||||
- TC=1-4 / BDS 0,8: Aircraft identification and category
|
||||
- TC=5-8 / BDS 0,6: Surface position
|
||||
- TC=9-18 / BDS 0,5: Airborne position
|
||||
- TC=19 / BDS 0,9: Airborne velocity
|
||||
- TC=28 / BDS 6,1: Airborne status [to be implemented]
|
||||
- TC=29 / BDS 6,2: Target state and status information [to be implemented]
|
||||
- TC=31 / BDS 6,5: Aircraft operational status [to be implemented]
|
||||
|
||||
- Mode-S Comm-B replies :Additional information in response to SSR interrogation, such as true airspeed, indicated airspeed, mach number, wind, temperature, etc.
|
||||
|
||||
- DF20: Altitude
|
||||
- DF21: Squawk code
|
||||
- DF20/21 BDS 2,0 Aircraft identification
|
||||
- DF20/21 BDS 2,1 Aircraft and airline registration markings
|
||||
- DF20/21 BDS 4,0 Selected vertical intention
|
||||
- DF20/21 BDS 4,4 Meteorological routine air report
|
||||
- DF20/21 BDS 5,0 Track and turn report
|
||||
- DF20/21 BDS 5,3 Air-referenced state vector
|
||||
- DF20/21 BDS 6,0 Heading and speed report
|
||||
**DF20 / DF21: Mode-S Comm-B replies**
|
||||
|
||||
- BDS 1,0: Data link capability report
|
||||
- BDS 1,7: Common usage GICB capability report
|
||||
- BDS 2,0: Aircraft identification
|
||||
- BDS 2,1: Aircraft and airline registration markings
|
||||
- BDS 3,0: ACAS active resolution advisory
|
||||
- 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
|
||||
|
||||
|
||||
**DF4 / DF20: Altitude code**
|
||||
|
||||
**DF5 / DF21: Identity code (squawk code)**
|
||||
|
||||
Detailed manual on Mode-S decoding is published by the author, at:
|
||||
http://mode-s.org/decode
|
||||
https://mode-s.org/decode
|
||||
|
||||
|
||||
New features in v2.0
|
||||
---------------------
|
||||
- New structure of the libraries
|
||||
- ADS-B and Comm-B data streaming
|
||||
- Active aircraft viewing (terminal curses)
|
||||
- Improved BDS identification
|
||||
- Optimizing decoding speed
|
||||
|
||||
|
||||
Source code
|
||||
-----------
|
||||
Checkout and contribute to this open source project at:
|
||||
Checkout and contribute to this open-source project at:
|
||||
https://github.com/junzis/pyModeS
|
||||
|
||||
API documentation at:
|
||||
http://pymodes.readthedocs.io
|
||||
[To be updated]
|
||||
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
The easiest installation is to use pip:
|
||||
|
||||
::
|
||||
|
||||
pip install pyModeS
|
||||
|
||||
To install latest devlopment version from the GitHub:
|
||||
To install latest version from the GitHub:
|
||||
|
||||
::
|
||||
|
||||
pip install git+https://github.com/junzis/pyModeS
|
||||
|
||||
|
||||
To install the stable version (2.0) from pip:
|
||||
|
||||
::
|
||||
|
||||
pip install pyModeS
|
||||
|
||||
|
||||
|
||||
Live view traffic (modeslive)
|
||||
----------------------------------------------------
|
||||
Supports **Mode-S Beast** and **AVR** raw stream
|
||||
|
||||
::
|
||||
|
||||
modeslive --server [server_address] --port [tcp_port] --rawtype [beast,avr,skysense] --latlon [lat] [lon] --dumpto [folder]
|
||||
|
||||
Arguments:
|
||||
-h, --help show this help message and exit
|
||||
--server SERVER server address or IP
|
||||
--port PORT raw data port
|
||||
--rawtype RAWTYPE beast, avr or skysense
|
||||
--latlon LAT LON receiver position
|
||||
--show-uncertainty display uncertaint values, default off
|
||||
--dumpto folder to dump decoded output
|
||||
|
||||
|
||||
If you have a RTL-SDR receiver or Mode-S Beast, use modesmixer2 (http://xdeco.org/?page_id=48) to create raw beast TCP stream:
|
||||
|
||||
::
|
||||
|
||||
$ modesmixer2 --inSeriel port[:speed[:flow_control]] --outServer beast:[tcp_port]
|
||||
|
||||
Example screenshot:
|
||||
|
||||
.. image:: https://github.com/junzis/pyModeS/raw/master/doc/modeslive-screenshot.png
|
||||
:width: 700px
|
||||
|
||||
Use the library
|
||||
---------------
|
||||
|
||||
@@ -56,33 +110,33 @@ Use the library
|
||||
import pyModeS as pms
|
||||
|
||||
|
||||
Common functions:
|
||||
Common functions
|
||||
*****************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.df(msg) # Downlink Format
|
||||
pms.icao(msg) # Infer the ICAO address from the message
|
||||
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.gray2int(str) # Convert grey code to interger
|
||||
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:
|
||||
**********************************
|
||||
Core functions for ADS-B decoding
|
||||
*********************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.adsb.icao(msg)
|
||||
pms.adsb.typecode(msg)
|
||||
|
||||
# typecode 1-4
|
||||
# Typecode 1-4
|
||||
pms.adsb.callsign(msg)
|
||||
|
||||
# typecode 5-8 (surface) and 9-18 (airborne)
|
||||
# Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 9-18 (airborne, GNSS height)
|
||||
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)
|
||||
@@ -93,9 +147,9 @@ Core functions for ADS-B decoding:
|
||||
|
||||
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
|
||||
# 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)
|
||||
|
||||
@@ -106,67 +160,91 @@ use `position_with_ref()` method to decode with only one position message
|
||||
messages. But the reference position shall be with in 180NM (airborne)
|
||||
or 45NM (surface) of the true position.
|
||||
|
||||
Core functions for ELS decoding:
|
||||
********************************
|
||||
|
||||
Decode altitude replies in DF4 / DF20
|
||||
**************************************
|
||||
.. code:: python
|
||||
|
||||
pms.common.altcode(msg) # Downlink format must be 4 or 20
|
||||
|
||||
|
||||
Decode identity replies in DF5 / DF21
|
||||
**************************************
|
||||
.. code:: python
|
||||
|
||||
pms.common.idcode(msg) # Downlink format must be 5 or 21
|
||||
|
||||
|
||||
|
||||
Common Mode-S functions
|
||||
************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
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
|
||||
pms.icao(msg) # Infer the ICAO address from the message
|
||||
pms.bds.infer(msg) # Infer the Modes-S BDS register
|
||||
|
||||
# Check if BDS is 5,0 or 6,0, give reference speed, track, altitude (from ADS-B)
|
||||
pms.bds.is50or60(msg, spd_ref, trk_ref, alt_ref)
|
||||
|
||||
# Check each BDS explicitly
|
||||
pms.bds.bds10.is10(msg)
|
||||
pms.bds.bds17.is17(msg)
|
||||
pms.bds.bds20.is20(msg)
|
||||
pms.bds.bds30.is30(msg)
|
||||
pms.bds.bds40.is40(msg)
|
||||
pms.bds.bds44.is44(msg)
|
||||
pms.bds.bds50.is50(msg)
|
||||
pms.bds.bds60.is60(msg)
|
||||
|
||||
|
||||
Core functions for EHS decoding:
|
||||
********************************
|
||||
|
||||
Mode-S Elementary Surveillance (ELS)
|
||||
*************************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
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.commb.ovc10(msg) # Overlay capability, BDS 1,0
|
||||
pms.commb.cap17(msg) # GICB capability, BDS 1,7
|
||||
pms.commb.cs20(msg) # Callsign, BDS 2,0
|
||||
|
||||
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
|
||||
Mode-S Enhanced Surveillance (EHS)
|
||||
***********************************
|
||||
|
||||
# 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)
|
||||
.. code:: python
|
||||
|
||||
# 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 register 4,0
|
||||
pms.commb.alt40mcp(msg) # MCP/FCU selected altitude (ft)
|
||||
pms.commb.alt40fms(msg) # FMS selected altitude (ft)
|
||||
pms.commb.p40baro(msg) # Barometric pressure (mb)
|
||||
|
||||
# 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 register 5,0
|
||||
pms.commb.roll50(msg) # Roll angle (deg)
|
||||
pms.commb.trk50(msg) # True track angle (deg)
|
||||
pms.commb.gs50(msg) # Ground speed (kt)
|
||||
pms.commb.rtrk50(msg) # Track angle rate (deg/sec)
|
||||
pms.commb.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 register 6,0
|
||||
pms.commb.hdg60(msg) # Magnetic heading (deg)
|
||||
pms.commb.ias60(msg) # Indicated airspeed (kt)
|
||||
pms.commb.mach60(msg) # Mach number (-)
|
||||
pms.commb.vr60baro(msg) # Barometric altitude rate (ft/min)
|
||||
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
|
||||
|
||||
|
||||
Meteorological routine air report (MRAR) [Experimental]
|
||||
*******************************************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
# For BDS register 4,4
|
||||
pms.commb.wind44(msg, rev=False) # Wind speed (kt) and direction (true) (deg)
|
||||
pms.commb.temp44(msg, rev=False) # Static air temperature (C)
|
||||
pms.commb.p44(msg, rev=False) # Average static pressure (hPa)
|
||||
pms.commb.hum44(msg, rev=False) # Humidity (%)
|
||||
|
||||
# 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
|
||||
------------
|
||||
|
||||
BIN
doc/modeslive-screenshot.png
Normal file
BIN
doc/modeslive-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -1,6 +1,15 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
from .util import *
|
||||
from . import adsb
|
||||
from . import ehs
|
||||
from . import els
|
||||
from .decoder.common import *
|
||||
from .decoder import adsb
|
||||
from .decoder import commb
|
||||
from .decoder import common
|
||||
from .decoder import bds
|
||||
from .extra import aero
|
||||
from .extra import tcpclient
|
||||
|
||||
# from .decoder import els # depricated
|
||||
# from .decoder import ehs # depricated
|
||||
|
||||
import os
|
||||
dirpath = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
696
pyModeS/adsb.py
696
pyModeS/adsb.py
@@ -1,696 +0,0 @@
|
||||
# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
A python package for decoding ABS-D messages.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import math
|
||||
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)
|
||||
|
||||
|
||||
def icao(msg):
|
||||
"""Get the ICAO 24 bits address, bytes 3 to 8.
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
String: ICAO address in 6 bytes hexadecimal string
|
||||
"""
|
||||
return msg[2:8]
|
||||
|
||||
|
||||
def data(msg):
|
||||
"""Return the data frame in the message, bytes 9 to 22"""
|
||||
return msg[8:22]
|
||||
|
||||
|
||||
def typecode(msg):
|
||||
"""Type code of ADS-B message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: type code number
|
||||
"""
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[32:37])
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Aircraft Identification
|
||||
# ---------------------------------------------
|
||||
def category(msg):
|
||||
"""Aircraft category number
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: category number
|
||||
"""
|
||||
|
||||
if typecode(msg) < 1 or typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[5:8])
|
||||
|
||||
|
||||
def callsign(msg):
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: callsign
|
||||
"""
|
||||
|
||||
if typecode(msg) < 1 or typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
|
||||
msgbin = util.hex2bin(msg)
|
||||
csbin = msgbin[40:96]
|
||||
|
||||
cs = ''
|
||||
cs += chars[util.bin2int(csbin[0:6])]
|
||||
cs += chars[util.bin2int(csbin[6:12])]
|
||||
cs += chars[util.bin2int(csbin[12:18])]
|
||||
cs += chars[util.bin2int(csbin[18:24])]
|
||||
cs += chars[util.bin2int(csbin[24:30])]
|
||||
cs += chars[util.bin2int(csbin[30:36])]
|
||||
cs += chars[util.bin2int(csbin[36:42])]
|
||||
cs += chars[util.bin2int(csbin[42:48])]
|
||||
|
||||
# clean string, remove spaces and marks, if any.
|
||||
# cs = cs.replace('_', '')
|
||||
cs = cs.replace('#', '')
|
||||
return cs
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Positions
|
||||
# ---------------------------------------------
|
||||
|
||||
def oe_flag(msg):
|
||||
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: 0 or 1, for even or odd frame
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
return int(msgbin[53])
|
||||
|
||||
|
||||
def cprlat(msg):
|
||||
"""CPR encoded latitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: encoded latitude
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[54:71])
|
||||
|
||||
|
||||
def cprlon(msg):
|
||||
"""CPR encoded longitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: encoded longitude
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[71:88])
|
||||
|
||||
|
||||
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
||||
"""Decode position from a pair of even and odd position message
|
||||
(works with both airborne and surface position messages)
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
if (5 <= typecode(msg0) <= 8 and 5 <= typecode(msg1) <= 8):
|
||||
if (not lat_ref) or (not lon_ref):
|
||||
raise RuntimeError("Surface position encountered, a reference \
|
||||
position lat/lon required. Location of \
|
||||
receiver can be used.")
|
||||
else:
|
||||
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
|
||||
|
||||
elif (9 <= typecode(msg0) <= 18 and 9 <= typecode(msg1) <= 18):
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
|
||||
|
||||
def airborne_position(msg0, msg1, t0, t1):
|
||||
"""Decode airborn position from a pair of even and odd position message
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
msgbin0 = util.hex2bin(msg0)
|
||||
msgbin1 = util.hex2bin(msg1)
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0
|
||||
cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0
|
||||
cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0
|
||||
cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0
|
||||
|
||||
air_d_lat_even = 360.0 / 60
|
||||
air_d_lat_odd = 360.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = util.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
|
||||
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
if lat_even >= 270:
|
||||
lat_even = lat_even - 360
|
||||
|
||||
if lat_odd >= 270:
|
||||
lat_odd = lat_odd - 360
|
||||
|
||||
# check if both are in the same latidude zone, exit if not
|
||||
if _cprNL(lat_even) != _cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
lat = lat_even
|
||||
nl = _cprNL(lat)
|
||||
ni = max(_cprNL(lat)- 0, 1)
|
||||
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = _cprNL(lat)
|
||||
ni = max(_cprNL(lat) - 1, 1)
|
||||
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
if lon > 180:
|
||||
lon = lon - 360
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode position with only one message,
|
||||
knowing reference nearby location, such as previously
|
||||
calculated location, ground station, or airport location, etc.
|
||||
Works with both airborne and surface position messages.
|
||||
The reference position shall be with in 180NM (airborne) or 45NM (surface)
|
||||
of the true position.
|
||||
|
||||
Args:
|
||||
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 surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
elif 9 <= typecode(msg) <= 18:
|
||||
return airborne_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
|
||||
|
||||
def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode airborne position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 180NM of the true position.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
|
||||
i = oe_flag(msg)
|
||||
d_lat = 360.0/59 if i else 360.0/60
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
cprlat = util.bin2int(msgbin[54:71]) / 131072.0
|
||||
cprlon = util.bin2int(msgbin[71:88]) / 131072.0
|
||||
|
||||
j = util.floor(lat_ref / d_lat) \
|
||||
+ util.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = _cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 360.0 / ni
|
||||
else:
|
||||
d_lon = 360.0
|
||||
|
||||
m = util.floor(lon_ref / d_lon) \
|
||||
+ util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
"""Decode surface position from a pair of even and odd position message,
|
||||
the lat/lon of receiver must be provided to yield the correct solution.
|
||||
|
||||
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
|
||||
lat_ref (float): latitude of the receiver
|
||||
lon_ref (float): longitude of the receiver
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
msgbin0 = util.hex2bin(msg0)
|
||||
msgbin1 = util.hex2bin(msg1)
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0
|
||||
cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0
|
||||
cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0
|
||||
cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0
|
||||
|
||||
air_d_lat_even = 90.0 / 60
|
||||
air_d_lat_odd = 90.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = util.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_s = lat_even_n - 90.0
|
||||
lat_odd_s = lat_odd_n - 90.0
|
||||
|
||||
# chose which solution corrispondes to receiver location
|
||||
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
|
||||
lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s
|
||||
|
||||
# check if both are in the same latidude zone, rare but possible
|
||||
if _cprNL(lat_even) != _cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
lat = lat_even
|
||||
nl = _cprNL(lat_even)
|
||||
ni = max(_cprNL(lat_even) - 0, 1)
|
||||
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = _cprNL(lat_odd)
|
||||
ni = max(_cprNL(lat_odd) - 1, 1)
|
||||
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
# four possible longitude solutions
|
||||
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
|
||||
|
||||
# the closest solution to receiver is the correct one
|
||||
dls = [abs(lon_ref - l) for l in lons]
|
||||
imin = min(range(4), key=dls.__getitem__)
|
||||
lon = lons[imin]
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode surface position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 45NM of the true position.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
|
||||
i = oe_flag(msg)
|
||||
d_lat = 90.0/59 if i else 90.0/60
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
cprlat = util.bin2int(msgbin[54:71]) / 131072.0
|
||||
cprlon = util.bin2int(msgbin[71:88]) / 131072.0
|
||||
|
||||
j = util.floor(lat_ref / d_lat) \
|
||||
+ util.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = _cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 90.0 / ni
|
||||
else:
|
||||
d_lon = 90.0
|
||||
|
||||
m = util.floor(lon_ref / d_lon) \
|
||||
+ util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def _cprNL(lat):
|
||||
"""NL() function in CPR decoding
|
||||
"""
|
||||
if lat == 0:
|
||||
return 59
|
||||
|
||||
if lat == 87 or lat == -87:
|
||||
return 2
|
||||
|
||||
if lat > 87 or lat < -87:
|
||||
return 1
|
||||
|
||||
nz = 15
|
||||
a = 1 - math.cos(math.pi / (2 * nz))
|
||||
b = math.cos(math.pi / 180.0 * abs(lat)) ** 2
|
||||
nl = 2 * math.pi / (math.acos(1 - a/b))
|
||||
NL = util.floor(nl)
|
||||
return NL
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
|
||||
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:
|
||||
n = util.bin2int(msgbin[40:47]+msgbin[48:52])
|
||||
alt = n * 25 - 1000
|
||||
return alt
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def nic(msg):
|
||||
"""Calculate NIC, navigation integrity category
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
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, expecting 8<TC<19" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
tc = typecode(msg)
|
||||
nic_sup_b = util.bin2int(msgbin[39])
|
||||
|
||||
if tc in [0, 18, 22]:
|
||||
nic = 0
|
||||
elif tc == 17:
|
||||
nic = 1
|
||||
elif tc == 16:
|
||||
if nic_sup_b:
|
||||
nic = 3
|
||||
else:
|
||||
nic = 2
|
||||
elif tc == 15:
|
||||
nic = 4
|
||||
elif tc == 14:
|
||||
nic = 5
|
||||
elif tc == 13:
|
||||
nic = 6
|
||||
elif tc == 12:
|
||||
nic = 7
|
||||
elif tc == 11:
|
||||
if nic_sup_b:
|
||||
nic = 9
|
||||
else:
|
||||
nic = 8
|
||||
elif tc in [10, 21]:
|
||||
nic = 10
|
||||
elif tc in [9, 20]:
|
||||
nic = 11
|
||||
else:
|
||||
nic = -1
|
||||
return nic
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Velocity
|
||||
# ---------------------------------------------
|
||||
|
||||
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), 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, 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 = -1 if int(msgbin[45]) else 1
|
||||
v_ew = util.bin2int(msgbin[46:56]) - 1 # east-west velocity
|
||||
|
||||
v_ns_sign = -1 if int(msgbin[56]) else 1
|
||||
v_ns = util.bin2int(msgbin[57:67]) - 1 # north-south velocity
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 = -1 if int(msgbin[68]) else 1
|
||||
vr = (util.bin2int(msgbin[69:78]) - 1) * 64 # vertical rate, fpm
|
||||
rocd = vr_sign * vr
|
||||
|
||||
return int(spd), round(trk_or_hdg, 1), int(rocd), tag
|
||||
|
||||
|
||||
def surface_velocity(msg):
|
||||
"""Decode surface velocity from from a surface position message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(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, 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.
|
||||
3
pyModeS/decoder/__init__.py
Normal file
3
pyModeS/decoder/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
from pyModeS.decoder import *
|
||||
21
pyModeS/decoder/acas.py
Normal file
21
pyModeS/decoder/acas.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Decoding Air-Air Surveillance (ACAS) DF=0/16
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
500
pyModeS/decoder/adsb.py
Normal file
500
pyModeS/decoder/adsb.py
Normal file
@@ -0,0 +1,500 @@
|
||||
# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
The wrapper for decoding ADS-B messages
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
import pyModeS as pms
|
||||
from pyModeS.decoder import common
|
||||
from pyModeS.decoder import uncertainty
|
||||
|
||||
# from pyModeS.decoder.bds import bds05, bds06, bds09
|
||||
from pyModeS.decoder.bds.bds05 import airborne_position, airborne_position_with_ref, altitude
|
||||
from pyModeS.decoder.bds.bds06 import surface_position, surface_position_with_ref, surface_velocity
|
||||
from pyModeS.decoder.bds.bds08 import category, callsign
|
||||
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
|
||||
|
||||
def df(msg):
|
||||
return common.df(msg)
|
||||
|
||||
def icao(msg):
|
||||
return common.icao(msg)
|
||||
|
||||
def typecode(msg):
|
||||
return common.typecode(msg)
|
||||
|
||||
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
||||
"""Decode position from a pair of even and odd position message
|
||||
(works with both airborne and surface position messages)
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
tc0 = typecode(msg0)
|
||||
tc1 = typecode(msg1)
|
||||
|
||||
if (5<=tc0<=8 and 5<=tc1<=8):
|
||||
if (not lat_ref) or (not lon_ref):
|
||||
raise RuntimeError("Surface position encountered, a reference \
|
||||
position lat/lon required. Location of \
|
||||
receiver can be used.")
|
||||
else:
|
||||
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
|
||||
|
||||
elif (9<=tc0<=18 and 9<=tc1<=18):
|
||||
# Airborne position with barometric height
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
elif (20<=tc0<=22 and 20<=tc1<=22):
|
||||
# Airborne position with GNSS height
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
|
||||
|
||||
def position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode position with only one message,
|
||||
knowing reference nearby location, such as previously
|
||||
calculated location, ground station, or airport location, etc.
|
||||
Works with both airborne and surface position messages.
|
||||
The reference position shall be with in 180NM (airborne) or 45NM (surface)
|
||||
of the true position.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
|
||||
tc = typecode(msg)
|
||||
|
||||
if 5<=tc<=8:
|
||||
return surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
elif 9<=tc<=18 or 20<=tc<=22:
|
||||
return airborne_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc<5 or tc==19 or tc>22:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
if tc>=5 and tc<=8:
|
||||
# surface position, altitude 0
|
||||
return 0
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
q = msgbin[47]
|
||||
if q:
|
||||
n = common.bin2int(msgbin[40:47]+msgbin[48:52])
|
||||
alt = n * 25 - 1000
|
||||
return alt
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
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), 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 oe_flag(msg):
|
||||
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: 0 or 1, for even or odd frame
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
return int(msgbin[53])
|
||||
|
||||
|
||||
def version(msg):
|
||||
"""ADS-B Version
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 31
|
||||
|
||||
Returns:
|
||||
int: version number
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 31:
|
||||
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
version = common.bin2int(msgbin[72:75])
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def nuc_p(msg):
|
||||
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string,
|
||||
|
||||
Returns:
|
||||
int: Horizontal Protection Limit
|
||||
int: 95% Containment Radius - Horizontal (meters)
|
||||
int: 95% Containment Radius - Vertical (meters)
|
||||
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)" % msg
|
||||
)
|
||||
|
||||
try:
|
||||
NUCp = uncertainty.TC_NUCp_lookup[tc]
|
||||
HPL = uncertainty.NUCp[NUCp]['HPL']
|
||||
RCu = uncertainty.NUCp[NUCp]['RCu']
|
||||
RCv = uncertainty.NUCp[NUCp]['RCv']
|
||||
except KeyError:
|
||||
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
|
||||
|
||||
|
||||
if tc in [20, 21]:
|
||||
RCv = uncertainty.NA
|
||||
|
||||
return HPL, RCu, RCv
|
||||
|
||||
|
||||
def nuc_v(msg):
|
||||
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string,
|
||||
|
||||
Returns:
|
||||
int or string: 95% Horizontal Velocity Error
|
||||
int or string: 95% Vertical Velocity Error
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg)
|
||||
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
NUCv = common.bin2int(msgbin[42:45])
|
||||
|
||||
try:
|
||||
HVE = uncertainty.NUCv[NUCv]['HVE']
|
||||
VVE = uncertainty.NUCv[NUCv]['VVE']
|
||||
except KeyError:
|
||||
HVE, VVE = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return HVE, VVE
|
||||
|
||||
|
||||
def nic_v1(msg, NICs):
|
||||
"""Calculate NIC, navigation integrity category, for ADS-B version 1
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
NICs (int or string): NIC supplement
|
||||
|
||||
Returns:
|
||||
int or string: Horizontal Radius of Containment
|
||||
int or string: Vertical Protection Limit
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)" % msg
|
||||
)
|
||||
|
||||
tc = typecode(msg)
|
||||
NIC = uncertainty.TC_NICv1_lookup[tc]
|
||||
|
||||
if isinstance(NIC, dict):
|
||||
NIC = NIC[NICs]
|
||||
|
||||
try:
|
||||
Rc = uncertainty.NICv1[NIC][NICs]['Rc']
|
||||
VPL = uncertainty.NICv1[NIC][NICs]['VPL']
|
||||
except KeyError:
|
||||
Rc, VPL = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return Rc, VPL
|
||||
|
||||
|
||||
def nic_v2(msg, NICa, NICbc):
|
||||
"""Calculate NIC, navigation integrity category, for ADS-B version 2
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
NICa (int or string): NIC supplement - A
|
||||
NICbc (int or srting): NIC supplement - B or C
|
||||
|
||||
Returns:
|
||||
int or string: Horizontal Radius of Containment
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)" % msg
|
||||
)
|
||||
|
||||
tc = typecode(msg)
|
||||
NIC = uncertainty.TC_NICv2_lookup[tc]
|
||||
|
||||
if 20<=tc<=22:
|
||||
NICs = 0
|
||||
else:
|
||||
NICs = NICa*2 + NICbc
|
||||
|
||||
try:
|
||||
if isinstance(NIC, dict):
|
||||
NIC = NIC[NICs]
|
||||
|
||||
Rc = uncertainty.NICv2[NIC][NICs]['Rc']
|
||||
except KeyError:
|
||||
Rc = uncertainty.NA
|
||||
|
||||
return Rc
|
||||
|
||||
|
||||
def nic_s(msg):
|
||||
"""Obtain NIC supplement bit, TC=31 message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: NICs number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 31:
|
||||
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_s = int(msgbin[75])
|
||||
|
||||
return nic_s
|
||||
|
||||
|
||||
def nic_a_c(msg):
|
||||
"""Obtain NICa/c, navigation integrity category supplements a and c
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(int, int): NICa and NICc number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 31:
|
||||
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_a = int(msgbin[75])
|
||||
nic_c = int(msgbin[51])
|
||||
|
||||
return nic_a, nic_c
|
||||
|
||||
|
||||
def nic_b(msg):
|
||||
"""Obtain NICb, navigation integrity category supplement-b
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: NICb number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc < 9 or tc > 18:
|
||||
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_b = int(msgbin[39])
|
||||
|
||||
return nic_b
|
||||
|
||||
|
||||
def nac_p(msg):
|
||||
"""Calculate NACp, Navigation Accuracy Category - Position
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 29 or 31
|
||||
|
||||
Returns:
|
||||
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
|
||||
int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc not in [29, 31]:
|
||||
raise RuntimeError("%s: Not a target state and status message, \
|
||||
or operation status message, expecting TC = 29 or 31" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
if tc == 29:
|
||||
NACp = common.bin2int(msgbin[71:75])
|
||||
elif tc == 31:
|
||||
NACp = common.bin2int(msgbin[76:80])
|
||||
|
||||
try:
|
||||
EPU = uncertainty.NACp[NACp]['EPU']
|
||||
VEPU = uncertainty.NACp[NACp]['VEPU']
|
||||
except KeyError:
|
||||
EPU, VEPU = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return EPU, VEPU
|
||||
|
||||
|
||||
def nac_v(msg):
|
||||
"""Calculate NACv, Navigation Accuracy Category - Velocity
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 19
|
||||
|
||||
Returns:
|
||||
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
|
||||
int or string: 95% vertical accuracy bounds for velocity, Vertical Figure of Merit
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
NACv = common.bin2int(msgbin[42:45])
|
||||
|
||||
try:
|
||||
HFOMr = uncertainty.NACv[NACv]['HFOMr']
|
||||
VFOMr = uncertainty.NACv[NACv]['VFOMr']
|
||||
except KeyError:
|
||||
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return HFOMr, VFOMr
|
||||
|
||||
|
||||
def sil(msg, version):
|
||||
"""Calculate SIL, Surveillance Integrity Level
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string with TC = 29, 31
|
||||
|
||||
Returns:
|
||||
int or string: Probability of exceeding Horizontal Radius of Containment RCu
|
||||
int or string: Probability of exceeding Vertical Integrity Containment Region VPL
|
||||
string: SIL supplement based on per "hour" or "sample", or 'unknown'
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc not in [29, 31]:
|
||||
raise RuntimeError("%s: Not a target state and status messag, \
|
||||
or operation status message, expecting TC = 29 or 31" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
if tc == 29:
|
||||
SIL = common.bin2int(msgbin[76:78])
|
||||
elif tc == 31:
|
||||
SIL = common.bin2int(msgbin[82:84])
|
||||
|
||||
try:
|
||||
PE_RCu = uncertainty.SIL[SIL]['PE_RCu']
|
||||
PE_VPL = uncertainty.SIL[SIL]['PE_VPL']
|
||||
except KeyError:
|
||||
PE_RCu, PE_VPL = uncertainty.NA, uncertainty.NA
|
||||
|
||||
base = 'unknown'
|
||||
|
||||
if version == 2:
|
||||
if tc == 29:
|
||||
SIL_SUP = common.bin2int(msgbin[39])
|
||||
elif tc == 31:
|
||||
SIL_SUP = common.bin2int(msgbin[86])
|
||||
|
||||
if SIL_SUP == 0:
|
||||
base = "hour"
|
||||
elif SIL_SUP == 1:
|
||||
base = "sample"
|
||||
|
||||
return PE_RCu, PE_VPL, base
|
||||
21
pyModeS/decoder/allcall.py
Normal file
21
pyModeS/decoder/allcall.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Decoding all call replies DF=11
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
147
pyModeS/decoder/bds/__init__.py
Normal file
147
pyModeS/decoder/bds/__init__.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
Common functions for Mode-S decoding
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import numpy as np
|
||||
|
||||
from pyModeS.extra import aero
|
||||
from pyModeS.decoder import common
|
||||
from pyModeS.decoder.bds import bds05, bds06, bds08, bds09, \
|
||||
bds10, bds17, bds20, bds30, bds40, bds44, bds50, bds53, bds60
|
||||
|
||||
|
||||
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.radians(angle))
|
||||
vy = v * np.cos(np.radians(angle))
|
||||
return vx, vy
|
||||
|
||||
if not (bds50.is50(msg) and bds60.is60(msg)):
|
||||
return None
|
||||
|
||||
h50 = bds50.trk50(msg)
|
||||
v50 = bds50.gs50(msg)
|
||||
|
||||
if h50 is None or v50 is None:
|
||||
return 'BDS50,BDS60'
|
||||
|
||||
h60 = bds60.hdg60(msg)
|
||||
m60 = bds60.mach60(msg)
|
||||
i60 = bds60.ias60(msg)
|
||||
|
||||
if h60 is None or (m60 is None and i60 is None):
|
||||
return 'BDS50,BDS60'
|
||||
|
||||
m60 = np.nan if m60 is None else m60
|
||||
i60 = np.nan if i60 is None else i60
|
||||
|
||||
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))
|
||||
|
||||
# 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
|
||||
try:
|
||||
dist = np.linalg.norm(X-Mu, axis=1)
|
||||
BDS = allbds[np.nanargmin(dist)]
|
||||
except ValueError:
|
||||
return 'BDS50,BDS60'
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
df = common.df(msg)
|
||||
|
||||
if common.allzeros(msg):
|
||||
return 'EMPTY'
|
||||
|
||||
# For ADS-B / Mode-S extended squitter
|
||||
if df == 17:
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if 1 <= tc <= 4:
|
||||
return 'BDS08' # indentification and category
|
||||
if 5 <= tc <= 8:
|
||||
return 'BDS06' # surface movement
|
||||
if 9 <= tc <= 18:
|
||||
return 'BDS05' # airborne position, baro-alt
|
||||
if tc == 19:
|
||||
return 'BDS09' # airborne velocity
|
||||
if 20 <= tc <= 22:
|
||||
return 'BDS05' # airborne position, gnss-alt
|
||||
if tc == 28:
|
||||
return 'BDS61' # aircraft status
|
||||
if tc == 29:
|
||||
return 'BDS62' # target state and status
|
||||
if tc == 31:
|
||||
return 'BDS65' # operational status
|
||||
|
||||
# For Comm-B replies, ELS + EHS only
|
||||
IS10 = bds10.is10(msg)
|
||||
IS17 = bds17.is17(msg)
|
||||
IS20 = bds20.is20(msg)
|
||||
IS30 = bds30.is30(msg)
|
||||
IS40 = bds40.is40(msg)
|
||||
IS50 = bds50.is50(msg)
|
||||
IS60 = bds60.is60(msg)
|
||||
|
||||
allbds = np.array([
|
||||
"BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60"
|
||||
])
|
||||
|
||||
mask = [IS10, IS17, IS20, IS30, IS40, IS50, IS60]
|
||||
|
||||
bds = ','.join(sorted(allbds[mask]))
|
||||
|
||||
if len(bds) == 0:
|
||||
return None
|
||||
else:
|
||||
return bds
|
||||
162
pyModeS/decoder/bds/bds05.py
Normal file
162
pyModeS/decoder/bds/bds05.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,5
|
||||
ADS-B TC=9-18
|
||||
Airborn position
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
def airborne_position(msg0, msg1, t0, t1):
|
||||
"""Decode airborn position from a pair of even and odd position message
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
mb0 = common.hex2bin(msg0)[32:]
|
||||
mb1 = common.hex2bin(msg1)[32:]
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
|
||||
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
|
||||
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
|
||||
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
|
||||
|
||||
air_d_lat_even = 360.0 / 60
|
||||
air_d_lat_odd = 360.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
|
||||
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
if lat_even >= 270:
|
||||
lat_even = lat_even - 360
|
||||
|
||||
if lat_odd >= 270:
|
||||
lat_odd = lat_odd - 360
|
||||
|
||||
# check if both are in the same latidude zone, exit if not
|
||||
if common.cprNL(lat_even) != common.cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
lat = lat_even
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat)- 0, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat) - 1, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
if lon > 180:
|
||||
lon = lon - 360
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode airborne position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 180NM of the true position.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
||||
|
||||
i = int(mb[21])
|
||||
d_lat = 360.0/59 if i else 360.0/60
|
||||
|
||||
j = common.floor(lat_ref / d_lat) \
|
||||
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = common.cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 360.0 / ni
|
||||
else:
|
||||
d_lon = 360.0
|
||||
|
||||
m = common.floor(lon_ref / d_lon) \
|
||||
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if tc<9 or tc==19 or tc>22:
|
||||
raise RuntimeError("%s: Not a airborn position message" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
if tc < 19:
|
||||
# barometric altitude
|
||||
q = mb[15]
|
||||
if q:
|
||||
n = common.bin2int(mb[8:15]+mb[16:20])
|
||||
alt = n * 25 - 1000
|
||||
else:
|
||||
alt = None
|
||||
else:
|
||||
# GNSS altitude, meters -> feet
|
||||
alt = common.bin2int(mb[8:20]) * 3.28084
|
||||
|
||||
return alt
|
||||
187
pyModeS/decoder/bds/bds06.py
Normal file
187
pyModeS/decoder/bds/bds06.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,6
|
||||
ADS-B TC=5-8
|
||||
Surface position
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
import math
|
||||
|
||||
|
||||
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
"""Decode surface position from a pair of even and odd position message,
|
||||
the lat/lon of receiver must be provided to yield the correct solution.
|
||||
|
||||
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
|
||||
lat_ref (float): latitude of the receiver
|
||||
lon_ref (float): longitude of the receiver
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
msgbin0 = common.hex2bin(msg0)
|
||||
msgbin1 = common.hex2bin(msg1)
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
|
||||
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
|
||||
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
|
||||
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
|
||||
|
||||
air_d_lat_even = 90.0 / 60
|
||||
air_d_lat_odd = 90.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_s = lat_even_n - 90.0
|
||||
lat_odd_s = lat_odd_n - 90.0
|
||||
|
||||
# chose which solution corrispondes to receiver location
|
||||
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
|
||||
lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s
|
||||
|
||||
# check if both are in the same latidude zone, rare but possible
|
||||
if common.cprNL(lat_even) != common.cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
lat = lat_even
|
||||
nl = common.cprNL(lat_even)
|
||||
ni = max(common.cprNL(lat_even) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = common.cprNL(lat_odd)
|
||||
ni = max(common.cprNL(lat_odd) - 1, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
# four possible longitude solutions
|
||||
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
|
||||
|
||||
# the closest solution to receiver is the correct one
|
||||
dls = [abs(lon_ref - l) for l in lons]
|
||||
imin = min(range(4), key=dls.__getitem__)
|
||||
lon = lons[imin]
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode surface position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 45NM of the true position.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
||||
|
||||
i = int(mb[21])
|
||||
d_lat = 90.0/59 if i else 90.0/60
|
||||
|
||||
j = common.floor(lat_ref / d_lat) \
|
||||
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = common.cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 90.0 / ni
|
||||
else:
|
||||
d_lon = 90.0
|
||||
|
||||
m = common.floor(lon_ref / d_lon) \
|
||||
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_velocity(msg):
|
||||
"""Decode surface velocity from from a surface position message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(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 common.typecode(msg) < 5 or common.typecode(msg) > 8:
|
||||
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
# ground track
|
||||
trk_status = int(mb[12])
|
||||
if trk_status == 1:
|
||||
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
|
||||
trk = round(trk, 1)
|
||||
else:
|
||||
trk = None
|
||||
|
||||
# ground movment / speed
|
||||
mov = common.bin2int(mb[5:12])
|
||||
|
||||
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'
|
||||
75
pyModeS/decoder/bds/bds08.py
Normal file
75
pyModeS/decoder/bds/bds08.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,8
|
||||
ADS-B TC=1-4
|
||||
Aircraft identitification and category
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
def category(msg):
|
||||
"""Aircraft category number
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: category number
|
||||
"""
|
||||
|
||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
return common.bin2int(msgbin[5:8])
|
||||
|
||||
|
||||
def callsign(msg):
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: callsign
|
||||
"""
|
||||
|
||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
|
||||
msgbin = common.hex2bin(msg)
|
||||
csbin = msgbin[40:96]
|
||||
|
||||
cs = ''
|
||||
cs += chars[common.bin2int(csbin[0:6])]
|
||||
cs += chars[common.bin2int(csbin[6:12])]
|
||||
cs += chars[common.bin2int(csbin[12:18])]
|
||||
cs += chars[common.bin2int(csbin[18:24])]
|
||||
cs += chars[common.bin2int(csbin[24:30])]
|
||||
cs += chars[common.bin2int(csbin[30:36])]
|
||||
cs += chars[common.bin2int(csbin[36:42])]
|
||||
cs += chars[common.bin2int(csbin[42:48])]
|
||||
|
||||
# clean string, remove spaces and marks, if any.
|
||||
# cs = cs.replace('_', '')
|
||||
cs = cs.replace('#', '')
|
||||
return cs
|
||||
117
pyModeS/decoder/bds/bds09.py
Normal file
117
pyModeS/decoder/bds/bds09.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,9
|
||||
ADS-B TC=19
|
||||
Aircraft Airborn velocity
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
import math
|
||||
|
||||
|
||||
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 common.typecode(msg) != 19:
|
||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:8])
|
||||
|
||||
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
|
||||
return None
|
||||
|
||||
if subtype in (1, 2):
|
||||
v_ew_sign = -1 if mb[13]=='1' else 1
|
||||
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
|
||||
|
||||
v_ns_sign = -1 if mb[24]=='1' else 1
|
||||
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
|
||||
|
||||
v_we = v_ew_sign * v_ew
|
||||
v_sn = v_ns_sign * v_ns
|
||||
|
||||
spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts
|
||||
spd = int(spd)
|
||||
|
||||
trk = math.atan2(v_we, v_sn)
|
||||
trk = math.degrees(trk) # convert to degrees
|
||||
trk = trk if trk >= 0 else trk + 360 # no negative val
|
||||
|
||||
tag = 'GS'
|
||||
trk_or_hdg = round(trk, 2)
|
||||
|
||||
else:
|
||||
if mb[13] == '0':
|
||||
hdg = None
|
||||
else:
|
||||
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
|
||||
hdg = round(hdg, 2)
|
||||
|
||||
trk_or_hdg = hdg
|
||||
|
||||
spd = common.bin2int(mb[25:35])
|
||||
spd = None if spd==0 else spd-1
|
||||
|
||||
if mb[24]=='0':
|
||||
tag = 'IAS'
|
||||
else:
|
||||
tag = 'TAS'
|
||||
|
||||
vr_sign = -1 if mb[36]=='1' else 1
|
||||
vr = common.bin2int(mb[37:46])
|
||||
rocd = None if vr==0 else int(vr_sign*(vr-1)*64)
|
||||
|
||||
return spd, trk_or_hdg, rocd, tag
|
||||
|
||||
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.
|
||||
"""
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
sign = -1 if int(msgbin[80]) else 1
|
||||
value = common.bin2int(msgbin[81:88])
|
||||
|
||||
if value == 0 or value == 127:
|
||||
return None
|
||||
else:
|
||||
return sign * (value - 1) * 25 # in ft.
|
||||
66
pyModeS/decoder/bds/bds10.py
Normal file
66
pyModeS/decoder/bds/bds10.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 1,0
|
||||
# Data link capability report
|
||||
# ------------------------------------------
|
||||
|
||||
def is10(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])
|
||||
75
pyModeS/decoder/bds/bds17.py
Normal file
75
pyModeS/decoder/bds/bds17.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 1,7
|
||||
Common usage GICB capability report
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
def is17(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', 'BDS08', 'BDS09', 'BDS20']).issubset(caps):
|
||||
# return False
|
||||
|
||||
# at least you can respond who you are
|
||||
if 'BDS20' not in 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
|
||||
73
pyModeS/decoder/bds/bds20.py
Normal file
73
pyModeS/decoder/bds/bds20.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 2,0
|
||||
# Aircraft identification
|
||||
# ------------------------------------------
|
||||
|
||||
def is20(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] != '00100000':
|
||||
return False
|
||||
|
||||
cs = cs20(msg)
|
||||
|
||||
if '#' in cs:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def cs20(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
|
||||
50
pyModeS/decoder/bds/bds30.py
Normal file
50
pyModeS/decoder/bds/bds30.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 3,0
|
||||
# ACAS active resolution advisory
|
||||
# ------------------------------------------
|
||||
|
||||
def is30(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
|
||||
119
pyModeS/decoder/bds/bds40.py
Normal file
119
pyModeS/decoder/bds/bds40.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,0
|
||||
# Selected vertical intention
|
||||
# ------------------------------------------
|
||||
|
||||
def is40(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
|
||||
|
||||
if wrongstatus(d, 48, 49, 51):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 54, 55, 56):
|
||||
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
|
||||
218
pyModeS/decoder/bds/bds44.py
Normal file
218
pyModeS/decoder/bds/bds44.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,4
|
||||
# Meteorological routine air report
|
||||
# ------------------------------------------
|
||||
|
||||
def is44(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 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)
|
||||
188
pyModeS/decoder/bds/bds50.py
Normal file
188
pyModeS/decoder/bds/bds50.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 5,0
|
||||
# Track and turn report
|
||||
# ------------------------------------------
|
||||
|
||||
def is50(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
|
||||
|
||||
roll = roll50(msg)
|
||||
if (roll is not None) and abs(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
|
||||
181
pyModeS/decoder/bds/bds53.py
Normal file
181
pyModeS/decoder/bds/bds53.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 5,3
|
||||
# Air-referenced state vector
|
||||
# ------------------------------------------
|
||||
|
||||
def is53(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
|
||||
189
pyModeS/decoder/bds/bds60.py
Normal file
189
pyModeS/decoder/bds/bds60.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 6,0
|
||||
# Heading and speed report
|
||||
# ------------------------------------------
|
||||
|
||||
def is60(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
|
||||
15
pyModeS/decoder/commb.py
Normal file
15
pyModeS/decoder/commb.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
# ELS - elementary surveillance
|
||||
from pyModeS.decoder.bds.bds10 import *
|
||||
from pyModeS.decoder.bds.bds17 import *
|
||||
from pyModeS.decoder.bds.bds20 import *
|
||||
from pyModeS.decoder.bds.bds30 import *
|
||||
|
||||
# ELS - enhanced surveillance
|
||||
from pyModeS.decoder.bds.bds40 import *
|
||||
from pyModeS.decoder.bds.bds50 import *
|
||||
from pyModeS.decoder.bds.bds60 import *
|
||||
|
||||
# MRAR
|
||||
from pyModeS.decoder.bds.bds44 import *
|
||||
313
pyModeS/decoder/common.py
Normal file
313
pyModeS/decoder/common.py
Normal file
@@ -0,0 +1,313 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import numpy as np
|
||||
|
||||
def hex2bin(hexstr):
|
||||
"""Convert a hexdecimal string to binary string, with zero fillings. """
|
||||
num_of_bits = len(hexstr) * 4
|
||||
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
|
||||
return binstr
|
||||
|
||||
|
||||
def bin2int(binstr):
|
||||
"""Convert a binary string to integer. """
|
||||
return int(binstr, 2)
|
||||
|
||||
|
||||
def hex2int(hexstr):
|
||||
"""Convert a hexdecimal string to integer. """
|
||||
return int(hexstr, 16)
|
||||
|
||||
|
||||
def bin2np(binstr):
|
||||
"""Convert a binary string to numpy array. """
|
||||
return np.array([int(i) for i in binstr])
|
||||
|
||||
|
||||
def np2bin(npbin):
|
||||
"""Convert a binary numpy array to string. """
|
||||
return np.array2string(npbin, separator='')[1:-1]
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Decode Downlink Format vaule, bits 1 to 5."""
|
||||
msgbin = hex2bin(msg)
|
||||
return min( bin2int(msgbin[0:5]) , 24 )
|
||||
|
||||
|
||||
def crc(msg, encode=False):
|
||||
"""Mode-S Cyclic Redundancy Check
|
||||
Detect if bit error occurs in the Mode-S message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
encode (bool): True to encode the date only and return the checksum
|
||||
Returns:
|
||||
string: message checksum, or partity bits (encoder)
|
||||
"""
|
||||
|
||||
# the polynominal generattor code for CRC [1111111111111010000001001]
|
||||
generator = np.array([1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,1,0,0,1])
|
||||
ng = len(generator)
|
||||
|
||||
msgnpbin = bin2np(hex2bin(msg))
|
||||
|
||||
if encode:
|
||||
msgnpbin[-24:] = [0] * 24
|
||||
|
||||
# loop all bits, except last 24 piraty bits
|
||||
for i in range(len(msgnpbin)-24):
|
||||
if msgnpbin[i] == 0:
|
||||
continue
|
||||
|
||||
# perform XOR, when 1
|
||||
msgnpbin[i:i+ng] = np.bitwise_xor(msgnpbin[i:i+ng], generator)
|
||||
|
||||
# last 24 bits
|
||||
reminder = np2bin(msgnpbin[-24:])
|
||||
return reminder
|
||||
|
||||
|
||||
def floor(x):
|
||||
""" Mode-S floor function
|
||||
|
||||
Defined as the greatest integer value k, such that k <= x
|
||||
|
||||
eg.: floor(3.6) = 3, while floor(-3.6) = -4
|
||||
"""
|
||||
return int(np.floor(x))
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
DF = df(msg)
|
||||
|
||||
if DF in (11, 17, 18):
|
||||
addr = msg[2:8]
|
||||
elif DF in (0, 4, 5, 16, 20, 21):
|
||||
c0 = bin2int(crc(msg, encode=True))
|
||||
c1 = hex2int(msg[-6:])
|
||||
addr = '%06X' % (c0 ^ c1)
|
||||
else:
|
||||
addr = None
|
||||
|
||||
return addr
|
||||
|
||||
|
||||
def is_icao_assigned(icao):
|
||||
""" Check whether the ICAO address is assigned (Annex 10, Vol 3)"""
|
||||
|
||||
if (icao is None) or (not isinstance(icao, str)) or (len(icao)!=6):
|
||||
return False
|
||||
|
||||
icaoint = hex2int(icao)
|
||||
|
||||
if 0x200000 < icaoint < 0x27FFFF: return False # AFI
|
||||
if 0x280000 < icaoint < 0x28FFFF: return False # SAM
|
||||
if 0x500000 < icaoint < 0x5FFFFF: return False # EUR, NAT
|
||||
if 0x600000 < icaoint < 0x67FFFF: return False # MID
|
||||
if 0x680000 < icaoint < 0x6F0000: return False # ASIA
|
||||
if 0x900000 < icaoint < 0x9FFFFF: return False # NAM, PAC
|
||||
if 0xB00000 < icaoint < 0xBFFFFF: return False # CAR
|
||||
if 0xD00000 < icaoint < 0xDFFFFF: return False # future
|
||||
if 0xF00000 < icaoint < 0xFFFFFF: return False # future
|
||||
|
||||
return True
|
||||
|
||||
def typecode(msg):
|
||||
"""Type code of ADS-B message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: type code number
|
||||
"""
|
||||
if df(msg) not in (17, 18):
|
||||
return None
|
||||
|
||||
msgbin = hex2bin(msg)
|
||||
return bin2int(msgbin[32:37])
|
||||
|
||||
|
||||
def cprNL(lat):
|
||||
"""NL() function in CPR decoding"""
|
||||
|
||||
if lat == 0:
|
||||
return 59
|
||||
|
||||
if lat == 87 or lat == -87:
|
||||
return 2
|
||||
|
||||
if lat > 87 or lat < -87:
|
||||
return 1
|
||||
|
||||
nz = 15
|
||||
a = 1 - np.cos(np.pi / (2 * nz))
|
||||
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
|
||||
nl = 2 * np.pi / (np.arccos(1 - a/b))
|
||||
NL = floor(nl)
|
||||
return NL
|
||||
|
||||
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 df(msg) not in [5, 21]:
|
||||
raise RuntimeError("Message must be Downlink Format 5 or 21.")
|
||||
|
||||
mbin = 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 df(msg) not in [0, 4, 16, 20]:
|
||||
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
|
||||
|
||||
# Altitude code, bit 20-32
|
||||
mbin = 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 = 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(bin2int(vbin) * 3.28084) # convert to ft
|
||||
|
||||
return alt
|
||||
|
||||
|
||||
def gray2alt(codestr):
|
||||
gc500 = codestr[:8]
|
||||
n500 = gray2int(gc500)
|
||||
|
||||
# in 100-ft step must be converted first
|
||||
gc100 = codestr[8:]
|
||||
n100 = 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
|
||||
|
||||
|
||||
def gray2int(graystr):
|
||||
"""Convert greycode to binary"""
|
||||
num = bin2int(graystr)
|
||||
num ^= (num >> 8)
|
||||
num ^= (num >> 4)
|
||||
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 = hex2bin(data(msg))
|
||||
|
||||
if 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 = bin2int(data[msb-1:lsb])
|
||||
|
||||
if not status:
|
||||
if value != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
18
pyModeS/decoder/ehs.py
Normal file
18
pyModeS/decoder/ehs.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import warnings
|
||||
|
||||
from pyModeS.decoder.bds.bds40 import *
|
||||
from pyModeS.decoder.bds.bds50 import *
|
||||
from pyModeS.decoder.bds.bds60 import *
|
||||
from pyModeS.decoder.bds import infer
|
||||
|
||||
warnings.simplefilter('once', DeprecationWarning)
|
||||
warnings.warn("pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning)
|
||||
|
||||
def BDS(msg):
|
||||
warnings.warn("pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning)
|
||||
return infer(msg)
|
||||
|
||||
def icao(msg):
|
||||
from pyModeS.decoder.common import icao
|
||||
return icao(msg)
|
||||
10
pyModeS/decoder/els.py
Normal file
10
pyModeS/decoder/els.py
Normal file
@@ -0,0 +1,10 @@
|
||||
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 *
|
||||
|
||||
import warnings
|
||||
warnings.simplefilter('once', DeprecationWarning)
|
||||
warnings.warn("pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning)
|
||||
21
pyModeS/decoder/surv.py
Normal file
21
pyModeS/decoder/surv.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Warpper for short roll call surveillance replies DF=4/5
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
120
pyModeS/decoder/uncertainty.py
Normal file
120
pyModeS/decoder/uncertainty.py
Normal file
@@ -0,0 +1,120 @@
|
||||
NA = None
|
||||
|
||||
TC_NUCp_lookup = {
|
||||
0:0, 5:9, 6:8, 7:7, 8:6,
|
||||
9:9, 10:8, 11:7, 12:6, 13:5, 14:4, 15:3, 16:2, 17:1, 18:0,
|
||||
20:9, 21:8, 22:0
|
||||
}
|
||||
|
||||
TC_NICv1_lookup = {
|
||||
5:11, 6:10, 7:9, 8:0,
|
||||
9:11, 10:10, 11:{1:9, 0:8}, 12:7, 13:6, 14:5, 15:4, 16:{1:3, 0:2}, 17:1, 18:0,
|
||||
20:11, 21:10, 22:0
|
||||
}
|
||||
|
||||
TC_NICv2_lookup = {
|
||||
5:11, 6:10, 7:{2:9, 0:8}, 8:{3:7, 2:6, 1:6, 0:0},
|
||||
9:11, 10:10, 11:{3:9, 0:8}, 12:7, 13:6, 14:5, 15:4, 16:{3:3, 0:2}, 17:1, 18:0,
|
||||
20:11, 21:10, 22:0
|
||||
}
|
||||
|
||||
|
||||
NUCp = {
|
||||
9: {'HPL':7.5, 'RCu':3, 'RCv':4},
|
||||
8: {'HPL':25, 'RCu':10, 'RCv':15},
|
||||
7: {'HPL':185, 'RCu':93, 'RCv':NA},
|
||||
6: {'HPL':370, 'RCu':185, 'RCv':NA},
|
||||
5: {'HPL':926, 'RCu':463, 'RCv':NA},
|
||||
4: {'HPL':1852, 'RCu':926, 'RCv':NA},
|
||||
3: {'HPL':3704, 'RCu':1852, 'RCv':NA},
|
||||
2: {'HPL':18520, 'RCu':9260, 'RCv':NA},
|
||||
1: {'HPL':37040, 'RCu':18520, 'RCv':NA},
|
||||
0: {'HPL':NA, 'RCu':NA, 'RCv':NA},
|
||||
}
|
||||
|
||||
NUCv = {
|
||||
0: {'HVE':NA, 'VVE':NA},
|
||||
1: {'HVE':10, 'VVE':15.2},
|
||||
2: {'HVE':3, 'VVE':4.5},
|
||||
3: {'HVE':1, 'VVE':1.5},
|
||||
4: {'HVE':0.3, 'VVE':0.46},
|
||||
}
|
||||
|
||||
NACp = {
|
||||
11: {'EPU': 3, 'VEPU': 4},
|
||||
10: {'EPU': 10, 'VEPU': 15},
|
||||
9: {'EPU': 30, 'VEPU': 45},
|
||||
8: {'EPU': 93, 'VEPU': NA},
|
||||
7: {'EPU': 185, 'VEPU': NA},
|
||||
6: {'EPU': 556, 'VEPU': NA},
|
||||
5: {'EPU': 926, 'VEPU': NA},
|
||||
4: {'EPU': 1852, 'VEPU': NA},
|
||||
3: {'EPU': 3704, 'VEPU': NA},
|
||||
2: {'EPU': 7408, 'VEPU': NA},
|
||||
1: {'EPU': 18520, 'VEPU': NA},
|
||||
0: {'EPU': NA, 'VEPU': NA},
|
||||
}
|
||||
|
||||
NACv = {
|
||||
0: {'HFOMr':NA, 'VFOMr':NA},
|
||||
1: {'HFOMr':10, 'VFOMr':15.2},
|
||||
2: {'HFOMr':3, 'VFOMr':4.5},
|
||||
3: {'HFOMr':1, 'VFOMr':1.5},
|
||||
4: {'HFOMr':0.3, 'VFOMr':0.46},
|
||||
}
|
||||
|
||||
SIL = {
|
||||
3: {'PE_RCu': 1e-7, 'PE_VPL': 2e-7},
|
||||
2: {'PE_RCu': 1e-5, 'PE_VPL': 1e-5},
|
||||
1: {'PE_RCu': 1e-3, 'PE_VPL': 1e-3},
|
||||
0: {'PE_RCu': NA, 'PE_VPL': NA},
|
||||
}
|
||||
|
||||
|
||||
NICv1 = {
|
||||
# NIC is used as the index at second Level
|
||||
|
||||
11: {0: {'Rc': 7.5, 'VPL': 11}},
|
||||
10: {0: {'Rc': 25, 'VPL': 37.5}},
|
||||
9: {1: {'Rc': 75, 'VPL': 112}},
|
||||
8: {0: {'Rc': 185, 'VPL': NA}},
|
||||
7: {0: {'Rc': 370, 'VPL': NA}},
|
||||
6: {
|
||||
0: {'Rc': 926, 'VPL': NA},
|
||||
1: {'Rc': 1111, 'VPL': NA},
|
||||
},
|
||||
5: {0: {'Rc': 1852, 'VPL': NA}},
|
||||
4: {0: {'Rc': 3702, 'VPL': NA}},
|
||||
3: {1: {'Rc': 7408, 'VPL': NA}},
|
||||
2: {0: {'Rc': 14008, 'VPL': NA}},
|
||||
1: {0: {'Rc': 37000, 'VPL': NA}},
|
||||
0: {0: {'Rc': NA, 'VPL': NA}},
|
||||
}
|
||||
|
||||
NICv2 = {
|
||||
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
|
||||
|
||||
11: {0: {'Rc': 7.5}},
|
||||
10: {0: {'Rc': 25}},
|
||||
9: {
|
||||
2: {'Rc': 75},
|
||||
3: {'Rc': 75},
|
||||
},
|
||||
8: {0: {'Rc': 185}},
|
||||
7: {
|
||||
0: {'Rc': 370},
|
||||
3: {'Rc': 370},
|
||||
},
|
||||
6: {
|
||||
0: {'Rc': 926},
|
||||
1: {'Rc': 556},
|
||||
2: {'Rc': 556},
|
||||
3: {'Rc': 1111},
|
||||
},
|
||||
5: {0: {'Rc': 1852}},
|
||||
4: {0: {'Rc': 3702}},
|
||||
3: {3: {'Rc': 7408}},
|
||||
2: {0: {'Rc': 14008}},
|
||||
1: {0: {'Rc': 37000}},
|
||||
0: {0: {'Rc': NA}},
|
||||
}
|
||||
999
pyModeS/ehs.py
999
pyModeS/ehs.py
@@ -1,999 +0,0 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
A python package for decoding ModeS (DF20, DF21) messages.
|
||||
"""
|
||||
|
||||
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 isnull(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 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 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)
|
||||
|
||||
# ------------------------------------------
|
||||
# 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):
|
||||
"""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 isnull(msg):
|
||||
return False
|
||||
|
||||
# status bit 1, 14, and 27
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
if util.bin2int(d[0:4]) != 2 or util.bin2int(d[4:8]) != 0:
|
||||
result &= False
|
||||
|
||||
cs = callsign(msg)
|
||||
|
||||
if '#' in cs:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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 isnull(msg):
|
||||
return False
|
||||
|
||||
# status bit 1, 14, and 27
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
result = result & checkbits(d, 1, 2, 13) \
|
||||
& checkbits(d, 14, 15, 26) & checkbits(d, 27, 28, 39)
|
||||
|
||||
# bits 40-47 and 52-53 shall all be zero
|
||||
if util.bin2int(d[39:47]) != 0:
|
||||
result &= False
|
||||
|
||||
if util.bin2int(d[51:53]) != 0:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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 isnull(msg):
|
||||
return False
|
||||
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
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
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
vw = wind44(msg, rev=rev)
|
||||
if vw is not None and vw[0] > 250:
|
||||
result &= False
|
||||
|
||||
if temp44(msg):
|
||||
if temp44(msg) > 60 or temp44(msg) < -80:
|
||||
result &= False
|
||||
elif temp44(msg) == 0:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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 isnull(msg):
|
||||
return False
|
||||
|
||||
# status bit 1, 12, 24, 35, 46
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
result = result & checkbits(d, 1, 3, 11) & checkbits(d, 12, 13, 23) \
|
||||
& 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:
|
||||
roll = abs(roll50(msg))
|
||||
if roll and roll > 60:
|
||||
result &= False
|
||||
|
||||
gs = gs50(msg)
|
||||
if gs is not None and gs > 600:
|
||||
result &= False
|
||||
|
||||
tas = tas50(msg)
|
||||
if tas is not None and tas > 500:
|
||||
result &= False
|
||||
|
||||
if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200):
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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, 1)
|
||||
|
||||
|
||||
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 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):
|
||||
"""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 isnull(msg):
|
||||
return False
|
||||
|
||||
# status bit 1, 13, 24, 35, 46
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
result = result & checkbits(d, 1, 2, 12) & checkbits(d, 13, 14, 23) \
|
||||
& checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \
|
||||
& checkbits(d, 46, 47, 56)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
ias = ias60(msg)
|
||||
if ias is not None and ias > 500:
|
||||
result &= False
|
||||
|
||||
mach = mach60(msg)
|
||||
if mach is not None and mach > 1:
|
||||
result &= False
|
||||
|
||||
# leave out the check from vertical rates,
|
||||
# due to very noisy measurement
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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, 1)
|
||||
|
||||
|
||||
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 BDS(msg):
|
||||
"""Estimate the most likely BDS code of an message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
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)
|
||||
is40 = isBDS40(msg)
|
||||
is44 = isBDS44(msg)
|
||||
is44rev = isBDS44(msg, rev=True)
|
||||
is50 = isBDS50(msg)
|
||||
is53 = isBDS53(msg)
|
||||
is60 = isBDS60(msg)
|
||||
|
||||
BDS = ["BDS17", "BDS20", "BDS40", "BDS44", "BDS44REV", "BDS50", "BDS53", "BDS60"]
|
||||
isBDS = [is17, is20, is40, is44, is44rev, is50, is53, is60]
|
||||
|
||||
if sum(isBDS) == 0:
|
||||
return None
|
||||
elif sum(isBDS) == 1:
|
||||
return BDS[isBDS.index(True)]
|
||||
else:
|
||||
return [bds for (bds, i) in zip(BDS, isBDS) if i]
|
||||
@@ -1,37 +0,0 @@
|
||||
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)
|
||||
1
pyModeS/extra/__init__.py
Normal file
1
pyModeS/extra/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
178
pyModeS/extra/aero.py
Normal file
178
pyModeS/extra/aero.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Functions for aeronautics in this module
|
||||
- physical quantities always in SI units
|
||||
- lat,lon,course and heading in degrees
|
||||
|
||||
International Standard Atmosphere
|
||||
p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m]
|
||||
a = vsound(H) # speed of sound [m/s] as function of H[m]
|
||||
p = pressure(H) # calls atmos but retruns only pressure [Pa]
|
||||
T = temperature(H) # calculates temperature [K]
|
||||
rho = density(H) # calls atmos but retruns only pressure [Pa]
|
||||
|
||||
Speed conversion at altitude H[m] in ISA:
|
||||
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
|
||||
Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion
|
||||
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
|
||||
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
|
||||
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
|
||||
Vcas = tas2cas(Vtas,H) # Vtas to Vcas conversion both m/s, H in [m]
|
||||
Vcas = mach2cas(Mach,H) # Mach to Vcas conversion Vcas in m/s, H in [m]
|
||||
Mach = cas2mach(Vcas,H) # Vcas to mach copnversion Vcas in m/s, H in [m]
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
"""Aero and geo Constants """
|
||||
kts = 0.514444 # knot -> m/s
|
||||
ft = 0.3048 # ft -> m
|
||||
fpm = 0.00508 # ft/min -> m/s
|
||||
inch = 0.0254 # inch -> m
|
||||
sqft = 0.09290304 # 1 square foot
|
||||
nm = 1852. # nautical mile -> m
|
||||
lbs = 0.453592 # pound -> kg
|
||||
g0 = 9.80665 # m/s2, Sea level gravity constant
|
||||
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
|
||||
p0 = 101325. # Pa, air pressure, sea level ISA
|
||||
rho0 = 1.225 # kg/m3, air density, sea level ISA
|
||||
T0 = 288.15 # K, temperature, sea level ISA
|
||||
gamma = 1.40 # cp/cv for air
|
||||
gamma1 = 0.2 # (gamma-1)/2 for air
|
||||
gamma2 = 3.5 # gamma/(gamma-1) for air
|
||||
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
|
||||
r_earth = 6371000. # m, average earth radius
|
||||
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
|
||||
|
||||
|
||||
def atmos(H):
|
||||
# H in metres
|
||||
T = np.maximum(288.15 - 0.0065 * H, 216.65)
|
||||
rhotrop = 1.225 * (T / 288.15)**4.256848030018761
|
||||
dhstrat = np.maximum(0., H - 11000.0)
|
||||
rho = rhotrop * np.exp(-dhstrat / 6341.552161)
|
||||
p = rho * R * T
|
||||
return p, rho, T
|
||||
|
||||
|
||||
def temperature(H):
|
||||
p, r, T = atmos(H)
|
||||
return T
|
||||
|
||||
|
||||
def pressure(H):
|
||||
p, r, T = atmos(H)
|
||||
return p
|
||||
|
||||
|
||||
def density(H):
|
||||
p, r, T = atmos(H)
|
||||
return r
|
||||
|
||||
|
||||
def vsound(H):
|
||||
"""Speed of sound"""
|
||||
T = temperature(H)
|
||||
a = np.sqrt(gamma * R * T)
|
||||
return a
|
||||
|
||||
|
||||
def distance(lat1, lon1, lat2, lon2, H=0):
|
||||
"""
|
||||
Compute spherical distance from spherical coordinates.
|
||||
|
||||
For two locations in spherical coordinates
|
||||
(1, theta, phi) and (1, theta', phi')
|
||||
cosine( arc length ) =
|
||||
sin phi sin phi' cos(theta-theta') + cos phi cos phi'
|
||||
distance = rho * arc length
|
||||
"""
|
||||
|
||||
# phi = 90 - latitude
|
||||
phi1 = np.radians(90.0 - lat1)
|
||||
phi2 = np.radians(90.0 - lat2)
|
||||
|
||||
# theta = longitude
|
||||
theta1 = np.radians(lon1)
|
||||
theta2 = np.radians(lon2)
|
||||
|
||||
cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(phi2)
|
||||
cos = np.where(cos>1, 1, cos)
|
||||
|
||||
arc = np.arccos(cos)
|
||||
dist = arc * (r_earth + H) # meters, radius of earth
|
||||
return dist
|
||||
|
||||
|
||||
def bearing(lat1, lon1, lat2, lon2):
|
||||
lat1 = np.radians(lat1)
|
||||
lon1 = np.radians(lon1)
|
||||
lat2 = np.radians(lat2)
|
||||
lon2 = np.radians(lon2)
|
||||
x = np.sin(lon2-lon1) * np.cos(lat2)
|
||||
y = np.cos(lat1) * np.sin(lat2) \
|
||||
- np.sin(lat1) * np.cos(lat2) * np.cos(lon2-lon1)
|
||||
initial_bearing = np.arctan2(x, y)
|
||||
initial_bearing = np.degrees(initial_bearing)
|
||||
bearing = (initial_bearing + 360) % 360
|
||||
return bearing
|
||||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Speed conversions, altitude H all in meters
|
||||
# -----------------------------------------------------
|
||||
def tas2mach(Vtas, H):
|
||||
"""True Airspeed to Mach number"""
|
||||
a = vsound(H)
|
||||
Mach = Vtas/a
|
||||
return Mach
|
||||
|
||||
|
||||
def mach2tas(Mach, H):
|
||||
"""Mach number to True Airspeed"""
|
||||
a = vsound(H)
|
||||
Vtas = Mach*a
|
||||
return Vtas
|
||||
|
||||
|
||||
def eas2tas(Veas, H):
|
||||
"""Equivalent Airspeed to True Airspeed"""
|
||||
rho = density(H)
|
||||
Vtas = Veas * np.sqrt(rho0/rho)
|
||||
return Vtas
|
||||
|
||||
|
||||
def tas2eas(Vtas, H):
|
||||
"""True Airspeed to Equivalent Airspeed"""
|
||||
rho = density(H)
|
||||
Veas = Vtas * np.sqrt(rho/rho0)
|
||||
return Veas
|
||||
|
||||
|
||||
def cas2tas(Vcas, H):
|
||||
"""Calibrated Airspeed to True Airspeed"""
|
||||
p, rho, T = atmos(H)
|
||||
qdyn = p0*((1.+rho0*Vcas*Vcas/(7.*p0))**3.5-1.)
|
||||
Vtas = np.sqrt(7.*p/rho*((1.+qdyn/p)**(2./7.)-1.))
|
||||
return Vtas
|
||||
|
||||
|
||||
def tas2cas(Vtas, H):
|
||||
"""True Airspeed to Calibrated Airspeed"""
|
||||
p, rho, T = atmos(H)
|
||||
qdyn = p*((1.+rho*Vtas*Vtas/(7.*p))**3.5-1.)
|
||||
Vcas = np.sqrt(7.*p0/rho0*((qdyn/p0+1.)**(2./7.)-1.))
|
||||
return Vcas
|
||||
|
||||
|
||||
def mach2cas(Mach, H):
|
||||
"""Mach number to Calibrated Airspeed"""
|
||||
Vtas = mach2tas(Mach, H)
|
||||
Vcas = tas2cas(Vtas, H)
|
||||
return Vcas
|
||||
|
||||
|
||||
def cas2mach(Vcas, H):
|
||||
"""Calibrated Airspeed to Mach number"""
|
||||
Vtas = cas2tas(Vcas, H)
|
||||
Mach = tas2mach(Vtas, H)
|
||||
return Mach
|
||||
278
pyModeS/extra/tcpclient.py
Normal file
278
pyModeS/extra/tcpclient.py
Normal file
@@ -0,0 +1,278 @@
|
||||
'''
|
||||
Stream beast raw data from a TCP server, convert to mode-s messages
|
||||
'''
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
if (sys.version_info > (3, 0)):
|
||||
PY_VERSION = 3
|
||||
else:
|
||||
PY_VERSION = 2
|
||||
|
||||
class BaseClient(Thread):
|
||||
def __init__(self, host, port, rawtype):
|
||||
Thread.__init__(self)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.buffer = []
|
||||
self.rawtype = rawtype
|
||||
if self.rawtype not in ['avr', 'beast', 'skysense']:
|
||||
print("rawtype must be either avr, beast or skysense")
|
||||
os._exit(1)
|
||||
|
||||
def connect(self):
|
||||
while True:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(10) # 10 second timeout
|
||||
s.connect((self.host, self.port))
|
||||
print("Server connected - %s:%s" % (self.host, self.port))
|
||||
print("collecting ADS-B messages...")
|
||||
return s
|
||||
except socket.error as err:
|
||||
print("Socket connection error: %s. reconnecting..." % err)
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
def read_avr_buffer(self):
|
||||
# -- testing --
|
||||
# for b in self.buffer:
|
||||
# print(chr(b), b)
|
||||
|
||||
# Append message with 0-9,A-F,a-f, until stop sign
|
||||
|
||||
messages = []
|
||||
|
||||
msg_stop = False
|
||||
for b in self.buffer:
|
||||
if b == 59:
|
||||
msg_stop = True
|
||||
ts = time.time()
|
||||
messages.append([self.current_msg, ts])
|
||||
if b == 42:
|
||||
msg_stop = False
|
||||
self.current_msg = ''
|
||||
|
||||
if (not msg_stop) and (48<=b<=57 or 65<=b<=70 or 97<=b<=102):
|
||||
self.current_msg = self.current_msg + chr(b)
|
||||
|
||||
self.buffer = []
|
||||
|
||||
return messages
|
||||
|
||||
def read_beast_buffer(self):
|
||||
'''
|
||||
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
|
||||
2 byte Mode-AC
|
||||
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
|
||||
7 byte Mode-S short frame
|
||||
<esc> "3" : 6 byte MLAT timestamp, 1 byte signal level,
|
||||
14 byte Mode-S long frame
|
||||
<esc> "4" : 6 byte MLAT timestamp, status data, DIP switch
|
||||
configuration settings (not on Mode-S Beast classic)
|
||||
<esc><esc>: true 0x1a
|
||||
<esc> is 0x1a, and "1", "2" and "3" are 0x31, 0x32 and 0x33
|
||||
|
||||
timestamp:
|
||||
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
|
||||
'''
|
||||
|
||||
messages_mlat = []
|
||||
msg = []
|
||||
i = 0
|
||||
|
||||
# process the buffer until the last divider <esc> 0x1a
|
||||
# then, reset the self.buffer with the remainder
|
||||
|
||||
while i < len(self.buffer):
|
||||
if (self.buffer[i:i+2] == [0x1a, 0x1a]):
|
||||
msg.append(0x1a)
|
||||
i += 1
|
||||
elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1a):
|
||||
# special case where the last bit is 0x1a
|
||||
msg.append(0x1a)
|
||||
elif self.buffer[i] == 0x1a:
|
||||
if i == len(self.buffer) - 1:
|
||||
# special case where the last bit is 0x1a
|
||||
msg.append(0x1a)
|
||||
elif len(msg) > 0:
|
||||
messages_mlat.append(msg)
|
||||
msg = []
|
||||
else:
|
||||
msg.append(self.buffer[i])
|
||||
i += 1
|
||||
|
||||
# save the reminder for next reading cycle, if not empty
|
||||
if len(msg) > 0:
|
||||
reminder = []
|
||||
for i, m in enumerate(msg):
|
||||
if (m == 0x1a) and (i < len(msg)-1):
|
||||
# rewind 0x1a, except when it is at the last bit
|
||||
reminder.extend([m, m])
|
||||
else:
|
||||
reminder.append(m)
|
||||
self.buffer = [0x1a] + msg
|
||||
else:
|
||||
self.buffer = []
|
||||
|
||||
# extract messages
|
||||
messages = []
|
||||
for mm in messages_mlat:
|
||||
ts = time.time()
|
||||
|
||||
msgtype = mm[0]
|
||||
# print(''.join('%02X' % i for i in mm))
|
||||
|
||||
if msgtype == 0x32:
|
||||
# Mode-S Short Message, 7 byte, 14-len hexstr
|
||||
msg = ''.join('%02X' % i for i in mm[8:15])
|
||||
elif msgtype == 0x33:
|
||||
# Mode-S Long Message, 14 byte, 28-len hexstr
|
||||
msg = ''.join('%02X' % i for i in mm[8:22])
|
||||
else:
|
||||
# Other message tupe
|
||||
continue
|
||||
|
||||
if len(msg) not in [14, 28]:
|
||||
# incomplete message
|
||||
continue
|
||||
|
||||
messages.append([msg, ts])
|
||||
return messages
|
||||
|
||||
def read_skysense_buffer(self):
|
||||
"""
|
||||
----------------------------------------------------------------------------------
|
||||
Field SS MS MS MS MS MS MS MS MS MS MS MS MS MS MS TS TS TS TS TS TS RS RS RS
|
||||
----------------------------------------------------------------------------------
|
||||
Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
||||
----------------------------------------------------------------------------------
|
||||
|
||||
SS field - Start character
|
||||
Position 0:
|
||||
1 byte = 8 bits
|
||||
Start character '$'
|
||||
|
||||
MS field - Payload
|
||||
Postion 1 through 14:
|
||||
14 bytes = 112 bits
|
||||
Mode-S payload
|
||||
In case of DF types that only carry 7 bytes of information position 8 through 14 are set to 0x00.
|
||||
|
||||
TS field - Time stamp
|
||||
Position 15 through 20:
|
||||
6 bytes = 48 bits
|
||||
Time stamp with fields as:
|
||||
|
||||
Lock Status - Status of internal time keeping mechanism
|
||||
Equal to 1 if operating normally
|
||||
Bit 47 - 1 bit
|
||||
|
||||
Time of day in UTC seconds, between 0 and 86399
|
||||
Bits 46 through 30 - 17 bits
|
||||
|
||||
Nanoseconds into current second, between 0 and 999999999
|
||||
Bits 29 through 0 - 30 bits
|
||||
|
||||
RS field - Signal Level
|
||||
Position 21 through 23:
|
||||
3 bytes = 24 bits
|
||||
RSSI (received signal strength indication) and relative noise level with fields
|
||||
|
||||
RNL, Q12.4 unsigned fixed point binary with 4 fractional bits and 8 integer bits.
|
||||
This is and indication of the noise level of the message. Roughly 40 counts per 10dBm.
|
||||
Bits 23 through 12 - 12 bits
|
||||
|
||||
RSSI, Q12.4 unsigned fixed point binary with 4 fractional bits and 8 integer bits.
|
||||
This is an indication of the signal level of the received message in ADC counts. Roughly 40 counts per 10dBm.
|
||||
Bits 11 through 0 - 12 bits
|
||||
"""
|
||||
SS_MSGLENGTH = 24
|
||||
SS_STARTCHAR = 0x24
|
||||
|
||||
if len(self.buffer) <= SS_MSGLENGTH:
|
||||
return None
|
||||
|
||||
messages = []
|
||||
while len(self.buffer) > SS_MSGLENGTH:
|
||||
i = 0
|
||||
if self.buffer[i] == SS_STARTCHAR and self.buffer[i+SS_MSGLENGTH] == SS_STARTCHAR:
|
||||
i += 1
|
||||
if (self.buffer[i]>>7):
|
||||
#Long message
|
||||
payload = self.buffer[i:i+14]
|
||||
else:
|
||||
#Short message
|
||||
payload = self.buffer[i:i+7]
|
||||
msg = ''.join('%02X' % j for j in payload)
|
||||
i += 14 #Both message types use 14 bytes
|
||||
tsbin = self.buffer[i:i+6]
|
||||
sec = ( (tsbin[0] & 0x7f) << 10) | (tsbin[1] << 2 ) | (tsbin[2] >> 6)
|
||||
nano = ( (tsbin[2] & 0x3f) << 24) | (tsbin[3] << 16) | (tsbin[4] << 8) | tsbin[5]
|
||||
ts = sec + nano*1.0e-9
|
||||
i += 6
|
||||
#Signal and noise level - Don't care for now
|
||||
i += 3
|
||||
self.buffer = self.buffer[SS_MSGLENGTH:]
|
||||
messages.append( [msg,ts] )
|
||||
else:
|
||||
self.buffer = self.buffer[1:]
|
||||
return messages
|
||||
|
||||
def handle_messages(self, messages):
|
||||
"""re-implement this method to handle the messages"""
|
||||
for msg, t in messages:
|
||||
print("%15.9f %s" % (t, msg))
|
||||
|
||||
def run(self):
|
||||
sock = self.connect()
|
||||
|
||||
while True:
|
||||
try:
|
||||
received = sock.recv(1024)
|
||||
|
||||
if PY_VERSION == 2:
|
||||
received = [ord(i) for i in received]
|
||||
|
||||
self.buffer.extend(received)
|
||||
# print(''.join(x.encode('hex') for x in self.buffer))
|
||||
|
||||
# process self.buffer when it is longer enough
|
||||
# if len(self.buffer) < 2048:
|
||||
# continue
|
||||
# -- Removed!! Cause delay in low data rate scenario --
|
||||
|
||||
if self.rawtype == 'beast':
|
||||
messages = self.read_beast_buffer()
|
||||
elif self.rawtype == 'avr':
|
||||
messages = self.read_avr_buffer()
|
||||
elif self.rawtype == 'skysense':
|
||||
messages = self.read_skysense_buffer()
|
||||
|
||||
if not messages:
|
||||
continue
|
||||
else:
|
||||
self.handle_messages(messages)
|
||||
|
||||
time.sleep(0.001)
|
||||
except Exception as e:
|
||||
print("Unexpected Error:", e)
|
||||
|
||||
try:
|
||||
sock = self.connect()
|
||||
except Exception as e:
|
||||
print("Unexpected Error:", e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# for testing purpose only
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
rawtype = sys.argv[3]
|
||||
client = BaseClient(host=host, port=port, rawtype=rawtype)
|
||||
client.daemon = True
|
||||
client.run()
|
||||
@@ -1,131 +0,0 @@
|
||||
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
|
||||
0
pyModeS/streamer/__init__.py
Normal file
0
pyModeS/streamer/__init__.py
Normal file
120
pyModeS/streamer/modeslive
Executable file
120
pyModeS/streamer/modeslive
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import curses
|
||||
from threading import Lock
|
||||
import pyModeS as pms
|
||||
from pyModeS.extra.tcpclient import BaseClient
|
||||
from pyModeS.streamer.stream import Stream
|
||||
from pyModeS.streamer.screen import Screen
|
||||
|
||||
LOCK = Lock()
|
||||
ADSB_MSG = []
|
||||
ADSB_TS = []
|
||||
COMMB_MSG = []
|
||||
COMMB_TS = []
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--server', help='server address or IP', required=True)
|
||||
parser.add_argument('--port', help='raw data port', required=True)
|
||||
parser.add_argument('--rawtype', help='beast, avr or skysense', required=True)
|
||||
parser.add_argument('--latlon', help='receiver position', nargs=2, metavar=('LAT', 'LON'), required=True)
|
||||
parser.add_argument('--show-uncertainty', dest='uncertainty', help='display uncertaint values, default off', action='store_true', required=False, default=False)
|
||||
parser.add_argument('--dumpto', help='folder to dump decoded output', required=False, default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
SERVER = args.server
|
||||
PORT = int(args.port)
|
||||
RAWTYPE = args.rawtype
|
||||
LAT0 = float(args.latlon[0])
|
||||
LON0 = float(args.latlon[1])
|
||||
UNCERTAINTY = args.uncertainty
|
||||
DUMPTO = args.dumpto
|
||||
|
||||
if DUMPTO is not None:
|
||||
# append to current folder except root is given
|
||||
if DUMPTO[0] != '/':
|
||||
DUMPTO = os.getcwd() + '/' + DUMPTO
|
||||
|
||||
if not os.path.isdir(DUMPTO):
|
||||
print('Error: dump folder (%s) does not exist' % DUMPTO)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ModesClient(BaseClient):
|
||||
def __init__(self, host, port, rawtype):
|
||||
super(ModesClient, self).__init__(host, port, rawtype)
|
||||
|
||||
def handle_messages(self, messages):
|
||||
local_buffer_adsb_msg = []
|
||||
local_buffer_adsb_ts = []
|
||||
local_buffer_ehs_msg = []
|
||||
local_buffer_ehs_ts = []
|
||||
|
||||
for msg, t in messages:
|
||||
if len(msg) < 28: # only process long messages
|
||||
continue
|
||||
|
||||
df = pms.df(msg)
|
||||
|
||||
if df == 17 or df == 18:
|
||||
local_buffer_adsb_msg.append(msg)
|
||||
local_buffer_adsb_ts.append(t)
|
||||
elif df == 20 or df == 21:
|
||||
local_buffer_ehs_msg.append(msg)
|
||||
local_buffer_ehs_ts.append(t)
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
LOCK.acquire()
|
||||
ADSB_MSG.extend(local_buffer_adsb_msg)
|
||||
ADSB_TS.extend(local_buffer_adsb_ts)
|
||||
COMMB_MSG.extend(local_buffer_ehs_msg)
|
||||
COMMB_TS.extend(local_buffer_ehs_ts)
|
||||
LOCK.release()
|
||||
|
||||
|
||||
# redirect all stdout to null, avoiding messing up with the screen
|
||||
sys.stdout = open(os.devnull, 'w')
|
||||
|
||||
client = ModesClient(host=SERVER, port=PORT, rawtype=RAWTYPE)
|
||||
client.daemon = True
|
||||
client.start()
|
||||
|
||||
stream = Stream(lat0=LAT0, lon0=LON0, dumpto=DUMPTO)
|
||||
|
||||
try:
|
||||
screen = Screen(uncertainty=UNCERTAINTY)
|
||||
screen.daemon = True
|
||||
screen.start()
|
||||
|
||||
while True:
|
||||
if len(ADSB_MSG) > 200:
|
||||
LOCK.acquire()
|
||||
stream.process_raw(ADSB_TS, ADSB_MSG, COMMB_TS, COMMB_MSG)
|
||||
ADSB_MSG = []
|
||||
ADSB_TS = []
|
||||
COMMB_MSG = []
|
||||
COMMB_TS = []
|
||||
LOCK.release()
|
||||
|
||||
acs = stream.get_aircraft()
|
||||
try:
|
||||
screen.update_data(acs)
|
||||
screen.update()
|
||||
time.sleep(0.02)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
continue
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
finally:
|
||||
curses.endwin()
|
||||
183
pyModeS/streamer/screen.py
Normal file
183
pyModeS/streamer/screen.py
Normal file
@@ -0,0 +1,183 @@
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import curses
|
||||
import numpy as np
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
COLUMNS = [
|
||||
('call', 10),
|
||||
('lat', 10),
|
||||
('lon', 10),
|
||||
('alt', 7),
|
||||
('gs', 5),
|
||||
('tas', 5),
|
||||
('ias', 5),
|
||||
('mach', 7),
|
||||
('roc', 7),
|
||||
('trk', 10),
|
||||
('hdg', 10),
|
||||
('live', 6),
|
||||
]
|
||||
|
||||
UNCERTAINTY_COLUMNS = [
|
||||
('|', 5),
|
||||
('ver', 4),
|
||||
('HPL', 5),
|
||||
('RCu', 5),
|
||||
('RCv', 5),
|
||||
('HVE', 5),
|
||||
('VVE', 5),
|
||||
('Rc', 4),
|
||||
('VPL', 5),
|
||||
('EPU', 5),
|
||||
('VEPU', 6),
|
||||
('HFOMr', 7),
|
||||
('VFOMr', 7),
|
||||
('PE_RCu', 8),
|
||||
('PE_VPL', 8),
|
||||
]
|
||||
|
||||
class Screen(Thread):
|
||||
def __init__(self, uncertainty=False):
|
||||
Thread.__init__(self)
|
||||
self.screen = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.mousemask(1)
|
||||
self.screen.keypad(True)
|
||||
self.y = 3
|
||||
self.x = 1
|
||||
self.offset = 0
|
||||
self.acs = {}
|
||||
self.lock_icao = None
|
||||
|
||||
self.columns = COLUMNS
|
||||
if uncertainty:
|
||||
self.columns.extend(UNCERTAINTY_COLUMNS)
|
||||
|
||||
|
||||
def reset_cursor_pos(self):
|
||||
self.screen.move(self.y, self.x)
|
||||
|
||||
def update_data(self, acs):
|
||||
self.acs = acs
|
||||
|
||||
def draw_frame(self):
|
||||
self.screen.border(0)
|
||||
self.screen.addstr(0, 2, "Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)" % len(self.acs))
|
||||
|
||||
def update(self):
|
||||
if len(self.acs) == 0:
|
||||
return
|
||||
|
||||
resized = curses.is_term_resized(self.scr_h, self.scr_w)
|
||||
if resized is True:
|
||||
self.scr_h, self.scr_w = self.screen.getmaxyx()
|
||||
self.screen.clear()
|
||||
curses.resizeterm(self.scr_h, self.scr_w)
|
||||
|
||||
self.screen.refresh()
|
||||
self.draw_frame()
|
||||
|
||||
row = 1
|
||||
|
||||
header = ' icao'
|
||||
for c, cw in self.columns:
|
||||
header += (cw-len(c))*' ' + c
|
||||
|
||||
# fill end with spaces
|
||||
header += (self.scr_w - 2 - len(header)) * ' '
|
||||
|
||||
if len(header) > self.scr_w - 2:
|
||||
header = header[:self.scr_w-3] + '>'
|
||||
|
||||
|
||||
self.screen.addstr(row, 1, header)
|
||||
|
||||
row +=1
|
||||
self.screen.addstr(row, 1, '-'*(self.scr_w-2))
|
||||
|
||||
icaos = np.array(list(self.acs.keys()))
|
||||
icaos = np.sort(icaos)
|
||||
|
||||
for row in range(3, self.scr_h - 3):
|
||||
icao = None
|
||||
idx = row + self.offset - 3
|
||||
|
||||
if idx > len(icaos) - 1:
|
||||
line = ' '*(self.scr_w-2)
|
||||
|
||||
else:
|
||||
line = ''
|
||||
|
||||
icao = icaos[idx]
|
||||
ac = self.acs[icao]
|
||||
|
||||
line += icao
|
||||
|
||||
for c, cw in self.columns:
|
||||
if c=='|':
|
||||
val = '|'
|
||||
elif c=='live':
|
||||
val = str(int(time.time() - ac[c]))+'s'
|
||||
elif ac[c] is None:
|
||||
val = ''
|
||||
else:
|
||||
val = ac[c]
|
||||
val_str = str(val)
|
||||
line += (cw-len(val_str))*' ' + val_str
|
||||
|
||||
# fill end with spaces
|
||||
line += (self.scr_w - 2 - len(line)) * ' '
|
||||
|
||||
if len(line) > self.scr_w - 2:
|
||||
line = line[:self.scr_w-3] + '>'
|
||||
|
||||
if (icao is not None) and (self.lock_icao == icao):
|
||||
self.screen.addstr(row, 1, line, curses.A_STANDOUT)
|
||||
elif row == self.y:
|
||||
self.screen.addstr(row, 1, line, curses.A_BOLD)
|
||||
else:
|
||||
self.screen.addstr(row, 1, line)
|
||||
|
||||
self.screen.addstr(self.scr_h-3, 1, '-'*(self.scr_w-2))
|
||||
|
||||
total_page = len(icaos) // (self.scr_h - 4) + 1
|
||||
current_page = self.offset // (self.scr_h - 4) + 1
|
||||
self.screen.addstr(self.scr_h-2, 1, '(%d / %d)' % (current_page, total_page))
|
||||
|
||||
self.reset_cursor_pos()
|
||||
|
||||
def run(self):
|
||||
self.draw_frame()
|
||||
self.scr_h, self.scr_w = self.screen.getmaxyx()
|
||||
|
||||
while True:
|
||||
c = self.screen.getch()
|
||||
|
||||
if c == curses.KEY_HOME:
|
||||
self.x = 1
|
||||
self.y = 1
|
||||
elif c == curses.KEY_NPAGE:
|
||||
offset_intent = self.offset + (self.scr_h - 4)
|
||||
if offset_intent < len(self.acs) - 5:
|
||||
self.offset = offset_intent
|
||||
elif c == curses.KEY_PPAGE:
|
||||
offset_intent = self.offset - (self.scr_h - 4)
|
||||
if offset_intent > 0:
|
||||
self.offset = offset_intent
|
||||
else:
|
||||
self.offset = 0
|
||||
elif c == curses.KEY_DOWN :
|
||||
y_intent = self.y + 1
|
||||
if y_intent < self.scr_h - 3:
|
||||
self.y = y_intent
|
||||
elif c == curses.KEY_UP:
|
||||
y_intent = self.y - 1
|
||||
if y_intent > 2:
|
||||
self.y = y_intent
|
||||
elif c == curses.KEY_ENTER or c == 10 or c == 13:
|
||||
self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode()
|
||||
elif c == curses.KEY_F5:
|
||||
self.screen.refresh()
|
||||
self.draw_frame()
|
||||
255
pyModeS/streamer/stream.py
Normal file
255
pyModeS/streamer/stream.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
import csv
|
||||
import pyModeS as pms
|
||||
|
||||
class Stream():
|
||||
def __init__(self, lat0, lon0, dumpto=None):
|
||||
|
||||
self.acs = dict()
|
||||
|
||||
self.lat0 = lat0
|
||||
self.lon0 = lon0
|
||||
|
||||
self.t = 0
|
||||
self.cache_timeout = 60 # seconds
|
||||
|
||||
|
||||
if dumpto is not None and os.path.isdir(dumpto):
|
||||
self.dumpto = dumpto
|
||||
else:
|
||||
self.dumpto = None
|
||||
|
||||
|
||||
def process_raw(self, adsb_ts, adsb_msgs, commb_ts, commb_msgs, tnow=None):
|
||||
"""process a chunk of adsb and commb messages recieved in the same
|
||||
time period.
|
||||
"""
|
||||
if tnow is None:
|
||||
tnow = time.time()
|
||||
|
||||
self.t = tnow
|
||||
|
||||
local_updated_acs_buffer = []
|
||||
output_buffer = []
|
||||
|
||||
# process adsb message
|
||||
for t, msg in zip(adsb_ts, adsb_msgs):
|
||||
icao = pms.icao(msg)
|
||||
tc = pms.adsb.typecode(msg)
|
||||
|
||||
if icao not in self.acs:
|
||||
self.acs[icao] = {
|
||||
'live': None,
|
||||
'call': None,
|
||||
'lat': None,
|
||||
'lon': None,
|
||||
'alt': None,
|
||||
'gs': None,
|
||||
'trk': None,
|
||||
'roc': None,
|
||||
'tas': None,
|
||||
'roll': None,
|
||||
'rtrk': None,
|
||||
'ias': None,
|
||||
'mach': None,
|
||||
'hdg': None,
|
||||
'ver' : None,
|
||||
'HPL' : None,
|
||||
'RCu' : None,
|
||||
'RCv' : None,
|
||||
'HVE' : None,
|
||||
'VVE' : None,
|
||||
'Rc' : None,
|
||||
'VPL' : None,
|
||||
'EPU' : None,
|
||||
'VEPU' : None,
|
||||
'HFOMr' : None,
|
||||
'VFOMr' : None,
|
||||
'PE_RCu' : None,
|
||||
'PE_VPL' : None,
|
||||
}
|
||||
|
||||
self.acs[icao]['t'] = t
|
||||
self.acs[icao]['live'] = int(t)
|
||||
|
||||
if 1 <= tc <= 4:
|
||||
cs = pms.adsb.callsign(msg)
|
||||
self.acs[icao]['call'] = cs
|
||||
output_buffer.append([t, icao, 'cs', cs])
|
||||
|
||||
if (5 <= tc <= 8) or (tc == 19):
|
||||
vdata = pms.adsb.velocity(msg)
|
||||
if vdata is None:
|
||||
continue
|
||||
|
||||
spd, trk, roc, tag = vdata
|
||||
if tag != 'GS':
|
||||
continue
|
||||
if (spd is None) or (trk is None):
|
||||
continue
|
||||
|
||||
self.acs[icao]['gs'] = spd
|
||||
self.acs[icao]['trk'] = trk
|
||||
self.acs[icao]['roc'] = roc
|
||||
self.acs[icao]['tv'] = t
|
||||
|
||||
output_buffer.append([t, icao, 'gs', spd])
|
||||
output_buffer.append([t, icao, 'trk', trk])
|
||||
output_buffer.append([t, icao, 'roc', roc])
|
||||
|
||||
|
||||
if (5 <= tc <= 18):
|
||||
oe = pms.adsb.oe_flag(msg)
|
||||
self.acs[icao][oe] = msg
|
||||
self.acs[icao]['t'+str(oe)] = t
|
||||
|
||||
if ('tpos' in self.acs[icao]) and (t - self.acs[icao]['tpos'] < 180):
|
||||
# use single message decoding
|
||||
rlat = self.acs[icao]['lat']
|
||||
rlon = self.acs[icao]['lon']
|
||||
latlon = pms.adsb.position_with_ref(msg, rlat, rlon)
|
||||
elif ('t0' in self.acs[icao]) and ('t1' in self.acs[icao]) and \
|
||||
(abs(self.acs[icao]['t0'] - self.acs[icao]['t1']) < 10):
|
||||
# use multi message decoding
|
||||
try:
|
||||
latlon = pms.adsb.position(
|
||||
self.acs[icao][0],
|
||||
self.acs[icao][1],
|
||||
self.acs[icao]['t0'],
|
||||
self.acs[icao]['t1'],
|
||||
self.lat0, self.lon0
|
||||
)
|
||||
except:
|
||||
# mix of surface and airborne position message
|
||||
continue
|
||||
else:
|
||||
latlon = None
|
||||
|
||||
if latlon is not None:
|
||||
self.acs[icao]['tpos'] = t
|
||||
self.acs[icao]['lat'] = latlon[0]
|
||||
self.acs[icao]['lon'] = latlon[1]
|
||||
|
||||
alt = pms.adsb.altitude(msg)
|
||||
self.acs[icao]['alt'] = alt
|
||||
|
||||
output_buffer.append([t, icao, 'lat', latlon[0]])
|
||||
output_buffer.append([t, icao, 'lon', latlon[1]])
|
||||
output_buffer.append([t, icao, 'alt', alt])
|
||||
|
||||
local_updated_acs_buffer.append(icao)
|
||||
|
||||
# Uncertainty & accuracy
|
||||
ac = self.acs[icao]
|
||||
|
||||
if 9 <= tc <= 18:
|
||||
ac['nic_bc'] = pms.adsb.nic_b(msg)
|
||||
|
||||
if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22):
|
||||
ac['HPL'], ac['RCu'], ac['RCv'] = pms.adsb.nuc_p(msg)
|
||||
|
||||
if (ac['ver'] == 1) and ('nic_s' in ac.keys()):
|
||||
ac['Rc'], ac['VPL'] = pms.adsb.nic_v1(msg, ac['nic_s'])
|
||||
elif (ac['ver'] == 2) and ('nic_a' in ac.keys()) and ('nic_bc' in ac.keys()):
|
||||
ac['Rc'] = pms.adsb.nic_v2(msg, ac['nic_a'], ac['nic_bc'])
|
||||
|
||||
if tc == 19:
|
||||
ac['HVE'], ac['VVE'] = pms.adsb.nuc_v(msg)
|
||||
if ac['ver'] in [1, 2]:
|
||||
ac['HFOMr'], ac['VFOMr'] = pms.adsb.nac_v(msg)
|
||||
|
||||
if tc == 29:
|
||||
ac['PE_RCu'], ac['PE_VPL'], ac['base'] = pms.adsb.sil(msg, ac['ver'])
|
||||
ac['EPU'], ac['VEPU'] = pms.adsb.nac_p(msg)
|
||||
|
||||
if tc == 31:
|
||||
ac['ver'] = pms.adsb.version(msg)
|
||||
ac['EPU'], ac['VEPU'] = pms.adsb.nac_p(msg)
|
||||
ac['PE_RCu'], ac['PE_VPL'], ac['sil_base'] = pms.adsb.sil(msg, ac['ver'])
|
||||
|
||||
if ac['ver'] == 1:
|
||||
ac['nic_s'] = pms.adsb.nic_s(msg)
|
||||
elif ac['ver'] == 2:
|
||||
ac['nic_a'], ac['nic_bc'] = pms.adsb.nic_a_c(msg)
|
||||
|
||||
|
||||
# process commb message
|
||||
for t, msg in zip(commb_ts, commb_msgs):
|
||||
icao = pms.icao(msg)
|
||||
|
||||
if icao not in self.acs:
|
||||
continue
|
||||
|
||||
bds = pms.bds.infer(msg)
|
||||
|
||||
if bds == 'BDS50':
|
||||
roll50 = pms.commb.roll50(msg)
|
||||
trk50 = pms.commb.trk50(msg)
|
||||
rtrk50 = pms.commb.rtrk50(msg)
|
||||
gs50 = pms.commb.gs50(msg)
|
||||
tas50 = pms.commb.tas50(msg)
|
||||
|
||||
self.acs[icao]['t50'] = t
|
||||
if tas50:
|
||||
self.acs[icao]['tas'] = tas50
|
||||
output_buffer.append([t, icao, 'tas50', tas50])
|
||||
if roll50:
|
||||
self.acs[icao]['roll'] = roll50
|
||||
output_buffer.append([t, icao, 'roll50', roll50])
|
||||
if rtrk50:
|
||||
self.acs[icao]['rtrk'] = rtrk50
|
||||
output_buffer.append([t, icao, 'rtrk50', rtrk50])
|
||||
|
||||
if trk50:
|
||||
output_buffer.append([t, icao, 'trk50', trk50])
|
||||
if gs50:
|
||||
output_buffer.append([t, icao, 'gs50', gs50])
|
||||
|
||||
elif bds == 'BDS60':
|
||||
ias60 = pms.commb.ias60(msg)
|
||||
hdg60 = pms.commb.hdg60(msg)
|
||||
mach60 = pms.commb.mach60(msg)
|
||||
roc60baro = pms.commb.vr60baro(msg)
|
||||
roc60ins = pms.commb.vr60ins(msg)
|
||||
|
||||
if ias60 or hdg60 or mach60:
|
||||
self.acs[icao]['t60'] = t
|
||||
if ias60:
|
||||
self.acs[icao]['ias'] = ias60
|
||||
if hdg60:
|
||||
self.acs[icao]['hdg'] = hdg60
|
||||
if mach60:
|
||||
self.acs[icao]['mach'] = mach60
|
||||
|
||||
if roc60baro:
|
||||
output_buffer.append([t, icao, 'roc60baro', roc60baro])
|
||||
if roc60ins:
|
||||
output_buffer.append([t, icao, 'roc60ins', roc60ins])
|
||||
|
||||
# clear up old data
|
||||
for icao in list(self.acs.keys()):
|
||||
if self.t - self.acs[icao]['live'] > self.cache_timeout:
|
||||
del self.acs[icao]
|
||||
continue
|
||||
|
||||
if self.dumpto is not None:
|
||||
dh = str(datetime.datetime.now().strftime("%Y%m%d_%H"))
|
||||
fn = self.dumpto + '/pymodes_dump_%s.csv' % dh
|
||||
output_buffer.sort(key=lambda x: x[0])
|
||||
with open(fn, "a") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerows(output_buffer)
|
||||
|
||||
return
|
||||
|
||||
def get_aircraft(self):
|
||||
"""all aircraft that are stored in memeory"""
|
||||
acs = self.acs
|
||||
icaos = list(acs.keys())
|
||||
for icao in icaos:
|
||||
if acs[icao]['lat'] is None:
|
||||
acs.pop(icao)
|
||||
return acs
|
||||
@@ -1,97 +0,0 @@
|
||||
# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
Common functions for ADS-B and Mode-S EHS decoder
|
||||
"""
|
||||
|
||||
|
||||
import math
|
||||
|
||||
# the polynominal generattor code for CRC
|
||||
GENERATOR = "1111111111111010000001001"
|
||||
|
||||
|
||||
def hex2bin(hexstr):
|
||||
"""Convert a hexdecimal string to binary string, with zero fillings. """
|
||||
scale = 16
|
||||
num_of_bits = len(hexstr) * math.log(scale, 2)
|
||||
binstr = bin(int(hexstr, scale))[2:].zfill(int(num_of_bits))
|
||||
return binstr
|
||||
|
||||
|
||||
def bin2int(binstr):
|
||||
"""Convert a binary string to integer. """
|
||||
return int(binstr, 2)
|
||||
|
||||
|
||||
def hex2int(hexstr):
|
||||
"""Convert a hexdecimal string to integer. """
|
||||
return int(hexstr, 16)
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Decode Downlink Format vaule, bits 1 to 5."""
|
||||
msgbin = hex2bin(msg)
|
||||
return bin2int(msgbin[0:5])
|
||||
|
||||
|
||||
def crc(msg, encode=False):
|
||||
"""Mode-S Cyclic Redundancy Check
|
||||
Detect if bit error occurs in the Mode-S message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
encode (bool): True to encode the date only and return the checksum
|
||||
Returns:
|
||||
string: message checksum, or partity bits (encoder)
|
||||
"""
|
||||
|
||||
msgbin = list(hex2bin(msg))
|
||||
|
||||
if encode:
|
||||
msgbin[-24:] = ['0'] * 24
|
||||
|
||||
# loop all bits, except last 24 piraty bits
|
||||
for i in range(len(msgbin)-24):
|
||||
# if 1, perform modulo 2 multiplication,
|
||||
if msgbin[i] == '1':
|
||||
for j in range(len(GENERATOR)):
|
||||
# modulo 2 multiplication = XOR
|
||||
msgbin[i+j] = str((int(msgbin[i+j]) ^ int(GENERATOR[j])))
|
||||
|
||||
# last 24 bits
|
||||
reminder = ''.join(msgbin[-24:])
|
||||
return reminder
|
||||
|
||||
|
||||
def floor(x):
|
||||
""" Mode-S floor function
|
||||
|
||||
Defined as the greatest integer value k, such that k <= 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
|
||||
10
setup.py
10
setup.py
@@ -30,9 +30,9 @@ 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.2.2',
|
||||
version='2.0',
|
||||
|
||||
description='Python Mode-S Decoder',
|
||||
description='Python ADS-B/Mode-S Decoder',
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
@@ -63,12 +63,12 @@ setup(
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
|
||||
# What does your project relate to?
|
||||
@@ -86,7 +86,7 @@ setup(
|
||||
# your project is installed. For an analysis of "install_requires" vs pip's
|
||||
# requirements files see:
|
||||
# https://packaging.python.org/en/latest/requirements.html
|
||||
install_requires=[''],
|
||||
install_requires=['numpy', 'argparse'],
|
||||
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
# dependencies). You can install these using the following syntax,
|
||||
@@ -118,4 +118,6 @@ setup(
|
||||
# 'sample=sample:main',
|
||||
# ],
|
||||
# },
|
||||
|
||||
scripts=['pyModeS/streamer/modeslive'],
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import adsb, ehs, util
|
||||
from pyModeS import adsb, ehs
|
||||
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
75
tests/sample_run_commb.py
Normal file
75
tests/sample_run_commb.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import commb, common, bds
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
def bds_info(BDS, m):
|
||||
if BDS == "BDS10":
|
||||
info = [commb.ovc10(m)]
|
||||
|
||||
elif BDS == "BDS17":
|
||||
info = ([i[-2:] for i in commb.cap17(m)])
|
||||
|
||||
elif BDS == "BDS20":
|
||||
info = [commb.cs20(m)]
|
||||
|
||||
elif BDS == "BDS40":
|
||||
info = (commb.alt40mcp(m), commb.alt40fms(m), commb.p40baro(m))
|
||||
|
||||
elif BDS == "BDS44":
|
||||
info = (commb.wind44(m), commb.temp44(m), commb.p44(m), commb.hum44(m))
|
||||
|
||||
elif BDS == "BDS44REV":
|
||||
info = (commb.wind44(m, rev=True), commb.temp44(m, rev=True), commb.p44(m, rev=True), commb.hum44(m, rev=True))
|
||||
|
||||
elif BDS == "BDS50":
|
||||
info = (commb.roll50(m), commb.trk50(m), commb.gs50(m), commb.rtrk50(m), commb.tas50(m))
|
||||
|
||||
elif BDS == "BDS60":
|
||||
info = (commb.hdg60(m), commb.ias60(m), commb.mach60(m), commb.vr60baro(m), commb.vr60ins(m))
|
||||
|
||||
else:
|
||||
info = None
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def commb_decode_all(df, n=None):
|
||||
import csv
|
||||
|
||||
print("===== Decode Comm-B sample data (DF=%s)=====" % df)
|
||||
|
||||
f = open('tests/data/sample_data_commb_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 = common.df(m)
|
||||
icao = common.icao(m)
|
||||
BDS = bds.infer(m)
|
||||
code = common.altcode(m) if df == 20 else common.idcode(m)
|
||||
|
||||
if not BDS:
|
||||
print(ts, m, icao, df, '%5s'%code, 'UNKNOWN')
|
||||
continue
|
||||
|
||||
if len(BDS.split(",")) > 1:
|
||||
print(ts, m, icao, df, '%5s' % code, end=' ')
|
||||
for i, _bds in enumerate(BDS.split(",")):
|
||||
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__':
|
||||
commb_decode_all(df=20, n=100)
|
||||
commb_decode_all(df=21, n=100)
|
||||
@@ -1,75 +0,0 @@
|
||||
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)
|
||||
@@ -58,22 +58,22 @@ 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 == (159, 182.88, -832, 'GS')
|
||||
assert vas == (375, 243.98, -2304, 'TAS')
|
||||
assert vgs_surface == (19.0, 42.2, 0 , 'GS')
|
||||
assert adsb.altitude_diff('8D485020994409940838175B284F') == 550
|
||||
|
||||
|
||||
def test_nic():
|
||||
assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
|
||||
assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
|
||||
assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
|
||||
assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
|
||||
assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
|
||||
assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
|
||||
assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
|
||||
assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
|
||||
assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
|
||||
assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
|
||||
assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
|
||||
assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11
|
||||
# def test_nic():
|
||||
# assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
|
||||
# assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
|
||||
# assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
|
||||
# assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
|
||||
# assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
|
||||
# assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
|
||||
# assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
|
||||
# assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
|
||||
# assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
|
||||
# assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
|
||||
# assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
|
||||
# assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11
|
||||
|
||||
20
tests/test_bds_inference.py
Normal file
20
tests/test_bds_inference.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from pyModeS import bds
|
||||
|
||||
def test_bds_infer():
|
||||
assert bds.infer("8D406B902015A678D4D220AA4BDA") == 'BDS08'
|
||||
assert bds.infer("8FC8200A3AB8F5F893096B000000") == 'BDS06'
|
||||
assert bds.infer("8D40058B58C901375147EFD09357") == 'BDS05'
|
||||
assert bds.infer("8D485020994409940838175B284F") == 'BDS09'
|
||||
|
||||
assert bds.infer("A800178D10010080F50000D5893C") == 'BDS10'
|
||||
assert bds.infer("A0000638FA81C10000000081A92F") == 'BDS17'
|
||||
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'
|
||||
49
tests/test_commb.py
Normal file
49
tests/test_commb.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from pyModeS import bds, commb
|
||||
# from pyModeS import ehs, els # deprecated
|
||||
|
||||
def test_bds20_callsign():
|
||||
assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
|
||||
assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
|
||||
|
||||
assert commb.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
|
||||
assert commb.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
|
||||
|
||||
|
||||
def test_bds40_functions():
|
||||
assert bds.bds40.alt40mcp("A000029C85E42F313000007047D3") == 3008
|
||||
assert bds.bds40.alt40fms("A000029C85E42F313000007047D3") == 3008
|
||||
assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0
|
||||
|
||||
assert commb.alt40mcp("A000029C85E42F313000007047D3") == 3008
|
||||
assert commb.alt40fms("A000029C85E42F313000007047D3") == 3008
|
||||
assert commb.p40baro("A000029C85E42F313000007047D3") == 1020.0
|
||||
|
||||
|
||||
def test_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
|
||||
|
||||
assert commb.roll50("A000139381951536E024D4CCF6B5") == 2.1
|
||||
assert commb.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
|
||||
assert commb.trk50("A000139381951536E024D4CCF6B5") == 114.258
|
||||
assert commb.gs50("A000139381951536E024D4CCF6B5") == 438
|
||||
assert commb.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
|
||||
assert commb.tas50("A000139381951536E024D4CCF6B5") == 424
|
||||
|
||||
|
||||
def test_bds60_functions():
|
||||
assert bds.bds60.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
|
||||
assert bds.bds60.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
||||
assert bds.bds60.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
||||
assert bds.bds60.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
assert bds.bds60.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
|
||||
assert commb.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
|
||||
assert commb.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
||||
assert commb.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
||||
assert commb.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
assert commb.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
42
tests/test_common.py
Normal file
42
tests/test_common.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from pyModeS import common
|
||||
|
||||
|
||||
def test_hex2bin():
|
||||
assert common.hex2bin('6E406B') == "011011100100000001101011"
|
||||
|
||||
def test_crc_decode():
|
||||
checksum = common.crc("8D406B902015A678D4D220AA4BDA")
|
||||
assert checksum == "000000000000000000000000"
|
||||
|
||||
def test_crc_encode():
|
||||
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
|
||||
assert common.hex2bin("AA4BDA") == parity
|
||||
|
||||
def test_icao():
|
||||
assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
|
||||
assert common.icao("A0001839CA3800315800007448D9") == '400940'
|
||||
assert common.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
|
||||
assert common.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
|
||||
|
||||
def test_modes_altcode():
|
||||
assert common.altcode("A02014B400000000000000F9D514") == 32300
|
||||
|
||||
def test_modes_idcode():
|
||||
assert common.idcode("A800292DFFBBA9383FFCEB903D01") == '1346'
|
||||
|
||||
def test_graycode_to_altitude():
|
||||
assert common.gray2alt('00000000010') == -1000
|
||||
assert common.gray2alt('00000001010') == -500
|
||||
assert common.gray2alt('00000011011') == -100
|
||||
assert common.gray2alt('00000011010') == 0
|
||||
assert common.gray2alt('00000011110') == 100
|
||||
assert common.gray2alt('00000010011') == 600
|
||||
assert common.gray2alt('00000110010') == 1000
|
||||
assert common.gray2alt('00001001001') == 5800
|
||||
assert common.gray2alt('00011100100') == 10300
|
||||
assert common.gray2alt('01100011010') == 32000
|
||||
assert common.gray2alt('01110000100') == 46300
|
||||
assert common.gray2alt('01010101100') == 50200
|
||||
assert common.gray2alt('11011110100') == 73200
|
||||
assert common.gray2alt('10000000011') == 126600
|
||||
assert common.gray2alt('10000000001') == 126700
|
||||
@@ -1,62 +0,0 @@
|
||||
from pyModeS import ehs
|
||||
from pyModeS import modes_common
|
||||
|
||||
def test_ehs_icao():
|
||||
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
|
||||
assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
|
||||
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("A00004128F39F91A7E27C46ADC21") == '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_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
|
||||
@@ -1,15 +0,0 @@
|
||||
from pyModeS import util
|
||||
|
||||
|
||||
def test_hex2bin():
|
||||
assert util.hex2bin('6E406B') == "011011100100000001101011"
|
||||
|
||||
|
||||
def test_crc_decode():
|
||||
checksum = util.crc("8D406B902015A678D4D220AA4BDA")
|
||||
assert checksum == "000000000000000000000000"
|
||||
|
||||
|
||||
def test_crc_encode():
|
||||
parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True)
|
||||
assert util.hex2bin("AA4BDA") == parity
|
||||
Reference in New Issue
Block a user