58 Commits

Author SHA1 Message Date
Junzi Sun
59d21de725 Merge branch 'master' into adsb_encoder 2020-05-04 01:53:27 +02:00
Junzi Sun
560b737739 update api link 2020-05-04 01:51:33 +02:00
Junzi Sun
d50f05d00d Merge branch 'master' of github.com:junzis/pyModeS 2020-05-04 01:09:40 +02:00
Junzi Sun
11a560dbb5 update API doc 2020-05-04 01:07:02 +02:00
Junzi Sun
b51facbdc6 update docstring 2020-05-04 00:56:36 +02:00
Junzi Sun
d77d1f7f6b update docstring 2020-05-04 00:11:29 +02:00
Junzi Sun
aff0f75de2 BDS08 and BDS09 encoders 2020-05-03 23:29:10 +02:00
Junzi Sun
5286355bf6 move common package to root level 2020-05-03 23:27:17 +02:00
Junzi Sun
fe9e033a64 update comments 2020-05-03 20:13:22 +02:00
Junzi Sun
57307109e6 remove python 2 imports 2020-05-02 19:32:54 +02:00
Junzi Sun
6571fe6fc0 Update README.rst 2020-04-04 20:48:23 +02:00
Junzi Sun
aa64d4e7a9 update README 2020-04-04 20:44:46 +02:00
wrobell
db57d7419f Improve doc for surface position decoding 2020-03-11 22:08:04 +01:00
Junzi Sun
ed18352c0c update NL() function in CPR decoding 2020-03-03 12:39:39 +01:00
Junzi Sun
a75d6fd050 install c module 2020-02-26 00:55:25 +01:00
Junzi Sun
0ff628bb8e update imports 2020-02-26 00:54:49 +01:00
Junzi Sun
ea7653ef79 update test 2020-02-26 00:25:11 +01:00
Junzi Sun
2046b1de07 speedup (#59)
* remove unused functions

* cythonize common

* add bds05

* separate cleanly cython and python, bds05, bds06, modulo issues

* bds08

* bds09

* optimisations in bds09

* "make" things easier

* clean up useless stuff

* add make options

* fix hidden altitude() call

* minor updates to C code

* update tests

* update benchmark

* consolidation

* update clean script

* reduce complexity and change default type to str

Co-authored-by: Xavier Olive <1360812+xoolive@users.noreply.github.com>
2020-02-26 00:16:48 +01:00
Junzi Sun
768b80df8e improve modeslive 2020-02-25 21:30:42 +01:00
Junzi Sun
e52d43f963 update rtlsdr reader 2020-02-14 17:53:56 +01:00
Junzi Sun
b60b31d4fb release 2.5 2020-02-14 10:47:34 +01:00
Flyer350
cb67c63326 Create uplink.py (#55)
First basic functionality for uplink messages - UF and ICAO address.
2019-12-31 11:16:55 +01:00
Xavier Olive
bddaf9aec6 compatibility with bytes (#57) 2019-12-31 11:15:25 +01:00
Junzi Sun
00fc1475ff Merge pull request #53 from dforsi/fix-typos
Fix typos
2019-11-19 11:17:41 +01:00
Daniele Forsi
489c405de0 Fix typos
Fixed with the command:
  codespell --write-changes --ignore-words-list hve,vas
codespell is available at https://github.com/lucasdemarchi/codespell
2019-10-31 13:38:46 +01:00
Junzi Sun
ec2721cfdc Merge pull request #52 from Flyer350/patch-1
Update README.rst
2019-10-30 20:42:19 +01:00
Flyer350
2f13524a7c Update README.rst
fixed typecodes for position messages
2019-10-30 15:54:37 +01:00
Junzi Sun
fb24d4f25c minor speed improvements 2019-10-16 16:24:38 +02:00
Junzi Sun
9ad535dc93 update package info 2019-09-30 23:50:07 +02:00
Junzi Sun
9a89766766 update installation guide 2019-09-24 21:36:48 +02:00
Junzi Sun
26173b4038 release version 2.4 - fix import errors 2019-09-24 21:26:35 +02:00
Junzi Sun
3619d52760 release version 2.3 2019-09-24 16:34:37 +02:00
Junzi Sun
e2ece806c2 update tell() 2019-09-24 16:30:38 +02:00
Junzi Sun
a683e40c41 deprecation warning only one time 2019-09-11 16:01:55 +02:00
Junzi Sun
0c1a3b06e1 Merge branch 'master' of github.com:junzis/pyModeS 2019-09-11 15:39:00 +02:00
Junzi Sun
f960cd71bc move dependencies to extras 2019-09-11 15:36:03 +02:00
Junzi Sun
bd54ac1d10 add tell() function 2019-09-11 14:58:31 +02:00
Junzi Sun
2c1db13122 reformat code with Black 2019-09-10 23:25:21 +02:00
Junzi Sun
695fc34988 update airborne_position(), allow swapping the odd and even messages 2019-09-10 14:47:15 +02:00
Junzi Sun
0eb333ba8c deal with Python 2 long int 2019-09-10 13:31:27 +02:00
Junzi Sun
d058e9f8b3 Merge pull request #49 from espinielli/patch-1
moved pms.adsb.surface_velocity under typecode 5-8
2019-09-06 23:01:47 +02:00
Enrico Spinielli
3d99deb049 moved pms.adsb.surface_velocity under typecode 5-8
I tried to `pms.adsb.surface_velocity()` as per README with a typecode 19 msg and got an error:

```python
>>> import pyModeS as pms
>>> msg = '8d484966990076b9e0762d5ff92c'
>>> pms.adsb.typecode(msg)
19
>>> pms.adsb.surface_velocity(msg)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\spi\AppData\Roaming\Python\Python27\site-packages\pyModeS\decoder\bds\bds06.py", line 162, in surface_velocity
    raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
RuntimeError: 8d484966990076b9e0762d5ff92c: Not a surface message, expecting 5<TC<8
```

so I propose this fix.
I used pyModeS      2.2
2019-09-05 16:56:42 +02:00
Junzi Sun
03f81d120b Merge pull request #48 from richardjanssen/master
Fix surface positions with negative longitude
2019-09-03 22:14:41 +02:00
Junzi Sun
b9b95320d8 keep only common virtualenv folder names 2019-09-03 22:12:30 +02:00
Richard Janssen
0ea2cf7ade Fix surface positions for negative longitude 2019-09-03 12:15:34 +02:00
Junzi Sun
458b02028d update readme 2019-08-26 16:50:05 +02:00
Junzi Sun
b062bdf998 fix local buffer bug 2019-08-26 16:38:36 +02:00
Junzi Sun
cfcd21b692 improve multiprocessing efficiency 2019-08-26 12:00:14 +02:00
Junzi Sun
86f302f05e change modeslive to multiprocessing 2019-08-26 00:21:29 +02:00
Junzi Sun
28a6e53d49 update dependencies 2019-08-23 14:59:50 +02:00
Junzi Sun
81d7cef6e8 Merge pull request #47 from junzis/rtlsdr
Add RTL-SDR support
2019-08-23 14:47:50 +02:00
Junzi Sun
4906a49e9c update modeslive command 2019-08-23 14:46:13 +02:00
Junzi Sun
7cb75ea8ca Update README.rst 2019-08-22 19:47:20 +02:00
Junzi Sun
785584aff5 improve singal process efficiency 2019-08-22 16:59:12 +02:00
Junzi Sun
fdc34497c0 update instruction for rtl-sdr 2019-08-22 10:51:11 +02:00
Junzi Sun
4a3c9438f7 adding support for rtl-sdr tunner 2019-08-22 10:45:12 +02:00
Junzi Sun
b723374337 Merge pull request #45 from hegrin-cz/master
Corrected ADS-B emitter category parsing.
2019-08-22 10:37:58 +02:00
Hegr,Jiri
8f4dff5b30 Corrected category parsing. 2019-08-19 14:50:09 +02:00
100 changed files with 3364 additions and 1995 deletions

9
.gitignore vendored
View File

@@ -5,6 +5,9 @@ __pycache__/
*.py[cod] *.py[cod]
.pytest_cache/ .pytest_cache/
#cython
*.c
# C extensions # C extensions
*.so *.so
@@ -58,3 +61,9 @@ target/
# PyCharm # PyCharm
.idea/ .idea/
# Environments
.env
.venv
env/
venv/

19
Makefile Normal file
View File

@@ -0,0 +1,19 @@
install:
pip install . --upgrade
uninstall:
pip uninstall pyModeS -y
ext:
python setup.py build_ext --inplace
test:
python -m pytest tests
clean:
find pyModeS/decoder -type f -name '*.c' -delete
find pyModeS/decoder -type f -name '*.so' -delete
find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf
rm -rf *.egg-info
rm -rf .pytest_cache
rm -rf build/*

View File

@@ -1,9 +1,43 @@
The Python ADS-B/Mode-S Decoder The Python ADS-B/Mode-S Decoder
=============================== ===============================
If you find this project useful for your research, please cite our work (bibtex format): PyModeS is a Python library designed to decode Mode-S (including ADS-B) message. It can be imported to your python project or used as a standalone tool to view and save live traffic data.
:: This is a project created by Junzi Sun, who works at `TU Delft <https://www.tudelft.nl/en/>`_, `Aerospace Engineering Faculty <https://www.tudelft.nl/en/ae/>`_, `CNS/ATM research group <http://cs.lr.tudelft.nl/atm/>`_. It is supported by many `contributors <https://github.com/junzis/pyModeS/graphs/contributors>`_ from different institutions.
Introduction
------------
pyModeS supports the decoding of following types of messages:
- DF4 / DF20: Altitude code
- DF5 / DF21: Identity code (squawk code)
- DF17 / DF18: Automatic Dependent Surveillance-Broadcast (ADS-B)
- 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]
- DF20 / DF21: Mode-S Comm-B messages
- BDS 1,0: Data link capability report
- BDS 1,7: Common usage GICB capability report
- BDS 2,0: Aircraft identification
- BDS 3,0: ACAS active resolution advisory
- BDS 4,0: Selected vertical intention
- BDS 4,4: Meteorological routine air report (experimental)
- BDS 4,5: Meteorological hazard report (experimental)
- BDS 5,0: Track and turn report
- BDS 6,0: Heading and speed report
If you find this project useful for your research, please considering cite this tool as::
@article{sun2019pymodes, @article{sun2019pymodes,
author={J. {Sun} and H. {V\^u} and J. {Ellerbroek} and J. M. {Hoekstra}}, author={J. {Sun} and H. {V\^u} and J. {Ellerbroek} and J. M. {Hoekstra}},
@@ -16,95 +50,82 @@ If you find this project useful for your research, please cite our work (bibtex
Introduction
---------------------
PyModeS is a Python library designed to decode Mode-S (including ADS-B) message.
Message with following Downlink Formats (DF) are supported:
**DF17 / DF18: Automatic Dependent Surveillance - Broadcast (ADS-B)**
- 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]
**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 3,0: ACAS active resolution advisory
- BDS 4,0: Selected vertical intention
- BDS 4,4: Meteorological routine air report (experimental)
- BDS 4,5: Meteorological hazard report (experimental)
- BDS 5,0: Track and turn report
- BDS 6,0: Heading and speed report
**DF4 / DF20: Altitude code**
**DF5 / DF21: Identity code (squawk code)**
Resources Resources
----------- -----------
Checkout and contribute to this open-source project at: Check out and contribute to this open-source project at:
https://github.com/junzis/pyModeS https://github.com/junzis/pyModeS
Detailed manual on Mode-S decoding is published at: Detailed manual on Mode-S decoding is published at:
https://mode-s.org/decode. https://mode-s.org/decode
API documentation of pyModeS is at: The API documentation of pyModeS is at:
http://pymodes.readthedocs.io https://mode-s.org/api
Install Basic installation
------- -------------------
To install latest version from the GitHub: Installation examples::
:: # stable version
pip install pyModeS
# development version
pip install git+https://github.com/junzis/pyModeS pip install git+https://github.com/junzis/pyModeS
To install the stable version (2.0) from pip: Dependencies ``numpy``, ``pyzmq`` and ``pyrtlsdr`` are installed automatically during previous installations processes.
:: Advanced installation (using c modules)
------------------------------------------
pip install pyModeS If you want to make use of the (faster) c module, install ``pyModeS`` as follows::
git clone https://github.com/junzis/pyModeS
cd pyModeS
make ext
make install
View live traffic (modeslive)
Live view traffic (modeslive)
---------------------------------------------------- ----------------------------------------------------
Supports **Mode-S Beast** and **AVR** raw stream
:: General usage::
modeslive --server [server_address] --port [tcp_port] --rawtype [beast,avr,skysense] --latlon [lat] [lon] --dumpto [folder] $ modeslive [-h] --source SOURCE [--connect SERVER PORT DATAYPE]
[--latlon LAT LON] [--show-uncertainty] [--dumpto DUMPTO]
Arguments: arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--server SERVER server address or IP --source SOURCE Choose data source, "rtlsdr" or "net"
--port PORT raw data port --connect SERVER PORT DATATYPE
--rawtype RAWTYPE beast, avr or skysense Define server, port and data type. Supported data
--latlon LAT LON receiver position types are: ['raw', 'beast', 'skysense']
--show-uncertainty display uncertaint values, default off --latlon LAT LON Receiver latitude and longitude, needed for the surface
--dumpto folder to dump decoded output position, default none
--show-uncertainty Display uncertainty values, default off
--dumpto DUMPTO Folder to dump decoded output, default none
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: Live with RTL-SDR
*******************
If you have an RTL-SDR receiver plugged to the computer, you can connect it with ``rtlsdr`` source switch, shown as follows::
$ modeslive --source rtlsdr
Live with network data
***************************
If you want to connect to a TCP server that broadcast raw data. use can use ``net`` source switch, for example::
$ modeslive --source net --connect localhost 30002 raw
$ modeslive --source net --connect 127.0.0.1 30005 beast
::
$ modesmixer2 --inSeriel port[:speed[:flow_control]] --outServer beast:[tcp_port]
Example screenshot: Example screenshot:
@@ -132,7 +153,7 @@ Common functions
pms.hex2bin(str) # Convert hexadecimal string to binary string pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer pms.hex2int(str) # Convert hexadecimal string to integer
pms.gray2int(str) # Convert grey code to interger pms.gray2int(str) # Convert grey code to integer
Core functions for ADS-B decoding Core functions for ADS-B decoding
@@ -146,10 +167,11 @@ Core functions for ADS-B decoding
# Typecode 1-4 # Typecode 1-4
pms.adsb.callsign(msg) pms.adsb.callsign(msg)
# Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 9-18 (airborne, GNSS height) # Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 20-22 (airborne, GNSS height)
pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None) 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.airborne_position(msg_even, msg_odd, t_even, t_odd)
pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref) pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref)
pms.adsb.surface_velocity(msg)
pms.adsb.position_with_ref(msg, lat_ref, lon_ref) pms.adsb.position_with_ref(msg, lat_ref, lon_ref)
pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref) pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref)
@@ -160,15 +182,10 @@ Core functions for ADS-B decoding
# Typecode: 19 # Typecode: 19
pms.adsb.velocity(msg) # Handles both surface & airborne messages pms.adsb.velocity(msg) # Handles both surface & airborne messages
pms.adsb.speed_heading(msg) # Handles both surface & airborne messages pms.adsb.speed_heading(msg) # Handles both surface & airborne messages
pms.adsb.surface_velocity(msg)
pms.adsb.airborne_velocity(msg) pms.adsb.airborne_velocity(msg)
Note: When you have a fix position of the aircraft, it is convenient to Note: When you have a fix position of the aircraft, it is convenient to use `position_with_ref()` method to decode with only one position message (either odd or even). This works with both airborne and surface position messages. But the reference position shall be within 180NM (airborne) or 45NM (surface) of the true position.
use `position_with_ref()` method to decode with only one position message
(either odd or even). This works with both airborne and surface position
messages. But the reference position shall be with in 180NM (airborne)
or 45NM (surface) of the true position.
Decode altitude replies in DF4 / DF20 Decode altitude replies in DF4 / DF20
@@ -275,25 +292,24 @@ Meteorological hazard air report (MHR) [Experimental]
Customize the streaming module Customize the streaming module
****************************** ******************************
The TCP client module from pyModeS can be re-used to stream and process Mode-S The TCP client module from pyModeS can be re-used to stream and process Mode-S data as you like. You need to re-implement the ``handle_messages()`` function from the ``TcpClient`` class to write your own logic to handle the messages.
data as your like. You need to re-implement the ``handle_messages()`` function from
the ``BaseClient`` class to write your own logic to handle the messages.
Here is an example: Here is an example:
.. code:: python .. code:: python
from pyModeS.extra.tcpclient import BaseClient import pyModeS as pms
from pyModeS.extra.tcpclient import TcpClient
# define your custom class by extending the BaseClient # define your custom class by extending the TcpClient
# - implement your handle_messages() methods # - implement your handle_messages() methods
class ADSBClient(BaseClient): class ADSBClient(TcpClient):
def __init__(self, host, port, rawtype): def __init__(self, host, port, rawtype):
super(ModesClient, self).__init__(host, port, rawtype) super(ADSBClient, self).__init__(host, port, rawtype)
def handle_messages(self, messages): def handle_messages(self, messages):
for msg, ts in messages: for msg, ts in messages:
if len(msg) < 28: # wrong data length if len(msg) != 28: # wrong data length
continue continue
df = pms.df(msg) df = pms.df(msg)
@@ -301,24 +317,35 @@ Here is an example:
if df != 17: # not ADSB if df != 17: # not ADSB
continue continue
if '1' in pms.crc(msg): # CRC fail if pms.crc(msg) !=0: # CRC fail
continue continue
icao = pms.adsb.icao(msg) icao = pms.adsb.icao(msg)
tc = pms.adsb.typecode(msg) tc = pms.adsb.typecode(msg)
# TODO: write you magic code here # TODO: write you magic code here
print ts, icao, tc, msg print(ts, icao, tc, msg)
# run new client, change the host, port, and rawtype if needed # run new client, change the host, port, and rawtype if needed
client = ADSBClient(host='127.0.0.1', port=30334, rawtype='beast') client = ADSBClient(host='127.0.0.1', port=30005, rawtype='beast')
client.run() client.run()
Unit test Unit test
--------- ---------
To perform unit tests. First install ``tox`` through pip, Then, run the following commands: To perform unit tests, ``pytest`` must be install first.
.. code:: bash Build Cython extensions
::
$ tox $ make ext
Run unit tests
::
$ make test
Clean build files
::
$ make clean

View File

@@ -17,5 +17,5 @@ help:
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile %: Makefile
rm -f source/pyModeS*.rst source/modules.rst rm -f source/pyModeS*.rst source/modules.rst
sphinx-apidoc -f -e -M -o source/ ../pyModeS sphinx-apidoc -f -e -M -o source/ ../pyModeS ../pyModeS/decoder/ehs.py ../pyModeS/decoder/els.py ../pyModeS/streamer ../pyModeS/extra
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -0,0 +1,17 @@
{% extends "!layout.html" %}
{% block footer %}
{{ super() }}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-74700456-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-74700456-1');
</script>
{% endblock %}

View File

@@ -14,19 +14,20 @@
# #
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'pyModeS' project = "pyModeS"
copyright = '2019, Junzi Sun' copyright = "2019, Junzi Sun"
author = 'Junzi Sun' author = "Junzi Sun"
# The short X.Y version # The short X.Y version
version = '' version = ""
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '' release = ""
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@@ -39,26 +40,24 @@ release = ''
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinx.ext.todo', "sphinx.ext.mathjax",
'sphinx.ext.coverage', "sphinx.ext.viewcode",
'sphinx.ext.mathjax', "sphinx.ext.githubpages",
'sphinx.ext.viewcode', "sphinx.ext.napoleon",
'sphinx.ext.githubpages',
'sphinx.ext.napoleon',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
# templates_path = [''] templates_path = ["_templates"]
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@@ -82,6 +81,10 @@ pygments_style = None
# a list of builtin themes. # a list of builtin themes.
# #
# html_theme = 'alabaster' # html_theme = 'alabaster'
html_theme = "neo_rtd_theme"
import sphinx_theme
html_theme_path = [sphinx_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@@ -108,7 +111,7 @@ pygments_style = None
# -- Options for HTMLHelp output --------------------------------------------- # -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'pyModeSdoc' htmlhelp_basename = "pyModeSdoc"
# -- Options for LaTeX output ------------------------------------------------ # -- Options for LaTeX output ------------------------------------------------
@@ -117,15 +120,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
@@ -135,8 +135,7 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'pyModeS.tex', 'pyModeS Documentation', (master_doc, "pyModeS.tex", "pyModeS Documentation", "Junzi Sun", "manual")
'Junzi Sun', 'manual'),
] ]
@@ -144,10 +143,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, "pymodes", "pyModeS Documentation", [author], 1)]
(master_doc, 'pymodes', 'pyModeS Documentation',
[author], 1)
]
# -- Options for Texinfo output ---------------------------------------------- # -- Options for Texinfo output ----------------------------------------------
@@ -156,9 +152,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'pyModeS', 'pyModeS Documentation', (
author, 'pyModeS', 'One line description of project.', master_doc,
'Miscellaneous'), "pyModeS",
"pyModeS Documentation",
author,
"pyModeS",
"One line description of project.",
"Miscellaneous",
)
] ]
@@ -177,7 +179,7 @@ epub_title = project
# epub_uid = '' # epub_uid = ''
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ["search.html"]
# -- Extension configuration ------------------------------------------------- # -- Extension configuration -------------------------------------------------

View File

@@ -9,11 +9,49 @@ Welcome to pyModeS documentation!
The source code can be found at: https://github.com/junzis/pyModeS The source code can be found at: https://github.com/junzis/pyModeS
.. toctree:: .. toctree::
:maxdepth: 3 :caption: Core modules
:maxdepth: 2
pyModeS.decoder.adsb
pyModeS.decoder.commb
.. toctree::
:caption: ADS-B messages
:maxdepth: 2
pyModeS.decoder.bds.bds05
pyModeS.decoder.bds.bds06
pyModeS.decoder.bds.bds08
pyModeS.decoder.bds.bds09
.. toctree::
:caption: ELS - elementary surveillance
:maxdepth: 2
pyModeS.decoder.bds.bds10
pyModeS.decoder.bds.bds17
pyModeS.decoder.bds.bds20
pyModeS.decoder.bds.bds30
.. toctree::
:caption: EHS - enhanced surveillance
:maxdepth: 2
pyModeS.decoder.bds.bds40
pyModeS.decoder.bds.bds50
pyModeS.decoder.bds.bds60
.. toctree::
:caption: MRAR / MHR
:maxdepth: 2
pyModeS.decoder.bds.bds44
pyModeS.decoder.bds.bds45
pyModeS.decoder
pyModeS.streamer
pyModeS.extra
---- ----

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@@ -0,0 +1,7 @@
pyModeS.c\_common module
========================
.. automodule:: pyModeS.c_common
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,7 @@
pyModeS.common module
=====================
.. automodule:: pyModeS.common
:members:
:undoc-members:
:show-inheritance:

View File

@@ -10,6 +10,7 @@ Submodules
---------- ----------
.. toctree:: .. toctree::
:maxdepth: 4
pyModeS.decoder.bds.bds05 pyModeS.decoder.bds.bds05
pyModeS.decoder.bds.bds06 pyModeS.decoder.bds.bds06
@@ -25,4 +26,3 @@ Submodules
pyModeS.decoder.bds.bds50 pyModeS.decoder.bds.bds50
pyModeS.decoder.bds.bds53 pyModeS.decoder.bds.bds53
pyModeS.decoder.bds.bds60 pyModeS.decoder.bds.bds60

View File

@@ -1,7 +0,0 @@
pyModeS.decoder.common module
=============================
.. automodule:: pyModeS.decoder.common
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,7 +0,0 @@
pyModeS.decoder.ehs module
==========================
.. automodule:: pyModeS.decoder.ehs
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,7 +0,0 @@
pyModeS.decoder.els module
==========================
.. automodule:: pyModeS.decoder.els
:members:
:undoc-members:
:show-inheritance:

View File

@@ -10,6 +10,7 @@ Subpackages
----------- -----------
.. toctree:: .. toctree::
:maxdepth: 4
pyModeS.decoder.bds pyModeS.decoder.bds
@@ -17,14 +18,12 @@ Submodules
---------- ----------
.. toctree:: .. toctree::
:maxdepth: 4
pyModeS.decoder.acas pyModeS.decoder.acas
pyModeS.decoder.adsb pyModeS.decoder.adsb
pyModeS.decoder.allcall pyModeS.decoder.allcall
pyModeS.decoder.commb pyModeS.decoder.commb
pyModeS.decoder.common
pyModeS.decoder.ehs
pyModeS.decoder.els
pyModeS.decoder.surv pyModeS.decoder.surv
pyModeS.decoder.uncertainty pyModeS.decoder.uncertainty
pyModeS.decoder.uplink

View File

@@ -0,0 +1,7 @@
pyModeS.decoder.uplink module
=============================
.. automodule:: pyModeS.decoder.uplink
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,7 +0,0 @@
pyModeS.extra.aero module
=========================
.. automodule:: pyModeS.extra.aero
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,16 +0,0 @@
pyModeS.extra package
=====================
.. automodule:: pyModeS.extra
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
pyModeS.extra.aero
pyModeS.extra.tcpclient

View File

@@ -1,7 +0,0 @@
pyModeS.extra.tcpclient module
==============================
.. automodule:: pyModeS.extra.tcpclient
:members:
:undoc-members:
:show-inheritance:

View File

@@ -10,8 +10,15 @@ Subpackages
----------- -----------
.. toctree:: .. toctree::
:maxdepth: 4
pyModeS.decoder pyModeS.decoder
pyModeS.extra
pyModeS.streamer
Submodules
----------
.. toctree::
:maxdepth: 4
pyModeS.c_common
pyModeS.common

View File

@@ -1,16 +0,0 @@
pyModeS.streamer package
========================
.. automodule:: pyModeS.streamer
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
pyModeS.streamer.screen
pyModeS.streamer.stream

View File

@@ -1,7 +0,0 @@
pyModeS.streamer.screen module
==============================
.. automodule:: pyModeS.streamer.screen
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,7 +0,0 @@
pyModeS.streamer.stream module
==============================
.. automodule:: pyModeS.streamer.stream
:members:
:undoc-members:
:show-inheritance:

0
doc/warnings Normal file
View File

View File

@@ -1,15 +1,21 @@
from __future__ import absolute_import, print_function, division import os
import warnings
from .decoder.common import * try:
from . import c_common as common
from .c_common import *
except:
from . import common
from .common import *
from .decoder import tell
from .decoder import adsb from .decoder import adsb
from .decoder import commb from .decoder import commb
from .decoder import common
from .decoder import bds from .decoder import bds
from .extra import aero from .extra import aero
from .extra import tcpclient from .extra import tcpclient
# from .decoder import els # depricated
# from .decoder import ehs # depricated
import os warnings.simplefilter("once", DeprecationWarning)
dirpath = os.path.dirname(os.path.realpath(__file__)) dirpath = os.path.dirname(os.path.realpath(__file__))

23
pyModeS/c_common.pxd Normal file
View File

@@ -0,0 +1,23 @@
# cython: language_level=3
cdef int char_to_int(unsigned char binstr)
cdef unsigned char int_to_char(unsigned char i)
cpdef str hex2bin(str hexstr)
cpdef long bin2int(str binstr)
cpdef long hex2int(str binstr)
cpdef unsigned char df(str msg)
cpdef long crc(str msg, bint encode=*)
cpdef long floor(double x)
cpdef str icao(str msg)
cpdef bint is_icao_assigned(str icao)
cpdef int typecode(str msg)
cpdef int cprNL(double lat)
cpdef str idcode(str msg)
cpdef int altcode(str msg)
cpdef str data(str msg)
cpdef bint allzeros(str msg)

429
pyModeS/c_common.pyx Normal file
View File

@@ -0,0 +1,429 @@
# cython: language_level=3
cimport cython
from cpython cimport array
from cpython.bytes cimport PyBytes_GET_SIZE
from cpython.bytearray cimport PyByteArray_GET_SIZE
from libc.math cimport abs, cos, acos, fabs, M_PI as pi, floor as c_floor
cdef int char_to_int(unsigned char binstr):
if 48 <= binstr <= 57: # 0 to 9
return binstr - 48
if 97 <= binstr <= 102: # a to f
return binstr - 97 + 10
if 65 <= binstr <= 70: # A to F
return binstr - 65 + 10
return 0
cdef unsigned char int_to_char(unsigned char i):
if i < 10:
return 48 + i # "0" + i
return 97 - 10 + i # "a" - 10 + i
@cython.boundscheck(False)
@cython.overflowcheck(False)
cpdef str hex2bin(str hexstr):
"""Convert a hexdecimal string to binary string, with zero fillings."""
# num_of_bits = len(hexstr) * 4
cdef hexbytes = bytes(hexstr.encode())
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
# binstr = bin(int(hexbytes, 16))[2:].zfill(int(num_of_bits))
cdef bytearray _binstr = bytearray(4 * len_hexstr)
cdef unsigned char[:] binstr = _binstr
cdef unsigned char int_
cdef Py_ssize_t i
for i in range(len_hexstr):
int_ = char_to_int(hexbytes[i])
binstr[4*i] = int_to_char((int_ >> 3) & 1)
binstr[4*i+1] = int_to_char((int_ >> 2) & 1)
binstr[4*i+2] = int_to_char((int_ >> 1) & 1)
binstr[4*i+3] = int_to_char((int_) & 1)
return _binstr.decode()
@cython.boundscheck(False)
cpdef long bin2int(str binstr):
"""Convert a binary string to integer."""
# return int(binstr, 2)
cdef bytearray binbytes = bytearray(binstr.encode())
cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes)
cdef long cumul = 0
cdef unsigned char[:] v_binstr = binbytes
for i in range(len_):
cumul = 2*cumul + char_to_int(v_binstr[i])
return cumul
@cython.boundscheck(False)
cpdef long hex2int(str hexstr):
"""Convert a binary string to integer."""
# return int(hexstr, 2)
cdef bytearray binbytes = bytearray(hexstr.encode())
cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes)
cdef long cumul = 0
cdef unsigned char[:] v_hexstr = binbytes
for i in range(len_):
cumul = 16*cumul + char_to_int(v_hexstr[i])
return cumul
@cython.boundscheck(False)
cpdef unsigned char df(str msg):
"""Decode Downlink Format vaule, bits 1 to 5."""
cdef str dfbin = hex2bin(msg[:2])
# return min(bin2int(dfbin[0:5]), 24)
cdef long df = bin2int(dfbin[0:5])
if df > 24:
return 24
return df
# the CRC generator
# G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000])
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef long crc(str msg, bint encode=False):
"""Mode-S Cyclic Redundancy Check.
Detect if bit error occurs in the Mode-S message. When encode option is on,
the checksum is generated.
Args:
msg (string): 28 bytes hexadecimal message string
encode (bool): True to encode the date only and return the checksum
Returns:
int: message checksum, or partity bits (encoder)
"""
# the CRC generator
# G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
# cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000])
cdef long[4] G = _G
# msgbin_split = wrap(msgbin, 8)
# mbytes = list(map(bin2int, msgbin_split))
cdef bytearray _msgbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] msgbin = _msgbin
cdef Py_ssize_t len_msgbin = PyByteArray_GET_SIZE(_msgbin)
cdef Py_ssize_t len_mbytes = len_msgbin // 8
cdef Py_ssize_t i
if encode:
for i in range(len_msgbin - 24, len_msgbin):
msgbin[i] = 0
cdef array.array _mbytes = array.array(
'l', [bin2int(_msgbin[8*i:8*i+8].decode()) for i in range(len_mbytes)]
)
cdef long[:] mbytes = _mbytes
cdef long bits, mask
cdef Py_ssize_t ibyte, ibit
for ibyte in range(len_mbytes - 3):
for ibit in range(8):
mask = 0x80 >> ibit
bits = mbytes[ibyte] & mask
if bits > 0:
mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit)
mbytes[ibyte + 1] = mbytes[ibyte + 1] ^ (
0xFF & ((G[0] << 8 - ibit) | (G[1] >> ibit))
)
mbytes[ibyte + 2] = mbytes[ibyte + 2] ^ (
0xFF & ((G[1] << 8 - ibit) | (G[2] >> ibit))
)
mbytes[ibyte + 3] = mbytes[ibyte + 3] ^ (
0xFF & ((G[2] << 8 - ibit) | (G[3] >> ibit))
)
cdef long result = (mbytes[len_mbytes-3] << 16) | (mbytes[len_mbytes-2] << 8) | mbytes[len_mbytes-1]
return result
cpdef long floor(double x):
"""Mode-S floor function.
Defined as the greatest integer value k, such that k <= x
For example: floor(3.6) = 3 and floor(-3.6) = -4
"""
return <long> c_floor(x)
cpdef str icao(str msg):
"""Calculate the ICAO address from an Mode-S message.
Applicable only with DF4, DF5, DF20, DF21 messages.
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
cdef unsigned char DF = df(msg)
cdef long c0, c1
if DF in (11, 17, 18):
addr = msg[2:8]
elif DF in (0, 4, 5, 16, 20, 21):
c0 = crc(msg, encode=True)
c1 = hex2int(msg[-6:])
addr = "%06X" % (c0 ^ c1)
else:
addr = None
return addr
cpdef bint is_icao_assigned(str 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
cdef long 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
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int typecode(str 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 -1
# return None
cdef str tcbin = hex2bin(msg[8:10])
return bin2int(tcbin[0:5])
@cython.cdivision(True)
cpdef int cprNL(double lat):
"""NL() function in CPR decoding."""
if abs(lat) <= 1e-08:
return 59
elif abs(abs(lat) - 87) <= 1e-08 + 1e-05 * 87:
return 2
elif lat > 87 or lat < -87:
return 1
cdef int nz = 15
cdef double a = 1 - cos(pi / (2 * nz))
cdef double b = cos(pi / 180.0 * fabs(lat)) ** 2
cdef double nl = 2 * pi / (acos(1 - a / b))
NL = floor(nl)
return NL
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef str idcode(str msg):
"""Compute identity (squawk code).
Applicable only for DF5 or DF21 messages, 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.")
cdef bytearray _mbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] mbin = _mbin
cdef bytearray _idcode = bytearray(4)
cdef unsigned char[:] idcode = _idcode
cdef unsigned char C1 = mbin[19]
cdef unsigned char A1 = mbin[20]
cdef unsigned char C2 = mbin[21]
cdef unsigned char A2 = mbin[22]
cdef unsigned char C4 = mbin[23]
cdef unsigned char A4 = mbin[24]
# _ = mbin[25]
cdef unsigned char B1 = mbin[26]
cdef unsigned char D1 = mbin[27]
cdef unsigned char B2 = mbin[28]
cdef unsigned char D2 = mbin[29]
cdef unsigned char B4 = mbin[30]
cdef unsigned char 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)
idcode[0] = int_to_char((char_to_int(A4)*2 + char_to_int(A2))*2 + char_to_int(A1))
idcode[1] = int_to_char((char_to_int(B4)*2 + char_to_int(B2))*2 + char_to_int(B1))
idcode[2] = int_to_char((char_to_int(C4)*2 + char_to_int(C2))*2 + char_to_int(C1))
idcode[3] = int_to_char((char_to_int(D4)*2 + char_to_int(D2))*2 + char_to_int(D1))
return _idcode.decode()
#return str(byte1) + str(byte2) + str(byte3) + str(byte4)
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef int altcode(str msg):
"""Compute the altitude.
Applicable only for 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
cdef bytearray _mbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] mbin = _mbin
cdef char mbit = mbin[25] # M bit: 26
cdef char qbit = mbin[27] # Q bit: 28
cdef int alt = 0
cdef bytearray vbin
cdef bytearray _graybytes = bytearray(11)
cdef unsigned char[:] graybytes = _graybytes
if mbit == 48: # unit in ft, "0" -> 48
if qbit == 49: # 25ft interval, "1" -> 49
vbin = _mbin[19:25] + _mbin[26:27] + _mbin[28:32]
alt = bin2int(vbin.decode()) * 25 - 1000
if qbit == 48: # 100ft interval, above 50175ft, "0" -> 48
graybytes[8] = mbin[19]
graybytes[2] = mbin[20]
graybytes[9] = mbin[21]
graybytes[3] = mbin[22]
graybytes[10] = mbin[23]
graybytes[4] = mbin[24]
# _ = mbin[25]
graybytes[5] = mbin[26]
# cdef char D1 = mbin[27] # always zero
graybytes[6] = mbin[28]
graybytes[0] = mbin[29]
graybytes[7] = mbin[30]
graybytes[1] = mbin[31]
# graybytes = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
alt = gray2alt(_graybytes.decode())
if mbit == 49: # unit in meter, "1" -> 49
vbin = _mbin[19:25] + _mbin[26:31]
alt = int(bin2int(vbin.decode()) * 3.28084) # convert to ft
return alt
cpdef int gray2alt(str codestr):
cdef str gc500 = codestr[:8]
cdef int n500 = gray2int(gc500)
# in 100-ft step must be converted first
cdef str gc100 = codestr[8:]
cdef int n100 = gray2int(gc100)
if n100 in [0, 5, 6]:
return -1
#return None
if n100 == 7:
n100 = 5
if n500 % 2:
n100 = 6 - n100
alt = (n500 * 500 + n100 * 100) - 1300
return alt
cdef int gray2int(str graystr):
"""Convert greycode to binary."""
cdef int num = bin2int(graystr)
num ^= num >> 8
num ^= num >> 4
num ^= num >> 2
num ^= num >> 1
return num
cpdef str data(str msg):
"""Return the data frame in the message, bytes 9 to 22."""
return msg[8:-6]
cpdef bint allzeros(str 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

View File

@@ -1,7 +1,7 @@
from __future__ import absolute_import, print_function, division
import numpy as np import numpy as np
from textwrap import wrap from textwrap import wrap
def hex2bin(hexstr): def hex2bin(hexstr):
"""Convert a hexdecimal string to binary string, with zero fillings.""" """Convert a hexdecimal string to binary string, with zero fillings."""
num_of_bits = len(hexstr) * 4 num_of_bits = len(hexstr) * 4
@@ -14,35 +14,15 @@ def hex2int(hexstr):
return int(hexstr, 16) return int(hexstr, 16)
def int2hex(n):
"""Convert a integer to hexadecimal string."""
return hex(n)[2:].rjust(6, '0').upper()
def bin2int(binstr): def bin2int(binstr):
"""Convert a binary string to integer.""" """Convert a binary string to integer."""
return int(binstr, 2) return int(binstr, 2)
def bin2hex(hexstr):
"""Convert a hexdecimal string to integer."""
return int2hex(bin2int(hexstr))
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): def df(msg):
"""Decode Downlink Format vaule, bits 1 to 5.""" """Decode Downlink Format value, bits 1 to 5."""
msgbin = hex2bin(msg) dfbin = hex2bin(msg[:2])
return min(bin2int(msgbin[0:5]), 24) return min(bin2int(dfbin[0:5]), 24)
def crc(msg, encode=False): def crc(msg, encode=False):
@@ -59,10 +39,7 @@ def crc(msg, encode=False):
""" """
# the CRC generator # the CRC generator
G = [ G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
int("11111111", 2), int("11111010", 2),
int("00000100", 2), int("10000000", 2)
]
if encode: if encode:
msg = msg[:-6] + "000000" msg = msg[:-6] + "000000"
@@ -71,16 +48,22 @@ def crc(msg, encode=False):
msgbin_split = wrap(msgbin, 8) msgbin_split = wrap(msgbin, 8)
mbytes = list(map(bin2int, msgbin_split)) mbytes = list(map(bin2int, msgbin_split))
for ibyte in range(len(mbytes)-3): for ibyte in range(len(mbytes) - 3):
for ibit in range(8): for ibit in range(8):
mask = 0x80 >> ibit mask = 0x80 >> ibit
bits = mbytes[ibyte] & mask bits = mbytes[ibyte] & mask
if bits > 0: if bits > 0:
mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit) mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit)
mbytes[ibyte+1] = mbytes[ibyte+1] ^ (0xFF & ((G[0] << 8-ibit) | (G[1] >> ibit))) mbytes[ibyte + 1] = mbytes[ibyte + 1] ^ (
mbytes[ibyte+2] = mbytes[ibyte+2] ^ (0xFF & ((G[1] << 8-ibit) | (G[2] >> ibit))) 0xFF & ((G[0] << 8 - ibit) | (G[1] >> ibit))
mbytes[ibyte+3] = mbytes[ibyte+3] ^ (0xFF & ((G[2] << 8-ibit) | (G[3] >> ibit))) )
mbytes[ibyte + 2] = mbytes[ibyte + 2] ^ (
0xFF & ((G[1] << 8 - ibit) | (G[2] >> ibit))
)
mbytes[ibyte + 3] = mbytes[ibyte + 3] ^ (
0xFF & ((G[2] << 8 - ibit) | (G[3] >> ibit))
)
result = (mbytes[-3] << 16) | (mbytes[-2] << 8) | mbytes[-1] result = (mbytes[-3] << 16) | (mbytes[-2] << 8) | mbytes[-1]
@@ -90,24 +73,28 @@ def crc(msg, encode=False):
def crc_legacy(msg, encode=False): def crc_legacy(msg, encode=False):
"""Mode-S Cyclic Redundancy Check. (Legacy code, 2x slow).""" """Mode-S Cyclic Redundancy Check. (Legacy code, 2x slow)."""
# the polynominal generattor code for CRC [1111111111111010000001001] # 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]) 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) ng = len(generator)
msgnpbin = bin2np(hex2bin(msg)) msgnpbin = np.array([int(i) for i in hex2bin(msg)])
if encode: if encode:
msgnpbin[-24:] = [0] * 24 msgnpbin[-24:] = [0] * 24
# loop all bits, except last 24 piraty bits # loop all bits, except last 24 piraty bits
for i in range(len(msgnpbin)-24): for i in range(len(msgnpbin) - 24):
if msgnpbin[i] == 0: if msgnpbin[i] == 0:
continue continue
# perform XOR, when 1 # perform XOR, when 1
msgnpbin[i:i+ng] = np.bitwise_xor(msgnpbin[i:i+ng], generator) msgnpbin[i : i + ng] = np.bitwise_xor(msgnpbin[i : i + ng], generator)
# last 24 bits # last 24 bits
reminder = bin2int(np2bin(msgnpbin[-24:])) msgbin = np.array2string(msgnpbin[-24:], separator="")[1:-1]
reminder = bin2int(msgbin)
return reminder return reminder
@@ -139,8 +126,8 @@ def icao(msg):
addr = msg[2:8] addr = msg[2:8]
elif DF in (0, 4, 5, 16, 20, 21): elif DF in (0, 4, 5, 16, 20, 21):
c0 = crc(msg, encode=True) c0 = crc(msg, encode=True)
c1 = hex2int(msg[-6:]) c1 = int(msg[-6:], 16)
addr = '%06X' % (c0 ^ c1) addr = "%06X" % (c0 ^ c1)
else: else:
addr = None addr = None
@@ -149,23 +136,33 @@ def icao(msg):
def is_icao_assigned(icao): def is_icao_assigned(icao):
"""Check whether the ICAO address is assigned (Annex 10, Vol 3).""" """Check whether the ICAO address is assigned (Annex 10, Vol 3)."""
if (icao is None) or (not isinstance(icao, str)) or (len(icao)!=6): if (icao is None) or (not isinstance(icao, str)) or (len(icao) != 6):
return False return False
icaoint = hex2int(icao) icaoint = int(icao, 16)
if 0x200000 < icaoint < 0x27FFFF: return False # AFI if 0x200000 < icaoint < 0x27FFFF:
if 0x280000 < icaoint < 0x28FFFF: return False # SAM return False # AFI
if 0x500000 < icaoint < 0x5FFFFF: return False # EUR, NAT if 0x280000 < icaoint < 0x28FFFF:
if 0x600000 < icaoint < 0x67FFFF: return False # MID return False # SAM
if 0x680000 < icaoint < 0x6F0000: return False # ASIA if 0x500000 < icaoint < 0x5FFFFF:
if 0x900000 < icaoint < 0x9FFFFF: return False # NAM, PAC return False # EUR, NAT
if 0xB00000 < icaoint < 0xBFFFFF: return False # CAR if 0x600000 < icaoint < 0x67FFFF:
if 0xD00000 < icaoint < 0xDFFFFF: return False # future return False # MID
if 0xF00000 < icaoint < 0xFFFFFF: return False # future 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 return True
def typecode(msg): def typecode(msg):
"""Type code of ADS-B message """Type code of ADS-B message
@@ -178,26 +175,24 @@ def typecode(msg):
if df(msg) not in (17, 18): if df(msg) not in (17, 18):
return None return None
msgbin = hex2bin(msg) tcbin = hex2bin(msg[8:10])
return bin2int(msgbin[32:37]) return bin2int(tcbin[0:5])
def cprNL(lat): def cprNL(lat):
"""NL() function in CPR decoding.""" """NL() function in CPR decoding."""
if lat == 0: if np.isclose(lat, 0):
return 59 return 59
elif np.isclose(abs(lat), 87):
if lat == 87 or lat == -87:
return 2 return 2
elif lat > 87 or lat < -87:
if lat > 87 or lat < -87:
return 1 return 1
nz = 15 nz = 15
a = 1 - np.cos(np.pi / (2 * nz)) a = 1 - np.cos(np.pi / (2 * nz))
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2 b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
nl = 2 * np.pi / (np.arccos(1 - a/b)) nl = 2 * np.pi / (np.arccos(1 - a / b))
NL = floor(nl) NL = floor(nl)
return NL return NL
@@ -234,10 +229,10 @@ def idcode(msg):
B4 = mbin[30] B4 = mbin[30]
D4 = mbin[31] D4 = mbin[31]
byte1 = int(A4+A2+A1, 2) byte1 = int(A4 + A2 + A1, 2)
byte2 = int(B4+B2+B1, 2) byte2 = int(B4 + B2 + B1, 2)
byte3 = int(C4+C2+C1, 2) byte3 = int(C4 + C2 + C1, 2)
byte4 = int(D4+D2+D1, 2) byte4 = int(D4 + D2 + D1, 2)
return str(byte1) + str(byte2) + str(byte3) + str(byte4) return str(byte1) + str(byte2) + str(byte3) + str(byte4)
@@ -264,12 +259,11 @@ def altcode(msg):
mbit = mbin[25] # M bit: 26 mbit = mbin[25] # M bit: 26
qbit = mbin[27] # Q bit: 28 qbit = mbin[27] # Q bit: 28
if mbit == "0": # unit in ft
if mbit == '0': # unit in ft if qbit == "1": # 25ft interval
if qbit == '1': # 25ft interval
vbin = mbin[19:25] + mbin[26] + mbin[28:32] vbin = mbin[19:25] + mbin[26] + mbin[28:32]
alt = bin2int(vbin) * 25 - 1000 alt = bin2int(vbin) * 25 - 1000
if qbit == '0': # 100ft interval, above 50175ft if qbit == "0": # 100ft interval, above 50175ft
C1 = mbin[19] C1 = mbin[19]
A1 = mbin[20] A1 = mbin[20]
C2 = mbin[21] C2 = mbin[21]
@@ -287,7 +281,7 @@ def altcode(msg):
graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4 graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
alt = gray2alt(graystr) alt = gray2alt(graystr)
if mbit == '1': # unit in meter if mbit == "1": # unit in meter
vbin = mbin[19:25] + mbin[26:31] vbin = mbin[19:25] + mbin[26:31]
alt = int(bin2int(vbin) * 3.28084) # convert to ft alt = int(bin2int(vbin) * 3.28084) # convert to ft
@@ -308,20 +302,20 @@ def gray2alt(codestr):
if n100 == 7: if n100 == 7:
n100 = 5 n100 = 5
if n500%2: if n500 % 2:
n100 = 6 - n100 n100 = 6 - n100
alt = (n500*500 + n100*100) - 1300 alt = (n500 * 500 + n100 * 100) - 1300
return alt return alt
def gray2int(graystr): def gray2int(graystr):
"""Convert greycode to binary.""" """Convert greycode to binary."""
num = bin2int(graystr) num = bin2int(graystr)
num ^= (num >> 8) num ^= num >> 8
num ^= (num >> 4) num ^= num >> 4
num ^= (num >> 2) num ^= num >> 2
num ^= (num >> 1) num ^= num >> 1
return num return num
@@ -355,8 +349,8 @@ def wrongstatus(data, sb, msb, lsb):
""" """
# status bit, most significant bit, least significant bit # status bit, most significant bit, least significant bit
status = int(data[sb-1]) status = int(data[sb - 1])
value = bin2int(data[msb-1:lsb]) value = bin2int(data[msb - 1 : lsb])
if not status: if not status:
if value != 0: if value != 0:

View File

@@ -1,3 +1,142 @@
from __future__ import absolute_import, print_function, division def tell(msg):
from pyModeS import common, adsb, commb, bds
from pyModeS.decoder import * def _print(label, value, unit=None):
print("%20s: " % label, end="")
print("%s " % value, end="")
if unit:
print(unit)
else:
print()
df = common.df(msg)
icao = common.icao(msg)
_print("Message", msg)
_print("ICAO address", icao)
_print("Downlink Format", df)
if df == 17:
_print("Protocol", "Mode-S Extended Squitter (ADS-B)")
tc = common.typecode(msg)
if 1 <= tc <= 4: # callsign
callsign = adsb.callsign(msg)
_print("Type", "Identitification and category")
_print("Callsign:", callsign)
if 5 <= tc <= 8: # surface position
_print("Type", "Surface position")
oe = adsb.oe_flag(msg)
msgbin = common.hex2bin(msg)
cprlat = common.bin2int(msgbin[54:71]) / 131072.0
cprlon = common.bin2int(msgbin[71:88]) / 131072.0
v = adsb.surface_velocity(msg)
_print("CPR format", "Odd" if oe else "Even")
_print("CPR Latitude", cprlat)
_print("CPR Longitude", cprlon)
_print("Speed", v[0], "knots")
_print("Track", v[1], "degrees")
if 9 <= tc <= 18: # airborne position
_print("Type", "Airborne position (with barometric altitude)")
alt = adsb.altitude(msg)
oe = adsb.oe_flag(msg)
msgbin = common.hex2bin(msg)
cprlat = common.bin2int(msgbin[54:71]) / 131072.0
cprlon = common.bin2int(msgbin[71:88]) / 131072.0
_print("CPR format", "Odd" if oe else "Even")
_print("CPR Latitude", cprlat)
_print("CPR Longitude", cprlon)
_print("Altitude", alt, "feet")
if tc == 19:
_print("Type", "Airborne velocity")
spd, trk, vr, t = adsb.velocity(msg)
types = {"GS": "Ground speed", "TAS": "True airspeed"}
_print("Speed", spd, "knots")
_print("Track", trk, "degrees")
_print("Vertical rate", vr, "feet/minute")
_print("Type", types[t])
if 20 <= tc <= 22: # airborne position
_print("Type", "Airborne position (with GNSS altitude)")
alt = adsb.altitude(msg)
oe = adsb.oe_flag(msg)
msgbin = common.hex2bin(msg)
cprlat = common.bin2int(msgbin[54:71]) / 131072.0
cprlon = common.bin2int(msgbin[71:88]) / 131072.0
_print("CPR format", "Odd" if oe else "Even")
_print("CPR Latitude", cprlat)
_print("CPR Longitude", cprlon)
_print("Altitude", alt, "feet")
if df == 20:
_print("Protocol", "Mode-S Comm-B altitude reply")
_print("Altitude", common.altcode(msg), "feet")
if df == 21:
_print("Protocol", "Mode-S Comm-B identity reply")
_print("Squawk code", common.idcode(msg))
if df == 20 or df == 21:
labels = {
"BDS10": "Data link capability",
"BDS17": "GICB capability",
"BDS20": "Aircraft identification",
"BDS30": "ACAS resolution",
"BDS40": "Vertical intention report",
"BDS50": "Track and turn report",
"BDS60": "Heading and speed report",
"BDS44": "Meteorological routine air report",
"BDS45": "Meteorological hazard report",
"EMPTY": "[No information available]",
}
BDS = bds.infer(msg, mrar=True)
if BDS in labels.keys():
_print("BDS", "%s (%s)" % (BDS, labels[BDS]))
else:
_print("BDS", BDS)
if BDS == "BDS20":
callsign = commb.cs20(msg)
_print("Callsign", callsign)
if BDS == "BDS40":
_print("MCP target alt", commb.selalt40mcp(msg), "feet")
_print("FMS Target alt", commb.selalt40fms(msg), "feet")
_print("Pressure", commb.p40baro(msg), "millibar")
if BDS == "BDS50":
_print("Roll angle", commb.roll50(msg), "degrees")
_print("Track angle", commb.trk50(msg), "degrees")
_print("Track rate", commb.rtrk50(msg), "degree/second")
_print("Ground speed", commb.gs50(msg), "knots")
_print("True airspeed", commb.tas50(msg), "knots")
if BDS == "BDS60":
_print("Megnatic Heading", commb.hdg60(msg), "degrees")
_print("Indicated airspeed", commb.ias60(msg), "knots")
_print("Mach number", commb.mach60(msg))
_print("Vertical rate (Baro)", commb.vr60baro(msg), "feet/minute")
_print("Vertical rate (INS)", commb.vr60ins(msg), "feet/minute")
if BDS == "BDS44":
_print("Wind speed", commb.wind44(msg)[0], "knots")
_print("Wind direction", commb.wind44(msg)[1], "degrees")
_print("Temperature 1", commb.temp44(msg)[0], "Celsius")
_print("Temperature 2", commb.temp44(msg)[1], "Celsius")
_print("Pressure", commb.p44(msg), "hPa")
_print("Humidity", commb.hum44(msg), "%")
_print("Turbulence", commb.turb44(msg))
if BDS == "BDS45":
_print("Turbulence", commb.turb45(msg))
_print("Wind shear", commb.ws45(msg))
_print("Microbust", commb.mb45(msg))
_print("Icing", commb.ic45(msg))
_print("Wake vortex", commb.wv45(msg))
_print("Temperature", commb.temp45(msg), "Celsius")
_print("Pressure", commb.p45(msg), "hPa")
_print("Radio height", commb.rh45(msg), "feet")

View File

@@ -1,23 +1,5 @@
# 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 Decoding Air-Air Surveillance (ACAS) DF=0/16
[To be implemented] [To be implemented]
""" """
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common

View File

@@ -1,100 +1,101 @@
# Copyright (C) 2015 Junzi Sun (TU Delft) """ADS-B module.
# This program is free software: you can redistribute it and/or modify The ADS-B module also imports functions from the following modules:
# 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, - pyModeS.decoder.bds.bds05: ``airborne_position()``, ``airborne_position_with_ref()``, ``altitude()``
# but WITHOUT ANY WARRANTY; without even the implied warranty of - pyModeS.decoder.bds.bds06: ``surface_position()``, ``surface_position_with_ref()``, ``surface_velocity()``
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - pyModeS.decoder.bds.bds08: ``category()``, ``callsign()``
# GNU General Public License for more details. - pyModeS.decoder.bds.bds09: ``airborne_velocity()``, ``altitude_diff()``
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""ADS-B Wrapper.
The ADS-B wrapper also imports functions from the following modules:
- pyModeS.decoder.bds.bds05
Functions: ``airborne_position``, ``airborne_position_with_ref``, ``altitude``
- pyModeS.decoder.bds.bds06
Functions: ``surface_position``, ``surface_position_with_ref``, ``surface_velocity``
- pyModeS.decoder.bds.bds08
Functions: ``category``, ``callsign``
- pyModeS.decoder.bds.bds09
Functions: ``airborne_velocity``, ``altitude_diff``
""" """
from __future__ import absolute_import, print_function, division
import pyModeS as pms import pyModeS as pms
from pyModeS.decoder import common
from pyModeS import common
from pyModeS.decoder import uncertainty from pyModeS.decoder import uncertainty
# from pyModeS.decoder.bds import bds05, bds06, bds09 # 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.bds05 import (
from pyModeS.decoder.bds.bds06 import surface_position, surface_position_with_ref, surface_velocity airborne_position,
airborne_position_with_ref,
altitude as altitude05,
)
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.bds08 import category, callsign
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
def df(msg): def df(msg):
return common.df(msg) return common.df(msg)
def icao(msg): def icao(msg):
return common.icao(msg) return common.icao(msg)
def typecode(msg): def typecode(msg):
return common.typecode(msg) return common.typecode(msg)
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None): def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
"""Decode position from a pair of even and odd position message """Decode surface or airborne position from a pair of even and odd
(works with both airborne and surface position messages) position messages.
Note, that to decode surface position using the position message pair,
the reference position has to be provided.
Args: Args:
msg0 (string): even message (28 bytes hexadecimal string) msg0 (string): even message (28 hexdigits)
msg1 (string): odd message (28 bytes hexadecimal string) msg1 (string): odd message (28 hexdigits)
t0 (int): timestamps for the even message t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message t1 (int): timestamps for the odd message
lat_ref (float): latitude of reference position
lon_ref (float): longitude of reference position
Returns: Returns:
(float, float): (latitude, longitude) of the aircraft (float, float): (latitude, longitude) of the aircraft
""" """
tc0 = typecode(msg0) tc0 = typecode(msg0)
tc1 = typecode(msg1) tc1 = typecode(msg1)
if (5<=tc0<=8 and 5<=tc1<=8): if 5 <= tc0 <= 8 and 5 <= tc1 <= 8:
if (not lat_ref) or (not lon_ref): if lat_ref is None or lon_ref is None:
raise RuntimeError("Surface position encountered, a reference \ raise RuntimeError(
position lat/lon required. Location of \ "Surface position encountered, a reference position"
receiver can be used.") " lat/lon required. Location of receiver can be used."
)
else: else:
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref) return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
elif (9<=tc0<=18 and 9<=tc1<=18): elif 9 <= tc0 <= 18 and 9 <= tc1 <= 18:
# Airborne position with barometric height # Airborne position with barometric height
return airborne_position(msg0, msg1, t0, t1) return airborne_position(msg0, msg1, t0, t1)
elif (20<=tc0<=22 and 20<=tc1<=22): elif 20 <= tc0 <= 22 and 20 <= tc1 <= 22:
# Airborne position with GNSS height # Airborne position with GNSS height
return airborne_position(msg0, msg1, t0, t1) return airborne_position(msg0, msg1, t0, t1)
else: else:
raise RuntimeError("incorrect or inconsistant message types") raise RuntimeError("Incorrect or inconsistent message types")
def position_with_ref(msg, lat_ref, lon_ref): def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message, """Decode position with only one message.
knowing reference nearby location, such as previously
calculated location, ground station, or airport location, etc. A reference position is required, which can be previously
Works with both airborne and surface position messages. calculated location, ground station, or airport location.
The function works with both airborne and surface position messages.
The reference position shall be with in 180NM (airborne) or 45NM (surface) The reference position shall be with in 180NM (airborne) or 45NM (surface)
of the true position. of the true position.
Args: Args:
msg (string): even message (28 bytes hexadecimal string) msg (str): even message (28 hexdigits)
lat_ref: previous known latitude lat_ref: previous known latitude
lon_ref: previous known longitude lon_ref: previous known longitude
@@ -104,78 +105,70 @@ def position_with_ref(msg, lat_ref, lon_ref):
tc = typecode(msg) tc = typecode(msg)
if 5<=tc<=8: if 5 <= tc <= 8:
return surface_position_with_ref(msg, lat_ref, lon_ref) return surface_position_with_ref(msg, lat_ref, lon_ref)
elif 9<=tc<=18 or 20<=tc<=22: elif 9 <= tc <= 18 or 20 <= tc <= 22:
return airborne_position_with_ref(msg, lat_ref, lon_ref) return airborne_position_with_ref(msg, lat_ref, lon_ref)
else: else:
raise RuntimeError("incorrect or inconsistant message types") raise RuntimeError("incorrect or inconsistent message types")
def altitude(msg): def altitude(msg):
"""Decode aircraft altitude """Decode aircraft altitude.
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: altitude in feet int: altitude in feet
"""
"""
tc = typecode(msg) tc = typecode(msg)
if tc<5 or tc==19 or tc>22: if tc < 5 or tc == 19 or tc > 22:
raise RuntimeError("%s: Not a position message" % msg) raise RuntimeError("%s: Not a position message" % msg)
if tc>=5 and tc<=8: elif tc >= 5 and tc <= 8:
# surface position, altitude 0 # surface position, altitude 0
return 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: else:
return None # airborn position
return altitude05(msg)
def velocity(msg, rtn_sources=False): def velocity(msg, source=False):
"""Calculate the speed, heading, and vertical rate """Calculate the speed, heading, and vertical rate (handles both airborne or surface message).
(handles both airborne or surface message)
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
rtn_source (boolean): If the function will return source (boolean): Include direction and vertical rate sources in return. Default to False.
the sources for direction of travel and vertical If set to True, the function will return six value instead of four.
rate. This will change the return value from a four
element array to a six element array.
Returns: Returns:
(int, float, int, string, string, string): speed (kt), int, float, int, string, [string], [string]: Four or six parameters, including:
ground track or heading (degree), - Speed (kt)
rate of climb/descent (ft/min), speed type - Angle (degree), either ground track or heading
('GS' for ground speed, 'AS' for airspeed), - Vertical rate (ft/min)
direction source ('true_north' for ground track / true north - Speed type ('GS' for ground speed, 'AS' for airspeed)
as refrence, 'mag_north' for magnetic north as reference), - [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
rate of climb/descent source ('Baro' for barometer, 'GNSS' - [Optional] Vertical rate source ('BARO' or 'GNSS')
for GNSS constellation).
For surface messages, vertical rate and its respective sources are set to None.
In the case of surface messages, None will be put in place
for vertical rate and its respective sources.
""" """
if 5 <= typecode(msg) <= 8: if 5 <= typecode(msg) <= 8:
return surface_velocity(msg, rtn_sources) return surface_velocity(msg, source)
elif typecode(msg) == 19: elif typecode(msg) == 19:
return airborne_velocity(msg, rtn_sources) return airborne_velocity(msg, source)
else: else:
raise RuntimeError("incorrect or inconsistant message types, expecting 4<TC<9 or TC=19") raise RuntimeError(
"incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
)
def speed_heading(msg): def speed_heading(msg):
@@ -183,7 +176,7 @@ def speed_heading(msg):
(handles both airborne or surface message) (handles both airborne or surface message)
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
(int, float): speed (kt), ground track or heading (degree) (int, float): speed (kt), ground track or heading (degree)
@@ -195,7 +188,7 @@ def speed_heading(msg):
def oe_flag(msg): def oe_flag(msg):
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd. """Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: 0 or 1, for even or odd frame int: 0 or 1, for even or odd frame
""" """
@@ -207,7 +200,7 @@ def version(msg):
"""ADS-B Version """ADS-B Version
Args: Args:
msg (string): 28 bytes hexadecimal message string, TC = 31 msg (str): 28 hexdigits string, TC = 31
Returns: Returns:
int: version number int: version number
@@ -215,7 +208,9 @@ def version(msg):
tc = typecode(msg) tc = typecode(msg)
if tc != 31: if tc != 31:
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg) raise RuntimeError(
"%s: Not a status operation message, expecting TC = 31" % msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
version = common.bin2int(msgbin[72:75]) version = common.bin2int(msgbin[72:75])
@@ -227,7 +222,7 @@ def nuc_p(msg):
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1) """Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
Args: Args:
msg (string): 28 bytes hexadecimal message string, msg (str): 28 hexdigits string,
Returns: Returns:
int: Horizontal Protection Limit int: Horizontal Protection Limit
@@ -241,18 +236,18 @@ def nuc_p(msg):
raise RuntimeError( raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \ "%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \ airborne position message (8<TC<19), \
or airborne position with GNSS height (20<TC<22)" % msg or airborne position with GNSS height (20<TC<22)"
% msg
) )
try: try:
NUCp = uncertainty.TC_NUCp_lookup[tc] NUCp = uncertainty.TC_NUCp_lookup[tc]
HPL = uncertainty.NUCp[NUCp]['HPL'] HPL = uncertainty.NUCp[NUCp]["HPL"]
RCu = uncertainty.NUCp[NUCp]['RCu'] RCu = uncertainty.NUCp[NUCp]["RCu"]
RCv = uncertainty.NUCp[NUCp]['RCv'] RCv = uncertainty.NUCp[NUCp]["RCv"]
except KeyError: except KeyError:
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
if tc in [20, 21]: if tc in [20, 21]:
RCv = uncertainty.NA RCv = uncertainty.NA
@@ -263,7 +258,7 @@ def nuc_v(msg):
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1) """Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
Args: Args:
msg (string): 28 bytes hexadecimal message string, msg (str): 28 hexdigits string,
Returns: Returns:
int or string: 95% Horizontal Velocity Error int or string: 95% Horizontal Velocity Error
@@ -272,15 +267,16 @@ def nuc_v(msg):
tc = typecode(msg) tc = typecode(msg)
if tc != 19: if tc != 19:
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg) raise RuntimeError(
"%s: Not an airborne velocity message, expecting TC = 19" % msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
NUCv = common.bin2int(msgbin[42:45]) NUCv = common.bin2int(msgbin[42:45])
try: try:
HVE = uncertainty.NUCv[NUCv]['HVE'] HVE = uncertainty.NUCv[NUCv]["HVE"]
VVE = uncertainty.NUCv[NUCv]['VVE'] VVE = uncertainty.NUCv[NUCv]["VVE"]
except KeyError: except KeyError:
HVE, VVE = uncertainty.NA, uncertainty.NA HVE, VVE = uncertainty.NA, uncertainty.NA
@@ -291,7 +287,7 @@ def nic_v1(msg, NICs):
"""Calculate NIC, navigation integrity category, for ADS-B version 1 """Calculate NIC, navigation integrity category, for ADS-B version 1
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
NICs (int or string): NIC supplement NICs (int or string): NIC supplement
Returns: Returns:
@@ -302,7 +298,8 @@ def nic_v1(msg, NICs):
raise RuntimeError( raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \ "%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \ airborne position message (8<TC<19), \
or airborne position with GNSS height (20<TC<22)" % msg or airborne position with GNSS height (20<TC<22)"
% msg
) )
tc = typecode(msg) tc = typecode(msg)
@@ -312,8 +309,8 @@ def nic_v1(msg, NICs):
NIC = NIC[NICs] NIC = NIC[NICs]
try: try:
Rc = uncertainty.NICv1[NIC][NICs]['Rc'] Rc = uncertainty.NICv1[NIC][NICs]["Rc"]
VPL = uncertainty.NICv1[NIC][NICs]['VPL'] VPL = uncertainty.NICv1[NIC][NICs]["VPL"]
except KeyError: except KeyError:
Rc, VPL = uncertainty.NA, uncertainty.NA Rc, VPL = uncertainty.NA, uncertainty.NA
@@ -324,7 +321,7 @@ def nic_v2(msg, NICa, NICbc):
"""Calculate NIC, navigation integrity category, for ADS-B version 2 """Calculate NIC, navigation integrity category, for ADS-B version 2
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
NICa (int or string): NIC supplement - A NICa (int or string): NIC supplement - A
NICbc (int or srting): NIC supplement - B or C NICbc (int or srting): NIC supplement - B or C
@@ -335,22 +332,23 @@ def nic_v2(msg, NICa, NICbc):
raise RuntimeError( raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \ "%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \ airborne position message (8<TC<19), \
or airborne position with GNSS height (20<TC<22)" % msg or airborne position with GNSS height (20<TC<22)"
% msg
) )
tc = typecode(msg) tc = typecode(msg)
NIC = uncertainty.TC_NICv2_lookup[tc] NIC = uncertainty.TC_NICv2_lookup[tc]
if 20<=tc<=22: if 20 <= tc <= 22:
NICs = 0 NICs = 0
else: else:
NICs = NICa*2 + NICbc NICs = NICa * 2 + NICbc
try: try:
if isinstance(NIC, dict): if isinstance(NIC, dict):
NIC = NIC[NICs] NIC = NIC[NICs]
Rc = uncertainty.NICv2[NIC][NICs]['Rc'] Rc = uncertainty.NICv2[NIC][NICs]["Rc"]
except KeyError: except KeyError:
Rc = uncertainty.NA Rc = uncertainty.NA
@@ -361,7 +359,7 @@ def nic_s(msg):
"""Obtain NIC supplement bit, TC=31 message """Obtain NIC supplement bit, TC=31 message
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: NICs number (0 or 1) int: NICs number (0 or 1)
@@ -369,7 +367,9 @@ def nic_s(msg):
tc = typecode(msg) tc = typecode(msg)
if tc != 31: if tc != 31:
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg) raise RuntimeError(
"%s: Not a status operation message, expecting TC = 31" % msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
nic_s = int(msgbin[75]) nic_s = int(msgbin[75])
@@ -381,7 +381,7 @@ def nic_a_c(msg):
"""Obtain NICa/c, navigation integrity category supplements a and c """Obtain NICa/c, navigation integrity category supplements a and c
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
(int, int): NICa and NICc number (0 or 1) (int, int): NICa and NICc number (0 or 1)
@@ -389,7 +389,9 @@ def nic_a_c(msg):
tc = typecode(msg) tc = typecode(msg)
if tc != 31: if tc != 31:
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg) raise RuntimeError(
"%s: Not a status operation message, expecting TC = 31" % msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
nic_a = int(msgbin[75]) nic_a = int(msgbin[75])
@@ -402,7 +404,7 @@ def nic_b(msg):
"""Obtain NICb, navigation integrity category supplement-b """Obtain NICb, navigation integrity category supplement-b
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: NICb number (0 or 1) int: NICb number (0 or 1)
@@ -410,7 +412,9 @@ def nic_b(msg):
tc = typecode(msg) tc = typecode(msg)
if tc < 9 or tc > 18: if tc < 9 or tc > 18:
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg) raise RuntimeError(
"%s: Not a airborne position message, expecting 8<TC<19" % msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
nic_b = int(msgbin[39]) nic_b = int(msgbin[39])
@@ -422,7 +426,7 @@ def nac_p(msg):
"""Calculate NACp, Navigation Accuracy Category - Position """Calculate NACp, Navigation Accuracy Category - Position
Args: Args:
msg (string): 28 bytes hexadecimal message string, TC = 29 or 31 msg (str): 28 hexdigits string, TC = 29 or 31
Returns: Returns:
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
@@ -431,8 +435,11 @@ def nac_p(msg):
tc = typecode(msg) tc = typecode(msg)
if tc not in [29, 31]: if tc not in [29, 31]:
raise RuntimeError("%s: Not a target state and status message, \ raise RuntimeError(
or operation status message, expecting TC = 29 or 31" % msg) "%s: Not a target state and status message, \
or operation status message, expecting TC = 29 or 31"
% msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
@@ -442,8 +449,8 @@ def nac_p(msg):
NACp = common.bin2int(msgbin[76:80]) NACp = common.bin2int(msgbin[76:80])
try: try:
EPU = uncertainty.NACp[NACp]['EPU'] EPU = uncertainty.NACp[NACp]["EPU"]
VEPU = uncertainty.NACp[NACp]['VEPU'] VEPU = uncertainty.NACp[NACp]["VEPU"]
except KeyError: except KeyError:
EPU, VEPU = uncertainty.NA, uncertainty.NA EPU, VEPU = uncertainty.NA, uncertainty.NA
@@ -454,7 +461,7 @@ def nac_v(msg):
"""Calculate NACv, Navigation Accuracy Category - Velocity """Calculate NACv, Navigation Accuracy Category - Velocity
Args: Args:
msg (string): 28 bytes hexadecimal message string, TC = 19 msg (str): 28 hexdigits string, TC = 19
Returns: Returns:
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
@@ -463,14 +470,16 @@ def nac_v(msg):
tc = typecode(msg) tc = typecode(msg)
if tc != 19: if tc != 19:
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg) raise RuntimeError(
"%s: Not an airborne velocity message, expecting TC = 19" % msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
NACv = common.bin2int(msgbin[42:45]) NACv = common.bin2int(msgbin[42:45])
try: try:
HFOMr = uncertainty.NACv[NACv]['HFOMr'] HFOMr = uncertainty.NACv[NACv]["HFOMr"]
VFOMr = uncertainty.NACv[NACv]['VFOMr'] VFOMr = uncertainty.NACv[NACv]["VFOMr"]
except KeyError: except KeyError:
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
@@ -481,7 +490,7 @@ def sil(msg, version):
"""Calculate SIL, Surveillance Integrity Level """Calculate SIL, Surveillance Integrity Level
Args: Args:
msg (string): 28 bytes hexadecimal message string with TC = 29, 31 msg (str): 28 hexdigits string with TC = 29, 31
Returns: Returns:
int or string: Probability of exceeding Horizontal Radius of Containment RCu int or string: Probability of exceeding Horizontal Radius of Containment RCu
@@ -491,8 +500,11 @@ def sil(msg, version):
tc = typecode(msg) tc = typecode(msg)
if tc not in [29, 31]: if tc not in [29, 31]:
raise RuntimeError("%s: Not a target state and status messag, \ raise RuntimeError(
or operation status message, expecting TC = 29 or 31" % msg) "%s: Not a target state and status message, \
or operation status message, expecting TC = 29 or 31"
% msg
)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
@@ -502,12 +514,12 @@ def sil(msg, version):
SIL = common.bin2int(msgbin[82:84]) SIL = common.bin2int(msgbin[82:84])
try: try:
PE_RCu = uncertainty.SIL[SIL]['PE_RCu'] PE_RCu = uncertainty.SIL[SIL]["PE_RCu"]
PE_VPL = uncertainty.SIL[SIL]['PE_VPL'] PE_VPL = uncertainty.SIL[SIL]["PE_VPL"]
except KeyError: except KeyError:
PE_RCu, PE_VPL = uncertainty.NA, uncertainty.NA PE_RCu, PE_VPL = uncertainty.NA, uncertainty.NA
base = 'unknown' base = "unknown"
if version == 2: if version == 2:
if tc == 29: if tc == 29:

View File

@@ -1,23 +1,5 @@
# 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 Decoding all call replies DF=11
[To be implemented] [To be implemented]
""" """
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common

View File

@@ -18,20 +18,34 @@
Common functions for Mode-S decoding Common functions for Mode-S decoding
""" """
from __future__ import absolute_import, print_function, division
import numpy as np import numpy as np
from pyModeS.extra import aero from pyModeS.extra import aero
from pyModeS.decoder import common from pyModeS import common
from pyModeS.decoder.bds import bds05, bds06, bds08, bds09, \
bds10, bds17, bds20, bds30, bds40, bds44, bds45, bds50, bds53, bds60 from pyModeS.decoder.bds import (
bds05,
bds06,
bds08,
bds09,
bds10,
bds17,
bds20,
bds30,
bds40,
bds44,
bds45,
bds50,
bds53,
bds60,
)
def is50or60(msg, spd_ref, trk_ref, alt_ref): def is50or60(msg, spd_ref, trk_ref, alt_ref):
"""Use reference ground speed and trk to determine BDS50 and DBS60. """Use reference ground speed and trk to determine BDS50 and DBS60.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
spd_ref (float): reference speed (ADS-B ground speed), kts spd_ref (float): reference speed (ADS-B ground speed), kts
trk_ref (float): reference track (ADS-B track angle), deg trk_ref (float): reference track (ADS-B track angle), deg
alt_ref (float): reference altitude (ADS-B altitude), ft alt_ref (float): reference altitude (ADS-B altitude), ft
@@ -40,6 +54,7 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
String or None: BDS version, or possible versions, or None if nothing matches. String or None: BDS version, or possible versions, or None if nothing matches.
""" """
def vxy(v, angle): def vxy(v, angle):
vx = v * np.sin(np.radians(angle)) vx = v * np.sin(np.radians(angle))
vy = v * np.cos(np.radians(angle)) vy = v * np.cos(np.radians(angle))
@@ -52,26 +67,26 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
v50 = bds50.gs50(msg) v50 = bds50.gs50(msg)
if h50 is None or v50 is None: if h50 is None or v50 is None:
return 'BDS50,BDS60' return "BDS50,BDS60"
h60 = bds60.hdg60(msg) h60 = bds60.hdg60(msg)
m60 = bds60.mach60(msg) m60 = bds60.mach60(msg)
i60 = bds60.ias60(msg) i60 = bds60.ias60(msg)
if h60 is None or (m60 is None and i60 is None): if h60 is None or (m60 is None and i60 is None):
return 'BDS50,BDS60' return "BDS50,BDS60"
m60 = np.nan if m60 is None else m60 m60 = np.nan if m60 is None else m60
i60 = np.nan if i60 is None else i60 i60 = np.nan if i60 is None else i60
XY5 = vxy(v50*aero.kts, h50) XY5 = vxy(v50 * aero.kts, h50)
XY6m = vxy(aero.mach2tas(m60, alt_ref*aero.ft), h60) XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
XY6i = vxy(aero.cas2tas(i60*aero.kts, alt_ref*aero.ft), h60) XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
allbds = ['BDS50', 'BDS60', 'BDS60'] allbds = ["BDS50", "BDS60", "BDS60"]
X = np.array([XY5, XY6m, XY6i]) X = np.array([XY5, XY6m, XY6i])
Mu = np.array(vxy(spd_ref*aero.kts, trk_ref)) Mu = np.array(vxy(spd_ref * aero.kts, trk_ref))
# compute Mahalanobis distance matrix # compute Mahalanobis distance matrix
# Cov = [[20**2, 0], [0, 20**2]] # Cov = [[20**2, 0], [0, 20**2]]
@@ -81,10 +96,10 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
# since the covariance matrix is identity matrix, # since the covariance matrix is identity matrix,
# M-dist is same as eculidian distance # M-dist is same as eculidian distance
try: try:
dist = np.linalg.norm(X-Mu, axis=1) dist = np.linalg.norm(X - Mu, axis=1)
BDS = allbds[np.nanargmin(dist)] BDS = allbds[np.nanargmin(dist)]
except ValueError: except ValueError:
return 'BDS50,BDS60' return "BDS50,BDS60"
return BDS return BDS
@@ -93,7 +108,7 @@ def infer(msg, mrar=False):
"""Estimate the most likely BDS code of an message. """Estimate the most likely BDS code of an message.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45). Defaults to False. mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45). Defaults to False.
Returns: Returns:
@@ -103,28 +118,28 @@ def infer(msg, mrar=False):
df = common.df(msg) df = common.df(msg)
if common.allzeros(msg): if common.allzeros(msg):
return 'EMPTY' return "EMPTY"
# For ADS-B / Mode-S extended squitter # For ADS-B / Mode-S extended squitter
if df == 17: if df == 17:
tc = common.typecode(msg) tc = common.typecode(msg)
if 1 <= tc <= 4: if 1 <= tc <= 4:
return 'BDS08' # indentification and category return "BDS08" # identification and category
if 5 <= tc <= 8: if 5 <= tc <= 8:
return 'BDS06' # surface movement return "BDS06" # surface movement
if 9 <= tc <= 18: if 9 <= tc <= 18:
return 'BDS05' # airborne position, baro-alt return "BDS05" # airborne position, baro-alt
if tc == 19: if tc == 19:
return 'BDS09' # airborne velocity return "BDS09" # airborne velocity
if 20 <= tc <= 22: if 20 <= tc <= 22:
return 'BDS05' # airborne position, gnss-alt return "BDS05" # airborne position, gnss-alt
if tc == 28: if tc == 28:
return 'BDS61' # aircraft status return "BDS61" # aircraft status
if tc == 29: if tc == 29:
return 'BDS62' # target state and status return "BDS62" # target state and status
if tc == 31: if tc == 31:
return 'BDS65' # operational status return "BDS65" # operational status
# For Comm-B replies # For Comm-B replies
IS10 = bds10.is10(msg) IS10 = bds10.is10(msg)
@@ -138,15 +153,27 @@ def infer(msg, mrar=False):
IS45 = bds45.is45(msg) IS45 = bds45.is45(msg)
if mrar: if mrar:
allbds = np.array(["BDS10", "BDS17", "BDS20", "BDS30", "BDS40", allbds = np.array(
"BDS44", "BDS45", "BDS50", "BDS60"]) [
"BDS10",
"BDS17",
"BDS20",
"BDS30",
"BDS40",
"BDS44",
"BDS45",
"BDS50",
"BDS60",
]
)
mask = [IS10, IS17, IS20, IS30, IS40, IS44, IS45, IS50, IS60] mask = [IS10, IS17, IS20, IS30, IS40, IS44, IS45, IS50, IS60]
else: else:
allbds = np.array(["BDS10", "BDS17", "BDS20", "BDS30", "BDS40", allbds = np.array(
"BDS50", "BDS60"]) ["BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60"]
)
mask = [IS10, IS17, IS20, IS30, IS40, IS50, IS60] mask = [IS10, IS17, IS20, IS30, IS40, IS50, IS60]
bds = ','.join(sorted(allbds[mask])) bds = ",".join(sorted(allbds[mask]))
if len(bds) == 0: if len(bds) == 0:
return None return None

View File

@@ -1,35 +1,18 @@
# 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 # BDS 0,5
# ADS-B TC=9-18 # ADS-B TC=9-18
# Airborn position # Airborn position
# ------------------------------------------ # ------------------------------------------
from pyModeS import common
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
def airborne_position(msg0, msg1, t0, t1): def airborne_position(msg0, msg1, t0, t1):
"""Decode airborn position from a pair of even and odd position message """Decode airborn position from a pair of even and odd position message
Args: Args:
msg0 (string): even message (28 bytes hexadecimal string) msg0 (string): even message (28 hexdigits)
msg1 (string): odd message (28 bytes hexadecimal string) msg1 (string): odd message (28 hexdigits)
t0 (int): timestamps for the even message t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message t1 (int): timestamps for the odd message
@@ -40,6 +23,16 @@ def airborne_position(msg0, msg1, t0, t1):
mb0 = common.hex2bin(msg0)[32:] mb0 = common.hex2bin(msg0)[32:]
mb1 = common.hex2bin(msg1)[32:] mb1 = common.hex2bin(msg1)[32:]
oe0 = int(mb0[21])
oe1 = int(mb1[21])
if oe0 == 0 and oe1 == 1:
pass
elif oe0 == 1 and oe1 == 0:
mb0, mb1 = mb1, mb0
t0, t1 = t1, t0
else:
raise RuntimeError("Both even and odd CPR frames are required.")
# 131072 is 2^17, since CPR lat and lon are 17 bits each. # 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0 cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0 cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
@@ -66,17 +59,17 @@ def airborne_position(msg0, msg1, t0, t1):
return None return None
# compute ni, longitude index m, and longitude # compute ni, longitude index m, and longitude
if (t0 > t1): if t0 > t1:
lat = lat_even lat = lat_even
nl = common.cprNL(lat) nl = common.cprNL(lat)
ni = max(common.cprNL(lat)- 0, 1) ni = max(common.cprNL(lat) - 0, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even) lon = (360.0 / ni) * (m % ni + cprlon_even)
else: else:
lat = lat_odd lat = lat_odd
nl = common.cprNL(lat) nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 1, 1) ni = max(common.cprNL(lat) - 1, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_odd) lon = (360.0 / ni) * (m % ni + cprlon_odd)
if lon > 180: if lon > 180:
@@ -92,7 +85,7 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
be with in 180NM of the true position. be with in 180NM of the true position.
Args: Args:
msg (string): even message (28 bytes hexadecimal string) msg (str): even message (28 hexdigits)
lat_ref: previous known latitude lat_ref: previous known latitude
lon_ref: previous known longitude lon_ref: previous known longitude
@@ -100,17 +93,17 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
(float, float): (latitude, longitude) of the aircraft (float, float): (latitude, longitude) of the aircraft
""" """
mb = common.hex2bin(msg)[32:] mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072.0 cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0 cprlon = common.bin2int(mb[39:56]) / 131072.0
i = int(mb[21]) i = int(mb[21])
d_lat = 360.0/59 if i else 360.0/60 d_lat = 360.0 / 59 if i else 360.0 / 60
j = common.floor(lat_ref / d_lat) \ j = common.floor(lat_ref / d_lat) + common.floor(
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat) 0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
)
lat = d_lat * (j + cprlat) lat = d_lat * (j + cprlat)
@@ -121,8 +114,9 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
else: else:
d_lon = 360.0 d_lon = 360.0
m = common.floor(lon_ref / d_lon) \ m = common.floor(lon_ref / d_lon) + common.floor(
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon) 0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
)
lon = d_lon * (m + cprlon) lon = d_lon * (m + cprlon)
@@ -133,7 +127,7 @@ def altitude(msg):
"""Decode aircraft altitude """Decode aircraft altitude
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: altitude in feet int: altitude in feet
@@ -141,7 +135,7 @@ def altitude(msg):
tc = common.typecode(msg) tc = common.typecode(msg)
if tc<9 or tc==19 or tc>22: if tc < 9 or tc == 19 or tc > 22:
raise RuntimeError("%s: Not a airborn position message" % msg) raise RuntimeError("%s: Not a airborn position message" % msg)
mb = common.hex2bin(msg)[32:] mb = common.hex2bin(msg)[32:]
@@ -150,7 +144,7 @@ def altitude(msg):
# barometric altitude # barometric altitude
q = mb[15] q = mb[15]
if q: if q:
n = common.bin2int(mb[8:15]+mb[16:20]) n = common.bin2int(mb[8:15] + mb[16:20])
alt = n * 25 - 1000 alt = n * 25 - 1000
else: else:
alt = None alt = None

View File

@@ -1,28 +1,10 @@
# 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 # BDS 0,6
# ADS-B TC=5-8 # ADS-B TC=5-8
# Surface position # Surface movment
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder import common
import math
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref): def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
@@ -30,8 +12,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
the lat/lon of receiver must be provided to yield the correct solution. the lat/lon of receiver must be provided to yield the correct solution.
Args: Args:
msg0 (string): even message (28 bytes hexadecimal string) msg0 (string): even message (28 hexdigits)
msg1 (string): odd message (28 bytes hexadecimal string) msg1 (string): odd message (28 hexdigits)
t0 (int): timestamps for the even message t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message t1 (int): timestamps for the odd message
lat_ref (float): latitude of the receiver lat_ref (float): latitude of the receiver
@@ -73,22 +55,25 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
return None return None
# compute ni, longitude index m, and longitude # compute ni, longitude index m, and longitude
if (t0 > t1): if t0 > t1:
lat = lat_even lat = lat_even
nl = common.cprNL(lat_even) nl = common.cprNL(lat_even)
ni = max(common.cprNL(lat_even) - 0, 1) ni = max(common.cprNL(lat_even) - 0, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_even) lon = (90.0 / ni) * (m % ni + cprlon_even)
else: else:
lat = lat_odd lat = lat_odd
nl = common.cprNL(lat_odd) nl = common.cprNL(lat_odd)
ni = max(common.cprNL(lat_odd) - 1, 1) ni = max(common.cprNL(lat_odd) - 1, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5) m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_odd) lon = (90.0 / ni) * (m % ni + cprlon_odd)
# four possible longitude solutions # four possible longitude solutions
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0] lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
# make sure lons are between -180 and 180
lons = [(l + 180) % 360 - 180 for l in lons]
# the closest solution to receiver is the correct one # the closest solution to receiver is the correct one
dls = [abs(lon_ref - l) for l in lons] dls = [abs(lon_ref - l) for l in lons]
imin = min(range(4), key=dls.__getitem__) imin = min(range(4), key=dls.__getitem__)
@@ -104,7 +89,7 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
be with in 45NM of the true position. be with in 45NM of the true position.
Args: Args:
msg (string): even message (28 bytes hexadecimal string) msg (str): even message (28 hexdigits)
lat_ref: previous known latitude lat_ref: previous known latitude
lon_ref: previous known longitude lon_ref: previous known longitude
@@ -112,17 +97,17 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
(float, float): (latitude, longitude) of the aircraft (float, float): (latitude, longitude) of the aircraft
""" """
mb = common.hex2bin(msg)[32:] mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072.0 cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0 cprlon = common.bin2int(mb[39:56]) / 131072.0
i = int(mb[21]) i = int(mb[21])
d_lat = 90.0/59 if i else 90.0/60 d_lat = 90.0 / 59 if i else 90.0 / 60
j = common.floor(lat_ref / d_lat) \ j = common.floor(lat_ref / d_lat) + common.floor(
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat) 0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
)
lat = d_lat * (j + cprlat) lat = d_lat * (j + cprlat)
@@ -133,31 +118,33 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
else: else:
d_lon = 90.0 d_lon = 90.0
m = common.floor(lon_ref / d_lon) \ m = common.floor(lon_ref / d_lon) + common.floor(
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon) 0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
)
lon = d_lon * (m + cprlon) lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5) return round(lat, 5), round(lon, 5)
def surface_velocity(msg, rtn_sources=False): def surface_velocity(msg, source=False):
"""Decode surface velocity from from a surface position message """Decode surface velocity from a surface position message
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
rtn_source (boolean): If the function will return source (boolean): Include direction and vertical rate sources in return. Default to False.
the sources for direction of travel and vertical If set to True, the function will return six value instead of four.
rate. This will change the return value from a four
element array to a six element array.
Returns: Returns:
(int, float, int, string, string, None): speed (kt), int, float, int, string, [string], [string]: Four or six parameters, including:
ground track (degree), None for rate of climb/descend (ft/min), - Speed (kt)
and speed type ('GS' for ground speed), direction source - Angle (degree), ground track
('true_north' for ground track / true north as reference), - Vertical rate, always 0
None rate of climb/descent source. - Speed type ('GS' for ground speed, 'AS' for airspeed)
""" - [Optional] Direction source ('TRUE_NORTH')
- [Optional] Vertical rate source (None)
"""
if common.typecode(msg) < 5 or common.typecode(msg) > 8: if common.typecode(msg) < 5 or common.typecode(msg) > 8:
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg) raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
@@ -171,7 +158,7 @@ def surface_velocity(msg, rtn_sources=False):
else: else:
trk = None trk = None
# ground movment / speed # ground movement / speed
mov = common.bin2int(mb[5:12]) mov = common.bin2int(mb[5:12])
if mov == 0 or mov > 124: if mov == 0 or mov > 124:
@@ -184,11 +171,11 @@ def surface_velocity(msg, rtn_sources=False):
movs = [2, 9, 13, 39, 94, 109, 124] movs = [2, 9, 13, 39, 94, 109, 124]
kts = [0.125, 1, 2, 15, 70, 100, 175] kts = [0.125, 1, 2, 15, 70, 100, 175]
i = next(m[0] for m in enumerate(movs) if m[1] > mov) 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]) step = (kts[i] - kts[i - 1]) * 1.0 / (movs[i] - movs[i - 1])
spd = kts[i-1] + (mov-movs[i-1]) * step spd = kts[i - 1] + (mov - movs[i - 1]) * step
spd = round(spd, 2) spd = round(spd, 2)
if rtn_sources: if source:
return spd, trk, 0, 'GS', 'true_north', None return spd, trk, 0, "GS", "TRUE_NORTH", None
else: else:
return spd, trk, 0, 'GS' return spd, trk, 0, "GS"

View File

@@ -1,33 +1,17 @@
# 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 # BDS 0,8
# ADS-B TC=1-4 # ADS-B TC=1-4
# Aircraft identitification and category # Aircraft identitification and category
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder import common
def category(msg): def category(msg):
"""Aircraft category number """Aircraft category number
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: category number int: category number
@@ -37,14 +21,15 @@ def category(msg):
raise RuntimeError("%s: Not a identification message" % msg) raise RuntimeError("%s: Not a identification message" % msg)
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
return common.bin2int(msgbin[5:8]) mebin = msgbin[32:87]
return common.bin2int(mebin[5:8])
def callsign(msg): def callsign(msg):
"""Aircraft callsign """Aircraft callsign
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
string: callsign string: callsign
@@ -53,11 +38,11 @@ def callsign(msg):
if common.typecode(msg) < 1 or common.typecode(msg) > 4: if common.typecode(msg) < 1 or common.typecode(msg) > 4:
raise RuntimeError("%s: Not a identification message" % msg) raise RuntimeError("%s: Not a identification message" % msg)
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######' chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
msgbin = common.hex2bin(msg) msgbin = common.hex2bin(msg)
csbin = msgbin[40:96] csbin = msgbin[40:96]
cs = '' cs = ""
cs += chars[common.bin2int(csbin[0:6])] cs += chars[common.bin2int(csbin[0:6])]
cs += chars[common.bin2int(csbin[6:12])] cs += chars[common.bin2int(csbin[6:12])]
cs += chars[common.bin2int(csbin[12:18])] cs += chars[common.bin2int(csbin[12:18])]
@@ -69,5 +54,5 @@ def callsign(msg):
# clean string, remove spaces and marks, if any. # clean string, remove spaces and marks, if any.
# cs = cs.replace('_', '') # cs = cs.replace('_', '')
cs = cs.replace('#', '') cs = cs.replace("#", "")
return cs return cs

View File

@@ -1,51 +1,33 @@
# 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 # BDS 0,9
# ADS-B TC=19 # ADS-B TC=19
# Aircraft Airborn velocity # Aircraft Airborn velocity
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder import common
import math import math
def airborne_velocity(msg, rtn_sources=False): def airborne_velocity(msg, source=False):
"""Calculate the speed, track (or heading), and vertical rate """Decode airborne velocity.
Args: Args:
msg (string): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
rtn_source (boolean): If the function will return source (boolean): Include direction and vertical rate sources in return. Default to False.
the sources for direction of travel and vertical If set to True, the function will return six value instead of four.
rate. This will change the return value from a four
element array to a six element array.
Returns: Returns:
(int, float, int, string, string, string): speed (kt), int, float, int, string, [string], [string]: Four or six parameters, including:
ground track or heading (degree), - Speed (kt)
rate of climb/descent (ft/min), speed type - Angle (degree), either ground track or heading
('GS' for ground speed, 'AS' for airspeed), - Vertical rate (ft/min)
direction source ('true_north' for ground track / true north - Speed type ('GS' for ground speed, 'AS' for airspeed)
as refrence, 'mag_north' for magnetic north as reference), - [Optional] Direction source ('TRUE_NORTH' or 'MAGENTIC_NORTH')
rate of climb/descent source ('Baro' for barometer, 'GNSS' - [Optional] Vertical rate source ('BARO' or 'GNSS')
for GNSS constellation).
"""
"""
if common.typecode(msg) != 19: if common.typecode(msg) != 19:
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg) raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
@@ -57,12 +39,12 @@ def airborne_velocity(msg, rtn_sources=False):
return None return None
if subtype in (1, 2): if subtype in (1, 2):
v_ew_sign = -1 if mb[13]=='1' else 1 v_ew_sign = -1 if mb[13] == "1" else 1
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
if subtype == 2: # Supersonic if subtype == 2: # Supersonic
v_ew *= 4 v_ew *= 4
v_ns_sign = -1 if mb[24]=='1' else 1 v_ns_sign = -1 if mb[24] == "1" else 1
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
if subtype == 2: # Supersonic if subtype == 2: # Supersonic
v_ns *= 4 v_ns *= 4
@@ -70,19 +52,19 @@ def airborne_velocity(msg, rtn_sources=False):
v_we = v_ew_sign * v_ew v_we = v_ew_sign * v_ew
v_sn = v_ns_sign * v_ns v_sn = v_ns_sign * v_ns
spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
spd = int(spd) spd = int(spd)
trk = math.atan2(v_we, v_sn) trk = math.atan2(v_we, v_sn)
trk = math.degrees(trk) # convert to degrees trk = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val trk = trk if trk >= 0 else trk + 360 # no negative val
tag = 'GS' spd_type = "GS"
trk_or_hdg = round(trk, 2) trk_or_hdg = round(trk, 2)
dir_type = 'true_north' dir_type = "TRUE_NORTH"
else: else:
if mb[13] == '0': if mb[13] == "0":
hdg = None hdg = None
else: else:
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0 hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
@@ -91,37 +73,38 @@ def airborne_velocity(msg, rtn_sources=False):
trk_or_hdg = hdg trk_or_hdg = hdg
spd = common.bin2int(mb[25:35]) spd = common.bin2int(mb[25:35])
spd = None if spd==0 else spd-1 spd = None if spd == 0 else spd - 1
if subtype == 4: # Supersonic if subtype == 4: # Supersonic
spd *= 4 spd *= 4
if mb[24]=='0': if mb[24] == "0":
tag = 'IAS' spd_type = "IAS"
else: else:
tag = 'TAS' spd_type = "TAS"
dir_type = 'mag_north' dir_type = "MAGENTIC_NORTH"
vr_source = 'GNSS' if mb[35]=='0' else 'Baro' vr_source = "GNSS" if mb[35] == "0" else "BARO"
vr_sign = -1 if mb[36]=='1' else 1 vr_sign = -1 if mb[36] == "1" else 1
vr = common.bin2int(mb[37:46]) vr = common.bin2int(mb[37:46])
rocd = None if vr==0 else int(vr_sign*(vr-1)*64) vs = None if vr == 0 else int(vr_sign * (vr - 1) * 64)
if rtn_sources: if source:
return spd, trk_or_hdg, rocd, tag, dir_type, vr_source return spd, trk_or_hdg, vs, spd_type, dir_type, vr_source
else: else:
return spd, trk_or_hdg, rocd, tag return spd, trk_or_hdg, vs, spd_type
def altitude_diff(msg): def altitude_diff(msg):
"""Decode the differece between GNSS and barometric altitude """Decode the differece between GNSS and barometric altitude.
Args: Args:
msg (string): 28 bytes hexadecimal message string, TC=19 msg (str): 28 hexdigits string, TC=19
Returns: Returns:
int: Altitude difference in ft. Negative value indicates GNSS altitude int: Altitude difference in feet. Negative value indicates GNSS altitude
below barometric altitude. below barometric altitude.
""" """
tc = common.typecode(msg) tc = common.typecode(msg)

View File

@@ -1,66 +1,52 @@
# 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 1,0 # BDS 1,0
# Data link capability report # Data link capability report
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
def is10(msg): def is10(msg):
"""Check if a message is likely to be BDS code 1,0 """Check if a message is likely to be BDS code 1,0
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# first 8 bits must be 0x10 # first 8 bits must be 0x10
if d[0:8] != '00010000': if d[0:8] != "00010000":
return False return False
# bit 10 to 14 are reserved # bit 10 to 14 are reserved
if bin2int(d[9:14]) != 0: if common.bin2int(d[9:14]) != 0:
return False return False
# overlay capabilty conflict # overlay capability conflict
if d[14] == '1' and bin2int(d[16:23]) < 5: if d[14] == "1" and common.bin2int(d[16:23]) < 5:
return False return False
if d[14] == '0' and bin2int(d[16:23]) > 4: if d[14] == "0" and common.bin2int(d[16:23]) > 4:
return False return False
return True return True
def ovc10(msg): def ovc10(msg):
"""Return the overlay control capability """Return the overlay control capability
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: Whether the transponder is OVC capable int: Whether the transponder is OVC capable
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
return int(d[14]) return int(d[14])

View File

@@ -1,45 +1,27 @@
# 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 1,7 # BDS 1,7
# Common usage GICB capability report # Common usage GICB capability report
# ------------------------------------------ # ------------------------------------------
from pyModeS import common
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
def is17(msg): def is17(msg):
"""Check if a message is likely to be BDS code 1,7 """Check if a message is likely to be BDS code 1,7
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if bin2int(d[28:56]) != 0: if common.bin2int(d[28:56]) != 0:
return False return False
caps = cap17(msg) caps = cap17(msg)
@@ -50,26 +32,54 @@ def is17(msg):
# return False # return False
# at least you can respond who you are # at least you can respond who you are
if 'BDS20' not in caps: if "BDS20" not in caps:
return False return False
return True return True
def cap17(msg): def cap17(msg):
"""Extract capacities from BDS 1,7 message """Extract capacities from BDS 1,7 message
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
list: list of suport BDS codes list: list of support BDS codes
""" """
allbds = ['05', '06', '07', '08', '09', '0A', '20', '21', '40', '41', allbds = [
'42', '43', '44', '45', '48', '50', '51', '52', '53', '54', "05",
'55', '56', '5F', '60', 'NA', 'NA', 'E1', 'E2'] "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)) d = common.hex2bin(common.data(msg))
idx = [i for i, v in enumerate(d[:28]) if v=='1'] 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'] capacity = ["BDS" + allbds[i] for i in idx if allbds[i] is not "NA"]
return capacity return capacity

View File

@@ -1,47 +1,32 @@
# 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 2,0 # BDS 2,0
# Aircraft identification # Aircraft identification
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
def is20(msg): def is20(msg):
"""Check if a message is likely to be BDS code 2,0 """Check if a message is likely to be BDS code 2,0
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0:8] != '00100000': if d[0:8] != "00100000":
return False return False
cs = cs20(msg) cs = cs20(msg)
if '#' in cs: if "#" in cs:
return False return False
return True return True
@@ -51,23 +36,23 @@ def cs20(msg):
"""Aircraft callsign """Aircraft callsign
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS40) string msg (str): 28 hexdigits string
Returns: Returns:
string: callsign, max. 8 chars string: callsign, max. 8 chars
""" """
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######' chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
cs = '' cs = ""
cs += chars[bin2int(d[8:14])] cs += chars[common.bin2int(d[8:14])]
cs += chars[bin2int(d[14:20])] cs += chars[common.bin2int(d[14:20])]
cs += chars[bin2int(d[20:26])] cs += chars[common.bin2int(d[20:26])]
cs += chars[bin2int(d[26:32])] cs += chars[common.bin2int(d[26:32])]
cs += chars[bin2int(d[32:38])] cs += chars[common.bin2int(d[32:38])]
cs += chars[bin2int(d[38:44])] cs += chars[common.bin2int(d[38:44])]
cs += chars[bin2int(d[44:50])] cs += chars[common.bin2int(d[44:50])]
cs += chars[bin2int(d[50:56])] cs += chars[common.bin2int(d[50:56])]
return cs return cs

View File

@@ -1,50 +1,35 @@
# 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 3,0 # BDS 3,0
# ACAS active resolution advisory # ACAS active resolution advisory
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
def is30(msg): def is30(msg):
"""Check if a message is likely to be BDS code 2,0 """Check if a message is likely to be BDS code 2,0
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0:8] != '00110000': if d[0:8] != "00110000":
return False return False
# threat type 3 not assigned # threat type 3 not assigned
if d[28:30] == '11': if d[28:30] == "11":
return False return False
# reserved for ACAS III, in far future # reserved for ACAS III, in far future
if bin2int(d[15:22]) >= 48: if common.bin2int(d[15:22]) >= 48:
return False return False
return True return True

View File

@@ -1,66 +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/>.
# ------------------------------------------ # ------------------------------------------
# BDS 4,0 # BDS 4,0
# Selected vertical intention # Selected vertical intention
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division
import warnings import warnings
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus from pyModeS import common
def is40(msg): def is40(msg):
"""Check if a message is likely to be BDS code 4,0 """Check if a message is likely to be BDS code 4,0
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# status bit 1, 14, and 27 # status bit 1, 14, and 27
if wrongstatus(d, 1, 2, 13): if common.wrongstatus(d, 1, 2, 13):
return False return False
if wrongstatus(d, 14, 15, 26): if common.wrongstatus(d, 14, 15, 26):
return False return False
if wrongstatus(d, 27, 28, 39): if common.wrongstatus(d, 27, 28, 39):
return False return False
if wrongstatus(d, 48, 49, 51): if common.wrongstatus(d, 48, 49, 51):
return False return False
if wrongstatus(d, 54, 55, 56): if common.wrongstatus(d, 54, 55, 56):
return False return False
# bits 40-47 and 52-53 shall all be zero # bits 40-47 and 52-53 shall all be zero
if bin2int(d[39:47]) != 0: if common.bin2int(d[39:47]) != 0:
return False return False
if bin2int(d[51:53]) != 0: if common.bin2int(d[51:53]) != 0:
return False return False
return True return True
@@ -70,17 +54,17 @@ def selalt40mcp(msg):
"""Selected altitude, MCP/FCU """Selected altitude, MCP/FCU
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS40) string msg (str): 28 hexdigits string
Returns: Returns:
int: altitude in feet int: altitude in feet
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0] == "0": if d[0] == "0":
return None return None
alt = bin2int(d[1:13]) * 16 # ft alt = common.bin2int(d[1:13]) * 16 # ft
return alt return alt
@@ -88,17 +72,17 @@ def selalt40fms(msg):
"""Selected altitude, FMS """Selected altitude, FMS
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS40) string msg (str): 28 hexdigits string
Returns: Returns:
int: altitude in feet int: altitude in feet
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[13] == "0": if d[13] == "0":
return None return None
alt = bin2int(d[14:26]) * 16 # ft alt = common.bin2int(d[14:26]) * 16 # ft
return alt return alt
@@ -106,22 +90,21 @@ def p40baro(msg):
"""Barometric pressure setting """Barometric pressure setting
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS40) string msg (str): 28 hexdigits string
Returns: Returns:
float: pressure in millibar float: pressure in millibar
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[26] == "0": if d[26] == "0":
return None return None
p = bin2int(d[27:39]) * 0.1 + 800 # millibar p = common.bin2int(d[27:39]) * 0.1 + 800 # millibar
return p return p
def alt40mcp(msg): def alt40mcp(msg):
warnings.simplefilter("once", DeprecationWarning)
warnings.warn( warnings.warn(
"alt40mcp() has been renamed to selalt40mcp(). It will be removed in the future.", "alt40mcp() has been renamed to selalt40mcp(). It will be removed in the future.",
DeprecationWarning, DeprecationWarning,
@@ -130,7 +113,6 @@ def alt40mcp(msg):
def alt40fms(msg): def alt40fms(msg):
warnings.simplefilter("once", DeprecationWarning)
warnings.warn( warnings.warn(
"alt40fms() has been renamed to selalt40fms(). It will be removed in the future.", "alt40fms() has been renamed to selalt40fms(). It will be removed in the future.",
DeprecationWarning, DeprecationWarning,

View File

@@ -1,31 +1,9 @@
# 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 4,4 # BDS 4,4
# Meteorological routine air report # Meteorological routine air report
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import (
hex2bin,
bin2int,
data,
allzeros,
wrongstatus,
)
def is44(msg): def is44(msg):
@@ -34,32 +12,32 @@ def is44(msg):
Meteorological routine air report Meteorological routine air report
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# status bit 5, 35, 47, 50 # status bit 5, 35, 47, 50
if wrongstatus(d, 5, 6, 23): if common.wrongstatus(d, 5, 6, 23):
return False return False
if wrongstatus(d, 35, 36, 46): if common.wrongstatus(d, 35, 36, 46):
return False return False
if wrongstatus(d, 47, 48, 49): if common.wrongstatus(d, 47, 48, 49):
return False return False
if wrongstatus(d, 50, 51, 56): if common.wrongstatus(d, 50, 51, 56):
return False return False
# Bits 1-4 indicate source, values > 4 reserved and should not occur # Bits 1-4 indicate source, values > 4 reserved and should not occur
if bin2int(d[0:4]) > 4: if common.bin2int(d[0:4]) > 4:
return False return False
vw, dw = wind44(msg) vw, dw = wind44(msg)
@@ -77,20 +55,20 @@ def wind44(msg):
"""Wind speed and direction. """Wind speed and direction.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
(int, float): speed (kt), direction (degree) (int, float): speed (kt), direction (degree)
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
status = int(d[4]) status = int(d[4])
if not status: if not status:
return None, None return None, None
speed = bin2int(d[5:14]) # knots speed = common.bin2int(d[5:14]) # knots
direction = bin2int(d[14:23]) * 180.0 / 256.0 # degree direction = common.bin2int(d[14:23]) * 180.0 / 256.0 # degree
return round(speed, 0), round(direction, 1) return round(speed, 0), round(direction, 1)
@@ -99,18 +77,18 @@ def temp44(msg):
"""Static air temperature. """Static air temperature.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
float, float: temperature and alternative temperature in Celsius degree. float, float: temperature and alternative temperature in Celsius degree.
Note: Two values returns due to what seems to be an inconsistancy Note: Two values returns due to what seems to be an inconsistency
error in ICAO 9871 (2008) Appendix A-67. error in ICAO 9871 (2008) Appendix A-67.
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
sign = int(d[23]) sign = int(d[23])
value = bin2int(d[24:34]) value = common.bin2int(d[24:34])
if sign: if sign:
value = value - 1024 value = value - 1024
@@ -128,18 +106,18 @@ def p44(msg):
"""Static pressure. """Static pressure.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: static pressure in hPa int: static pressure in hPa
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[34] == "0": if d[34] == "0":
return None return None
p = bin2int(d[35:46]) # hPa p = common.bin2int(d[35:46]) # hPa
return p return p
@@ -148,17 +126,17 @@ def hum44(msg):
"""humidity """humidity
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
float: percentage of humidity, [0 - 100] % float: percentage of humidity, [0 - 100] %
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[49] == "0": if d[49] == "0":
return None return None
hm = bin2int(d[50:56]) * 100.0 / 64 # % hm = common.bin2int(d[50:56]) * 100.0 / 64 # %
return round(hm, 1) return round(hm, 1)
@@ -167,17 +145,17 @@ def turb44(msg):
"""Turblence. """Turblence.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe int: turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[46] == "0": if d[46] == "0":
return None return None
turb = bin2int(d[47:49]) turb = common.bin2int(d[47:49])
return turb return turb

View File

@@ -1,25 +1,9 @@
# 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 4,5 # BDS 4,5
# Meteorological hazard report # Meteorological hazard report
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
def is45(msg): def is45(msg):
@@ -28,44 +12,44 @@ def is45(msg):
Meteorological hazard report Meteorological hazard report
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# status bit 1, 4, 7, 10, 13, 16, 27, 39 # status bit 1, 4, 7, 10, 13, 16, 27, 39
if wrongstatus(d, 1, 2, 3): if common.wrongstatus(d, 1, 2, 3):
return False return False
if wrongstatus(d, 4, 5, 6): if common.wrongstatus(d, 4, 5, 6):
return False return False
if wrongstatus(d, 7, 8, 9): if common.wrongstatus(d, 7, 8, 9):
return False return False
if wrongstatus(d, 10, 11, 12): if common.wrongstatus(d, 10, 11, 12):
return False return False
if wrongstatus(d, 13, 14, 15): if common.wrongstatus(d, 13, 14, 15):
return False return False
if wrongstatus(d, 16, 17, 26): if common.wrongstatus(d, 16, 17, 26):
return False return False
if wrongstatus(d, 27, 28, 38): if common.wrongstatus(d, 27, 28, 38):
return False return False
if wrongstatus(d, 39, 40, 51): if common.wrongstatus(d, 39, 40, 51):
return False return False
# reserved # reserved
if bin2int(d[51:56]) != 0: if common.bin2int(d[51:56]) != 0:
return False return False
temp = temp45(msg) temp = temp45(msg)
@@ -80,17 +64,17 @@ def turb45(msg):
"""Turbulence. """Turbulence.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: Turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe int: Turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0] == '0': if d[0] == "0":
return None return None
turb = bin2int(d[1:3]) turb = common.bin2int(d[1:3])
return turb return turb
@@ -98,17 +82,17 @@ def ws45(msg):
"""Wind shear. """Wind shear.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: Wind shear level. 0=NIL, 1=Light, 2=Moderate, 3=Severe int: Wind shear level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[3] == '0': if d[3] == "0":
return None return None
ws = bin2int(d[4:6]) ws = common.bin2int(d[4:6])
return ws return ws
@@ -116,17 +100,17 @@ def mb45(msg):
"""Microburst. """Microburst.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: Microburst level. 0=NIL, 1=Light, 2=Moderate, 3=Severe int: Microburst level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[6] == '0': if d[6] == "0":
return None return None
mb = bin2int(d[7:9]) mb = common.bin2int(d[7:9])
return mb return mb
@@ -134,17 +118,17 @@ def ic45(msg):
"""Icing. """Icing.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: Icing level. 0=NIL, 1=Light, 2=Moderate, 3=Severe int: Icing level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[9] == '0': if d[9] == "0":
return None return None
ic = bin2int(d[10:12]) ic = common.bin2int(d[10:12])
return ic return ic
@@ -152,17 +136,17 @@ def wv45(msg):
"""Wake vortex. """Wake vortex.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: Wake vortex level. 0=NIL, 1=Light, 2=Moderate, 3=Severe int: Wake vortex level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[12] == '0': if d[12] == "0":
return None return None
ws = bin2int(d[13:15]) ws = common.bin2int(d[13:15])
return ws return ws
@@ -170,16 +154,16 @@ def temp45(msg):
"""Static air temperature. """Static air temperature.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
float: tmeperature in Celsius degree float: tmeperature in Celsius degree
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
sign = int(d[16]) sign = int(d[16])
value = bin2int(d[17:26]) value = common.bin2int(d[17:26])
if sign: if sign:
value = value - 512 value = value - 512
@@ -194,16 +178,16 @@ def p45(msg):
"""Average static pressure. """Average static pressure.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: static pressure in hPa int: static pressure in hPa
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[26] == '0': if d[26] == "0":
return None return None
p = bin2int(d[27:38]) # hPa p = common.bin2int(d[27:38]) # hPa
return p return p
@@ -211,14 +195,14 @@ def rh45(msg):
"""Radio height. """Radio height.
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
int: radio height in ft int: radio height in ft
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[38] == '0': if d[38] == "0":
return None return None
rh = bin2int(d[39:51]) * 16 rh = common.bin2int(d[39:51]) * 16
return rh return rh

View File

@@ -1,25 +1,9 @@
# 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 5,0 # BDS 5,0
# Track and turn report # Track and turn report
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
def is50(msg): def is50(msg):
@@ -27,32 +11,32 @@ def is50(msg):
(Track and turn report) (Track and turn report)
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# status bit 1, 12, 24, 35, 46 # status bit 1, 12, 24, 35, 46
if wrongstatus(d, 1, 3, 11): if common.wrongstatus(d, 1, 3, 11):
return False return False
if wrongstatus(d, 12, 13, 23): if common.wrongstatus(d, 12, 13, 23):
return False return False
if wrongstatus(d, 24, 25, 34): if common.wrongstatus(d, 24, 25, 34):
return False return False
if wrongstatus(d, 35, 36, 45): if common.wrongstatus(d, 35, 36, 45):
return False return False
if wrongstatus(d, 46, 47, 56): if common.wrongstatus(d, 46, 47, 56):
return False return False
roll = roll50(msg) roll = roll50(msg)
@@ -77,19 +61,19 @@ def roll50(msg):
"""Roll angle, BDS 5,0 message """Roll angle, BDS 5,0 message
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS50) string msg (str): 28 hexdigits string
Returns: Returns:
float: angle in degrees, float: angle in degrees,
negative->left wing down, positive->right wing down negative->left wing down, positive->right wing down
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0] == '0': if d[0] == "0":
return None return None
sign = int(d[1]) # 1 -> left wing down sign = int(d[1]) # 1 -> left wing down
value = bin2int(d[2:11]) value = common.bin2int(d[2:11])
if sign: if sign:
value = value - 512 value = value - 512
@@ -102,18 +86,18 @@ def trk50(msg):
"""True track angle, BDS 5,0 message """True track angle, BDS 5,0 message
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS50) string msg (str): 28 hexdigits string
Returns: Returns:
float: angle in degrees to true north (from 0 to 360) float: angle in degrees to true north (from 0 to 360)
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[11] == '0': if d[11] == "0":
return None return None
sign = int(d[12]) # 1 -> west sign = int(d[12]) # 1 -> west
value = bin2int(d[13:23]) value = common.bin2int(d[13:23])
if sign: if sign:
value = value - 1024 value = value - 1024
@@ -131,17 +115,17 @@ def gs50(msg):
"""Ground speed, BDS 5,0 message """Ground speed, BDS 5,0 message
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS50) string msg (str): 28 hexdigits string
Returns: Returns:
int: ground speed in knots int: ground speed in knots
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[23] == '0': if d[23] == "0":
return None return None
spd = bin2int(d[24:34]) * 2 # kts spd = common.bin2int(d[24:34]) * 2 # kts
return spd return spd
@@ -149,21 +133,21 @@ def rtrk50(msg):
"""Track angle rate, BDS 5,0 message """Track angle rate, BDS 5,0 message
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS50) string msg (str): 28 hexdigits string
Returns: Returns:
float: angle rate in degrees/second float: angle rate in degrees/second
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[34] == '0': if d[34] == "0":
return None return None
if d[36:45] == "111111111": if d[36:45] == "111111111":
return None return None
sign = int(d[35]) # 1 -> negative value, two's complement sign = int(d[35]) # 1 -> negative value, two's complement
value = bin2int(d[36:45]) value = common.bin2int(d[36:45])
if sign: if sign:
value = value - 512 value = value - 512
@@ -175,15 +159,15 @@ def tas50(msg):
"""Aircraft true airspeed, BDS 5,0 message """Aircraft true airspeed, BDS 5,0 message
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS50) string msg (str): 28 hexdigits string
Returns: Returns:
int: true airspeed in knots int: true airspeed in knots
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[45] == '0': if d[45] == "0":
return None return None
tas = bin2int(d[46:56]) * 2 # kts tas = common.bin2int(d[46:56]) * 2 # kts
return tas return tas

View File

@@ -1,25 +1,9 @@
# 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 5,3 # BDS 5,3
# Air-referenced state vector # Air-referenced state vector
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
def is53(msg): def is53(msg):
@@ -27,32 +11,32 @@ def is53(msg):
(Air-referenced state vector) (Air-referenced state vector)
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# status bit 1, 13, 24, 34, 47 # status bit 1, 13, 24, 34, 47
if wrongstatus(d, 1, 3, 12): if common.wrongstatus(d, 1, 3, 12):
return False return False
if wrongstatus(d, 13, 14, 23): if common.wrongstatus(d, 13, 14, 23):
return False return False
if wrongstatus(d, 24, 25, 33): if common.wrongstatus(d, 24, 25, 33):
return False return False
if wrongstatus(d, 34, 35, 46): if common.wrongstatus(d, 34, 35, 46):
return False return False
if wrongstatus(d, 47, 49, 56): if common.wrongstatus(d, 47, 49, 56):
return False return False
ias = ias53(msg) ias = ias53(msg)
@@ -78,18 +62,18 @@ def hdg53(msg):
"""Magnetic heading, BDS 5,3 message """Magnetic heading, BDS 5,3 message
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS53) string msg (str): 28 hexdigits string
Returns: Returns:
float: angle in degrees to true north (from 0 to 360) float: angle in degrees to true north (from 0 to 360)
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0] == '0': if d[0] == "0":
return None return None
sign = int(d[1]) # 1 -> west sign = int(d[1]) # 1 -> west
value = bin2int(d[2:12]) value = common.bin2int(d[2:12])
if sign: if sign:
value = value - 1024 value = value - 1024
@@ -107,17 +91,17 @@ def ias53(msg):
"""Indicated airspeed, DBS 5,3 message """Indicated airspeed, DBS 5,3 message
Args: Args:
msg (String): 28 bytes hexadecimal message msg (str): 28 hexdigits
Returns: Returns:
int: indicated arispeed in knots int: indicated arispeed in knots
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[12] == '0': if d[12] == "0":
return None return None
ias = bin2int(d[13:23]) # knots ias = common.bin2int(d[13:23]) # knots
return ias return ias
@@ -125,17 +109,17 @@ def mach53(msg):
"""MACH number, DBS 5,3 message """MACH number, DBS 5,3 message
Args: Args:
msg (String): 28 bytes hexadecimal message msg (str): 28 hexdigits
Returns: Returns:
float: MACH number float: MACH number
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[23] == '0': if d[23] == "0":
return None return None
mach = bin2int(d[24:33]) * 0.008 mach = common.bin2int(d[24:33]) * 0.008
return round(mach, 3) return round(mach, 3)
@@ -143,35 +127,36 @@ def tas53(msg):
"""Aircraft true airspeed, BDS 5,3 message """Aircraft true airspeed, BDS 5,3 message
Args: Args:
msg (String): 28 bytes hexadecimal message msg (str): 28 hexdigits
Returns: Returns:
float: true airspeed in knots float: true airspeed in knots
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[33] == '0': if d[33] == "0":
return None return None
tas = bin2int(d[34:46]) * 0.5 # kts tas = common.bin2int(d[34:46]) * 0.5 # kts
return round(tas, 1) return round(tas, 1)
def vr53(msg): def vr53(msg):
"""Vertical rate """Vertical rate
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS60) string msg (str): 28 hexdigits (BDS60) string
Returns: Returns:
int: vertical rate in feet/minutes int: vertical rate in feet/minutes
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[46] == '0': if d[46] == "0":
return None return None
sign = int(d[47]) # 1 -> negative value, two's complement sign = int(d[47]) # 1 -> negative value, two's complement
value = bin2int(d[48:56]) value = common.bin2int(d[48:56])
if value == 0 or value == 255: # all zeros or all ones if value == 0 or value == 255: # all zeros or all ones
return 0 return 0

View File

@@ -1,56 +1,41 @@
# 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 6,0 # BDS 6,0
# Heading and speed report # Heading and speed report
# ------------------------------------------ # ------------------------------------------
from __future__ import absolute_import, print_function, division from pyModeS import common
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
def is60(msg): def is60(msg):
"""Check if a message is likely to be BDS code 6,0 """Check if a message is likely to be BDS code 6,0
Args: Args:
msg (String): 28 bytes hexadecimal message string msg (str): 28 hexdigits string
Returns: Returns:
bool: True or False bool: True or False
""" """
if allzeros(msg): if common.allzeros(msg):
return False return False
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
# status bit 1, 13, 24, 35, 46 # status bit 1, 13, 24, 35, 46
if wrongstatus(d, 1, 2, 12): if common.wrongstatus(d, 1, 2, 12):
return False return False
if wrongstatus(d, 13, 14, 23): if common.wrongstatus(d, 13, 14, 23):
return False return False
if wrongstatus(d, 24, 25, 34): if common.wrongstatus(d, 24, 25, 34):
return False return False
if wrongstatus(d, 35, 36, 45): if common.wrongstatus(d, 35, 36, 45):
return False return False
if wrongstatus(d, 46, 47, 56): if common.wrongstatus(d, 46, 47, 56):
return False return False
ias = ias60(msg) ias = ias60(msg)
@@ -76,18 +61,18 @@ def hdg60(msg):
"""Megnetic heading of aircraft """Megnetic heading of aircraft
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS60) string msg (str): 28 hexdigits string
Returns: Returns:
float: heading in degrees to megnetic north (from 0 to 360) float: heading in degrees to megnetic north (from 0 to 360)
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[0] == '0': if d[0] == "0":
return None return None
sign = int(d[1]) # 1 -> west sign = int(d[1]) # 1 -> west
value = bin2int(d[2:12]) value = common.bin2int(d[2:12])
if sign: if sign:
value = value - 1024 value = value - 1024
@@ -105,17 +90,17 @@ def ias60(msg):
"""Indicated airspeed """Indicated airspeed
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS60) string msg (str): 28 hexdigits string
Returns: Returns:
int: indicated airspeed in knots int: indicated airspeed in knots
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[12] == '0': if d[12] == "0":
return None return None
ias = bin2int(d[13:23]) # kts ias = common.bin2int(d[13:23]) # kts
return ias return ias
@@ -123,17 +108,17 @@ def mach60(msg):
"""Aircraft MACH number """Aircraft MACH number
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS60) string msg (str): 28 hexdigits string
Returns: Returns:
float: MACH number float: MACH number
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[23] == '0': if d[23] == "0":
return None return None
mach = bin2int(d[24:34]) * 2.048 / 512.0 mach = common.bin2int(d[24:34]) * 2.048 / 512.0
return round(mach, 3) return round(mach, 3)
@@ -141,18 +126,18 @@ def vr60baro(msg):
"""Vertical rate from barometric measurement, this value may be very noisy. """Vertical rate from barometric measurement, this value may be very noisy.
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS60) string msg (str): 28 hexdigits string
Returns: Returns:
int: vertical rate in feet/minutes int: vertical rate in feet/minutes
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[34] == '0': if d[34] == "0":
return None return None
sign = int(d[35]) # 1 -> negative value, two's complement sign = int(d[35]) # 1 -> negative value, two's complement
value = bin2int(d[36:45]) value = common.bin2int(d[36:45])
if value == 0 or value == 511: # all zeros or all ones if value == 0 or value == 511: # all zeros or all ones
return 0 return 0
@@ -164,21 +149,21 @@ def vr60baro(msg):
def vr60ins(msg): def vr60ins(msg):
"""Vertical rate messured by onbard equiments (IRS, AHRS) """Vertical rate measurd by onbard equiments (IRS, AHRS)
Args: Args:
msg (String): 28 bytes hexadecimal message (BDS60) string msg (str): 28 hexdigits string
Returns: Returns:
int: vertical rate in feet/minutes int: vertical rate in feet/minutes
""" """
d = hex2bin(data(msg)) d = common.hex2bin(common.data(msg))
if d[45] == '0': if d[45] == "0":
return None return None
sign = int(d[46]) # 1 -> negative value, two's complement sign = int(d[46]) # 1 -> negative value, two's complement
value = bin2int(d[47:56]) value = common.bin2int(d[47:56])
if value == 0 or value == 511: # all zeros or all ones if value == 0 or value == 511: # all zeros or all ones
return 0 return 0

View File

@@ -1,26 +1,27 @@
"""Comm-B Wrapper. """Comm-B module.
The Comm-B wrapper imports all functions from the following modules: The Comm-B module imports all functions from the following modules:
**ELS - elementary surveillance** ELS - elementary surveillance
- pyModeS.decoder.bds.bds10
- pyModeS.decoder.bds.bds17
- pyModeS.decoder.bds.bds20
- pyModeS.decoder.bds.bds30
**EHS - enhanced surveillance** - pyModeS.decoder.bds.bds10
- pyModeS.decoder.bds.bds40 - pyModeS.decoder.bds.bds17
- pyModeS.decoder.bds.bds50 - pyModeS.decoder.bds.bds20
- pyModeS.decoder.bds.bds60 - pyModeS.decoder.bds.bds30
**MRAR and MHR** EHS - enhanced surveillance
- pyModeS.decoder.bds.bds44
- pyModeS.decoder.bds.bds45 - pyModeS.decoder.bds.bds40
- pyModeS.decoder.bds.bds50
- pyModeS.decoder.bds.bds60
MRAR and MHR
- pyModeS.decoder.bds.bds44
- pyModeS.decoder.bds.bds45
""" """
from __future__ import absolute_import, print_function, division
# ELS - elementary surveillance # ELS - elementary surveillance
from pyModeS.decoder.bds.bds10 import * from pyModeS.decoder.bds.bds10 import *
from pyModeS.decoder.bds.bds17 import * from pyModeS.decoder.bds.bds17 import *

View File

@@ -9,7 +9,6 @@ The EHS wrapper imports all functions from the following modules:
""" """
from __future__ import absolute_import, print_function, division
import warnings import warnings
from pyModeS.decoder.bds.bds40 import * from pyModeS.decoder.bds.bds40 import *
@@ -17,13 +16,20 @@ from pyModeS.decoder.bds.bds50 import *
from pyModeS.decoder.bds.bds60 import * from pyModeS.decoder.bds.bds60 import *
from pyModeS.decoder.bds import infer from pyModeS.decoder.bds import infer
warnings.simplefilter('once', DeprecationWarning) warnings.simplefilter("once", DeprecationWarning)
warnings.warn("pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning) warnings.warn(
"pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning
)
def BDS(msg): def BDS(msg):
warnings.warn("pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning) warnings.warn(
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning
)
return infer(msg) return infer(msg)
def icao(msg): def icao(msg):
from pyModeS.decoder.common import icao from pyModeS.decoder.common import icao
return icao(msg) return icao(msg)

View File

@@ -10,13 +10,14 @@ The ELS wrapper imports all functions from the following modules:
""" """
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.bds.bds10 import * from pyModeS.decoder.bds.bds10 import *
from pyModeS.decoder.bds.bds17 import * from pyModeS.decoder.bds.bds17 import *
from pyModeS.decoder.bds.bds20 import * from pyModeS.decoder.bds.bds20 import *
from pyModeS.decoder.bds.bds30 import * from pyModeS.decoder.bds.bds30 import *
import warnings import warnings
warnings.simplefilter('once', DeprecationWarning)
warnings.warn("pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning) warnings.simplefilter("once", DeprecationWarning)
warnings.warn(
"pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning
)

View File

@@ -1,23 +1,5 @@
# 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 Warpper for short roll call surveillance replies DF=4/5
[To be implemented] [To be implemented]
""" """
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common

View File

@@ -6,120 +6,147 @@ See source code at: https://github.com/junzis/pyModeS/blob/master/pyModeS/decode
NA = None NA = None
TC_NUCp_lookup = { TC_NUCp_lookup = {
0:0, 5:9, 6:8, 7:7, 8:6, 0: 0,
9:9, 10:8, 11:7, 12:6, 13:5, 14:4, 15:3, 16:2, 17:1, 18:0, 5: 9,
20:9, 21:8, 22:0 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 = { TC_NICv1_lookup = {
5:11, 6:10, 7:9, 8:0, 5: 11,
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, 6: 10,
20:11, 21:10, 22:0 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 = { TC_NICv2_lookup = {
5:11, 6:10, 7:{2:9, 0:8}, 8:{3:7, 2:6, 1:6, 0:0}, 5: 11,
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, 6: 10,
20:11, 21:10, 22:0 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 = { NUCp = {
9: {'HPL':7.5, 'RCu':3, 'RCv':4}, 9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
8: {'HPL':25, 'RCu':10, 'RCv':15}, 8: {"HPL": 25, "RCu": 10, "RCv": 15},
7: {'HPL':185, 'RCu':93, 'RCv':NA}, 7: {"HPL": 185, "RCu": 93, "RCv": NA},
6: {'HPL':370, 'RCu':185, 'RCv':NA}, 6: {"HPL": 370, "RCu": 185, "RCv": NA},
5: {'HPL':926, 'RCu':463, 'RCv':NA}, 5: {"HPL": 926, "RCu": 463, "RCv": NA},
4: {'HPL':1852, 'RCu':926, 'RCv':NA}, 4: {"HPL": 1852, "RCu": 926, "RCv": NA},
3: {'HPL':3704, 'RCu':1852, 'RCv':NA}, 3: {"HPL": 3704, "RCu": 1852, "RCv": NA},
2: {'HPL':18520, 'RCu':9260, 'RCv':NA}, 2: {"HPL": 18520, "RCu": 9260, "RCv": NA},
1: {'HPL':37040, 'RCu':18520, 'RCv':NA}, 1: {"HPL": 37040, "RCu": 18520, "RCv": NA},
0: {'HPL':NA, 'RCu':NA, 'RCv':NA}, 0: {"HPL": NA, "RCu": NA, "RCv": NA},
} }
NUCv = { NUCv = {
0: {'HVE':NA, 'VVE':NA}, 0: {"HVE": NA, "VVE": NA},
1: {'HVE':10, 'VVE':15.2}, 1: {"HVE": 10, "VVE": 15.2},
2: {'HVE':3, 'VVE':4.5}, 2: {"HVE": 3, "VVE": 4.5},
3: {'HVE':1, 'VVE':1.5}, 3: {"HVE": 1, "VVE": 1.5},
4: {'HVE':0.3, 'VVE':0.46}, 4: {"HVE": 0.3, "VVE": 0.46},
} }
NACp = { NACp = {
11: {'EPU': 3, 'VEPU': 4}, 11: {"EPU": 3, "VEPU": 4},
10: {'EPU': 10, 'VEPU': 15}, 10: {"EPU": 10, "VEPU": 15},
9: {'EPU': 30, 'VEPU': 45}, 9: {"EPU": 30, "VEPU": 45},
8: {'EPU': 93, 'VEPU': NA}, 8: {"EPU": 93, "VEPU": NA},
7: {'EPU': 185, 'VEPU': NA}, 7: {"EPU": 185, "VEPU": NA},
6: {'EPU': 556, 'VEPU': NA}, 6: {"EPU": 556, "VEPU": NA},
5: {'EPU': 926, 'VEPU': NA}, 5: {"EPU": 926, "VEPU": NA},
4: {'EPU': 1852, 'VEPU': NA}, 4: {"EPU": 1852, "VEPU": NA},
3: {'EPU': 3704, 'VEPU': NA}, 3: {"EPU": 3704, "VEPU": NA},
2: {'EPU': 7408, 'VEPU': NA}, 2: {"EPU": 7408, "VEPU": NA},
1: {'EPU': 18520, 'VEPU': NA}, 1: {"EPU": 18520, "VEPU": NA},
0: {'EPU': NA, 'VEPU': NA}, 0: {"EPU": NA, "VEPU": NA},
} }
NACv = { NACv = {
0: {'HFOMr':NA, 'VFOMr':NA}, 0: {"HFOMr": NA, "VFOMr": NA},
1: {'HFOMr':10, 'VFOMr':15.2}, 1: {"HFOMr": 10, "VFOMr": 15.2},
2: {'HFOMr':3, 'VFOMr':4.5}, 2: {"HFOMr": 3, "VFOMr": 4.5},
3: {'HFOMr':1, 'VFOMr':1.5}, 3: {"HFOMr": 1, "VFOMr": 1.5},
4: {'HFOMr':0.3, 'VFOMr':0.46}, 4: {"HFOMr": 0.3, "VFOMr": 0.46},
} }
SIL = { SIL = {
3: {'PE_RCu': 1e-7, 'PE_VPL': 2e-7}, 3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
2: {'PE_RCu': 1e-5, 'PE_VPL': 1e-5}, 2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
1: {'PE_RCu': 1e-3, 'PE_VPL': 1e-3}, 1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
0: {'PE_RCu': NA, 'PE_VPL': NA}, 0: {"PE_RCu": NA, "PE_VPL": NA},
} }
NICv1 = { NICv1 = {
# NIC is used as the index at second Level # NIC is used as the index at second Level
11: {0: {"Rc": 7.5, "VPL": 11}},
11: {0: {'Rc': 7.5, 'VPL': 11}}, 10: {0: {"Rc": 25, "VPL": 37.5}},
10: {0: {'Rc': 25, 'VPL': 37.5}}, 9: {1: {"Rc": 75, "VPL": 112}},
9: {1: {'Rc': 75, 'VPL': 112}}, 8: {0: {"Rc": 185, "VPL": NA}},
8: {0: {'Rc': 185, 'VPL': NA}}, 7: {0: {"Rc": 370, "VPL": NA}},
7: {0: {'Rc': 370, 'VPL': NA}}, 6: {0: {"Rc": 926, "VPL": NA}, 1: {"Rc": 1111, "VPL": NA}},
6: { 5: {0: {"Rc": 1852, "VPL": NA}},
0: {'Rc': 926, 'VPL': NA}, 4: {0: {"Rc": 3702, "VPL": NA}},
1: {'Rc': 1111, 'VPL': NA}, 3: {1: {"Rc": 7408, "VPL": NA}},
}, 2: {0: {"Rc": 14008, "VPL": NA}},
5: {0: {'Rc': 1852, 'VPL': NA}}, 1: {0: {"Rc": 37000, "VPL": NA}},
4: {0: {'Rc': 3702, 'VPL': NA}}, 0: {0: {"Rc": NA, "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 = { NICv2 = {
# Decimal value of [NICa NICb/NICc] is used as the index at second Level # Decimal value of [NICa NICb/NICc] is used as the index at second Level
11: {0: {"Rc": 7.5}},
11: {0: {'Rc': 7.5}}, 10: {0: {"Rc": 25}},
10: {0: {'Rc': 25}}, 9: {2: {"Rc": 75}, 3: {"Rc": 75}},
9: { 8: {0: {"Rc": 185}},
2: {'Rc': 75}, 7: {0: {"Rc": 370}, 3: {"Rc": 370}},
3: {'Rc': 75}, 6: {0: {"Rc": 926}, 1: {"Rc": 556}, 2: {"Rc": 556}, 3: {"Rc": 1111}},
}, 5: {0: {"Rc": 1852}},
8: {0: {'Rc': 185}}, 4: {0: {"Rc": 3702}},
7: { 3: {3: {"Rc": 7408}},
0: {'Rc': 370}, 2: {0: {"Rc": 14008}},
3: {'Rc': 370}, 1: {0: {"Rc": 37000}},
}, 0: {0: {"Rc": NA}},
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}},
} }

25
pyModeS/decoder/uplink.py Normal file
View File

@@ -0,0 +1,25 @@
from pyModeS import common
def uplink_icao(msg):
"""Calculate the ICAO address from a Mode-S interrogation (uplink message)"""
p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4)
data = int(msg[:-6], 16)
PA = int(msg[-6:], 16)
ad = 0
topbit = 0b1 << (len(msg) * 4 - 25)
for j in range(0, len(msg) * 4, 1):
if data & topbit:
data ^= p_gen
data = (data << 1) + ((PA >> 23) & 1)
PA = PA << 1
if j > (len(msg) * 4 - 26):
ad = ad + ((data >> (len(msg) * 4 - 25)) & 1)
ad = ad << 1
return "%06X" % (ad >> 2)
def uf(msg):
"""Decode Uplink Format value, bits 1 to 5."""
ufbin = common.hex2bin(msg[:2])
return min(common.bin2int(ufbin[0:5]), 24)

View File

@@ -0,0 +1,68 @@
from .bds.bds08 import me08
from .bds.bds09 import me09
from pyModeS import common
def encode_adsb(**kwargs):
"""Encode ADS-B message.
Args:
icao (string): Transponder ICAO address (6 hexdigits)
capability (int): Transponder capability, between 0 and 7
typecode (int): Typecode, less than 32
callsign (string): Callsign (6 hexdigits)
category (int): Aircraft category, between 0 and 7, Default to 0.
speed (int): Speed in knots.
angle (float): Track angle or heading angle in degrees.
vertical_rate (int): vertical rate in feet/minute
intent_change (int): Intent change flag, 0 or 1. Default to 0.
ifr_capability (int): IFR capability flag, 0 or 1. Default to 1.
navigation_quality (int): NUC (ver 0) or NACv (ver 1, 2), between 0 and 7.
Default to 0.
supersonic (bool): Is this a supersonic flight? Default to False.
speed_type (str): Speed type: GS, IAS, or TAS. Default to GS.
vertical_rate_source (str): GNSS or BARO. Default to BARO.
gnss_baro_alt_diff (int): Different between GNSS and barometric altitude in feet.
Negative value indicates GNSS altitude below barometric altitude. Default to 0
Returns:
string: 28 hexdigits raw message
"""
tc = kwargs.get("typecode")
if 1 <= tc <= 4:
me = me08(**kwargs)
elif tc == 19:
me = me09(**kwargs)
msg = _constuct(**dict(kwargs, me=me))
return msg
def _constuct(**kwargs):
icao = kwargs.get("icao")
me = kwargs.get("me")
capability = kwargs.get("capability", 6)
if icao is None or len(icao) != 6:
raise Exception("Transponder address must be 6 hexadecimal characters.")
if me is None or len(me) != 14:
raise Exception("Message be 14 hexadecimal characters.")
if capability > 6:
raise Exception("Transponder capability must be smaller than 7.")
header_bin = "10001" + "{0:03b}".format(capability)
header_hex = "{0:02X}".format(int(header_bin, 2))
msg = header_hex + icao + me + "000000"
pi = common.crc(msg, encode=True)
pi_hex = "{0:06X}".format(pi)
msg = msg[:-6] + pi_hex
return msg

View File

View File

@@ -0,0 +1,5 @@
# ------------------------------------------
# BDS 0,5
# ADS-B TC=9-18
# Airborn position
# ------------------------------------------

View File

@@ -0,0 +1,5 @@
# ------------------------------------------
# BDS 0,6
# ADS-B TC=5-8
# Surface position
# ------------------------------------------

View File

@@ -0,0 +1,40 @@
# ------------------------------------------
# BDS 0,8
# ADS-B TC=1-4
# Aircraft identitification and category
# ------------------------------------------
from pyModeS import common
charmap = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######"
def me08(callsign, **kwargs):
cs = callsign
tc = kwargs.get("typecode")
cat = kwargs.get("category", 0)
if len(cs) > 8:
raise Exception("callsign must contain less than 9 characters")
if tc > 4:
raise Exception("typecode must be less 5")
if cat > 7:
raise Exception("category must be less 8")
if not cs.isalnum():
raise Exception("callsign must only contain alphanumeric characters")
cs = "{:<8}".format(cs.upper())
idx = [charmap.index(c) for c in cs]
me_bin = (
"{0:05b}".format(tc)
+ "{0:03b}".format(cat)
+ "".join("{0:06b}".format(i) for i in idx)
)
me_hex = "{0:04X}".format(int(me_bin, 2))
return me_hex

View File

@@ -0,0 +1,119 @@
# ------------------------------------------
# BDS 0,9
# ADS-B TC=19
# Aircraft Airborn velocity
# ------------------------------------------
import numpy as np
def me09(speed, angle, vertical_rate, **kwargs):
spd = speed
agl = angle
vr = vertical_rate
tc = kwargs.get("typecode")
intent = kwargs.get("intent_change", 0)
ifr = kwargs.get("ifr_capability", 1)
navq = kwargs.get("navigation_quality", 0)
supersonic = kwargs.get("supersonic", False)
spd_type = kwargs.get("speed_type", "gs").lower()
vr_source = kwargs.get("vertical_rate_source", "baro").lower()
alt_diff = kwargs.get("gnss_baro_alt_diff", 0)
if tc != 19:
raise Exception("Typecode must be 19.")
if intent not in (0, 1):
raise Exception("Intent change flag must be 0 or 1.")
if ifr not in (0, 1):
raise Exception("IFR capability flag must be 0 or 1.")
if type(supersonic) != bool:
raise Exception("Subsonic flag must be True or False.")
if navq > 7:
raise Exception("Navigation quality indicator must be smaller than 8.")
if spd_type not in ["gs", "tas"]:
raise Exception("Speed type must be 'gs', 'ias', or 'tas'.")
if vr_source not in ["baro", "gnss"]:
raise Exception("Vertical rate source must be 'baro' or 'gnss'.")
me_bin = ""
# typecode
me_bin += "{0:05b}".format(tc)
# sub-type
if supersonic:
if spd_type == "gs":
me_bin += "010"
else:
me_bin += "100"
else:
if spd_type == "gs":
me_bin += "001"
else:
me_bin += "011"
# intent, ifr, navigation quality
me_bin += str(intent) + str(ifr) + "{0:03b}".format(navq)
# speed and angle part
if spd_type == "gs":
vx = spd * np.sin(np.radians(agl))
vy = spd * np.cos(np.radians(agl))
if supersonic:
vx /= 4
vy /= 4
vx = int(round(vx))
vy = int(round(vy))
sew = "0" if vx >= 0 else "1"
sns = "0" if vy >= 0 else "1"
vew = "{0:010b}".format(min(abs(vx), 1023) + 1)
vns = "{0:010b}".format(min(abs(vy), 1023) + 1)
me_bin += sew + vew + sns + vns
elif spd_type == "ias" or spd_type == "tas":
hdg = int(round(agl * 1024 / 360))
hdg = min(hdg, 1023)
air_type = "1" if spd_type == "tas" else "0"
if supersonic:
spd /= 4
spd = min(int(round(spd)), 1023)
me_bin += "1" + "{0:010b}".format(hdg) + air_type + "{0:010b}".format(spd)
# vertical rate source
me_bin += "1" if vr_source == "baro" else "0"
# vertical rate
me_bin += "0" if vr > 0 else "1"
vr = int(round((abs(vr) / 64 + 1)))
vr = min(vr, 511)
me_bin += "{0:09b}".format(vr)
# reserved
me_bin += "00"
# altitude difference
me_bin += "1" if alt_diff < 0 else "0"
alt_diff = int(round(abs(alt_diff) / 25 + 1))
alt_diff = min(alt_diff, 127)
me_bin += "{0:07b}".format(alt_diff)
print(me_bin)
# convert to hexdigits
me_hex = "{0:04X}".format(int(me_bin, 2))
return me_hex

View File

@@ -1 +0,0 @@
from __future__ import absolute_import, print_function, division

View File

@@ -9,9 +9,9 @@ International Standard Atmosphere
p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m] 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] a = vsound(H) # speed of sound [m/s] as function of H[m]
p = pressure(H) # calls atmos but retruns only pressure [Pa] p = pressure(H) # calls atmos but returns only pressure [Pa]
T = temperature(H) # calculates temperature [K] T = temperature(H) # calculates temperature [K]
rho = density(H) # calls atmos but retruns only pressure [Pa] rho = density(H) # calls atmos but returns only pressure [Pa]
Speed conversion at altitude H[m] in ISA Speed conversion at altitude H[m] in ISA
:: ::
@@ -35,26 +35,26 @@ ft = 0.3048 # ft -> m
fpm = 0.00508 # ft/min -> m/s fpm = 0.00508 # ft/min -> m/s
inch = 0.0254 # inch -> m inch = 0.0254 # inch -> m
sqft = 0.09290304 # 1 square foot sqft = 0.09290304 # 1 square foot
nm = 1852. # nautical mile -> m nm = 1852.0 # nautical mile -> m
lbs = 0.453592 # pound -> kg lbs = 0.453592 # pound -> kg
g0 = 9.80665 # m/s2, Sea level gravity constant g0 = 9.80665 # m/s2, Sea level gravity constant
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
p0 = 101325. # Pa, air pressure, sea level ISA p0 = 101325.0 # Pa, air pressure, sea level ISA
rho0 = 1.225 # kg/m3, air density, sea level ISA rho0 = 1.225 # kg/m3, air density, sea level ISA
T0 = 288.15 # K, temperature, sea level ISA T0 = 288.15 # K, temperature, sea level ISA
gamma = 1.40 # cp/cv for air gamma = 1.40 # cp/cv for air
gamma1 = 0.2 # (gamma-1)/2 for air gamma1 = 0.2 # (gamma-1)/2 for air
gamma2 = 3.5 # gamma/(gamma-1) for air gamma2 = 3.5 # gamma/(gamma-1) for air
beta = -0.0065 # [K/m] ISA temp gradient below tropopause beta = -0.0065 # [K/m] ISA temp gradient below tropopause
r_earth = 6371000. # m, average earth radius r_earth = 6371000.0 # m, average earth radius
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0) a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
def atmos(H): def atmos(H):
# H in metres # H in metres
T = np.maximum(288.15 - 0.0065 * H, 216.65) T = np.maximum(288.15 - 0.0065 * H, 216.65)
rhotrop = 1.225 * (T / 288.15)**4.256848030018761 rhotrop = 1.225 * (T / 288.15) ** 4.256848030018761
dhstrat = np.maximum(0., H - 11000.0) dhstrat = np.maximum(0.0, H - 11000.0)
rho = rhotrop * np.exp(-dhstrat / 6341.552161) rho = rhotrop * np.exp(-dhstrat / 6341.552161)
p = rho * R * T p = rho * R * T
return p, rho, T return p, rho, T
@@ -101,8 +101,10 @@ def distance(lat1, lon1, lat2, lon2, H=0):
theta1 = np.radians(lon1) theta1 = np.radians(lon1)
theta2 = np.radians(lon2) theta2 = np.radians(lon2)
cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(phi2) cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(
cos = np.where(cos>1, 1, cos) phi2
)
cos = np.where(cos > 1, 1, cos)
arc = np.arccos(cos) arc = np.arccos(cos)
dist = arc * (r_earth + H) # meters, radius of earth dist = arc * (r_earth + H) # meters, radius of earth
@@ -114,9 +116,8 @@ def bearing(lat1, lon1, lat2, lon2):
lon1 = np.radians(lon1) lon1 = np.radians(lon1)
lat2 = np.radians(lat2) lat2 = np.radians(lat2)
lon2 = np.radians(lon2) lon2 = np.radians(lon2)
x = np.sin(lon2-lon1) * np.cos(lat2) x = np.sin(lon2 - lon1) * np.cos(lat2)
y = np.cos(lat1) * np.sin(lat2) \ y = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(lon2 - lon1)
- np.sin(lat1) * np.cos(lat2) * np.cos(lon2-lon1)
initial_bearing = np.arctan2(x, y) initial_bearing = np.arctan2(x, y)
initial_bearing = np.degrees(initial_bearing) initial_bearing = np.degrees(initial_bearing)
bearing = (initial_bearing + 360) % 360 bearing = (initial_bearing + 360) % 360
@@ -129,44 +130,44 @@ def bearing(lat1, lon1, lat2, lon2):
def tas2mach(Vtas, H): def tas2mach(Vtas, H):
"""True Airspeed to Mach number""" """True Airspeed to Mach number"""
a = vsound(H) a = vsound(H)
Mach = Vtas/a Mach = Vtas / a
return Mach return Mach
def mach2tas(Mach, H): def mach2tas(Mach, H):
"""Mach number to True Airspeed""" """Mach number to True Airspeed"""
a = vsound(H) a = vsound(H)
Vtas = Mach*a Vtas = Mach * a
return Vtas return Vtas
def eas2tas(Veas, H): def eas2tas(Veas, H):
"""Equivalent Airspeed to True Airspeed""" """Equivalent Airspeed to True Airspeed"""
rho = density(H) rho = density(H)
Vtas = Veas * np.sqrt(rho0/rho) Vtas = Veas * np.sqrt(rho0 / rho)
return Vtas return Vtas
def tas2eas(Vtas, H): def tas2eas(Vtas, H):
"""True Airspeed to Equivalent Airspeed""" """True Airspeed to Equivalent Airspeed"""
rho = density(H) rho = density(H)
Veas = Vtas * np.sqrt(rho/rho0) Veas = Vtas * np.sqrt(rho / rho0)
return Veas return Veas
def cas2tas(Vcas, H): def cas2tas(Vcas, H):
"""Calibrated Airspeed to True Airspeed""" """Calibrated Airspeed to True Airspeed"""
p, rho, T = atmos(H) p, rho, T = atmos(H)
qdyn = p0*((1.+rho0*Vcas*Vcas/(7.*p0))**3.5-1.) qdyn = p0 * ((1.0 + rho0 * Vcas * Vcas / (7.0 * p0)) ** 3.5 - 1.0)
Vtas = np.sqrt(7.*p/rho*((1.+qdyn/p)**(2./7.)-1.)) Vtas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
return Vtas return Vtas
def tas2cas(Vtas, H): def tas2cas(Vtas, H):
"""True Airspeed to Calibrated Airspeed""" """True Airspeed to Calibrated Airspeed"""
p, rho, T = atmos(H) p, rho, T = atmos(H)
qdyn = p*((1.+rho*Vtas*Vtas/(7.*p))**3.5-1.) qdyn = p * ((1.0 + rho * Vtas * Vtas / (7.0 * p)) ** 3.5 - 1.0)
Vcas = np.sqrt(7.*p0/rho0*((qdyn/p0+1.)**(2./7.)-1.)) Vcas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
return Vcas return Vcas

188
pyModeS/extra/rtlreader.py Normal file
View File

@@ -0,0 +1,188 @@
import traceback
import numpy as np
import pyModeS as pms
from rtlsdr import RtlSdr
import time
sampling_rate = 2e6
smaples_per_microsec = 2
modes_frequency = 1090e6
buffer_size = 1024 * 200
read_size = 1024 * 100
pbits = 8
fbits = 112
preamble = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]
th_amp_diff = 0.8 # signal amplitude threshold difference between 0 and 1 bit
class RtlReader(object):
def __init__(self, **kwargs):
super(RtlReader, self).__init__()
self.signal_buffer = [] # amplitude of the sample only
self.sdr = RtlSdr()
self.sdr.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency
self.sdr.gain = "auto"
self.debug = kwargs.get("debug", False)
self.raw_pipe_in = None
self.stop_flag = False
self.noise_floor = 1e6
def _calc_noise(self):
"""Calculate noise floor"""
window = smaples_per_microsec * 100
total_len = len(self.signal_buffer)
means = (
np.array(self.signal_buffer[: total_len // window * window])
.reshape(-1, window)
.mean(axis=1)
)
return min(means)
def _process_buffer(self):
"""process raw IQ data in the buffer"""
# update noise floor
self.noise_floor = min(self._calc_noise(), self.noise_floor)
# set minimum signal amplitude
min_sig_amp = 3.162 * self.noise_floor # 10 dB SNR
# Mode S messages
messages = []
buffer_length = len(self.signal_buffer)
i = 0
while i < buffer_length:
if self.signal_buffer[i] < min_sig_amp:
i += 1
continue
if self._check_preamble(self.signal_buffer[i : i + pbits * 2]):
frame_start = i + pbits * 2
frame_end = i + pbits * 2 + (fbits + 1) * 2
frame_length = (fbits + 1) * 2
frame_pulses = self.signal_buffer[frame_start:frame_end]
threshold = max(frame_pulses) * 0.2
msgbin = []
for j in range(0, frame_length, 2):
p2 = frame_pulses[j : j + 2]
if len(p2) < 2:
break
if p2[0] < threshold and p2[1] < threshold:
break
elif p2[0] >= p2[1]:
c = 1
elif p2[0] < p2[1]:
c = 0
else:
msgbin = []
break
msgbin.append(c)
# advance i with a jump
i = frame_start + j
if len(msgbin) > 0:
msghex = pms.bin2hex("".join([str(i) for i in msgbin]))
if self._check_msg(msghex):
messages.append([msghex, time.time()])
if self.debug:
self._debug_msg(msghex)
# elif i > buffer_length - 500:
# # save some for next process
# break
else:
i += 1
# reset the buffer
self.signal_buffer = self.signal_buffer[i:]
return messages
def _check_preamble(self, pulses):
if len(pulses) != 16:
return False
for i in range(16):
if abs(pulses[i] - preamble[i]) > th_amp_diff:
return False
return True
def _check_msg(self, msg):
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
if pms.crc(msg) == 0:
return True
elif df in [20, 21] and msglen == 28:
return True
elif df in [4, 5, 11] and msglen == 14:
return True
def _debug_msg(self, msg):
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
print(msg, pms.icao(msg), pms.crc(msg))
elif df in [20, 21] and msglen == 28:
print(msg, pms.icao(msg))
elif df in [4, 5, 11] and msglen == 14:
print(msg, pms.icao(msg))
else:
# print("[*]", msg)
pass
def _read_callback(self, data, rtlsdr_obj):
amp = np.absolute(data)
self.signal_buffer.extend(amp.tolist())
if len(self.signal_buffer) >= buffer_size:
messages = self._process_buffer()
self.handle_messages(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))
pass
def stop(self, *args, **kwargs):
self.sdr.close()
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self.raw_pipe_in = raw_pipe_in
self.stop_flag = stop_flag
try:
# raise RuntimeError("test exception")
while True:
data = self.sdr.read_samples(read_size)
self._read_callback(data, None)
except Exception as e:
tb = traceback.format_exc()
if exception_queue is not None:
exception_queue.put(tb)
raise e
if __name__ == "__main__":
import signal
rtl = RtlReader()
signal.signal(signal.SIGINT, rtl.stop)
rtl.debug = True
rtl.run()

View File

@@ -1,55 +1,54 @@
''' """Stream beast raw data from a TCP server, convert to mode-s messages."""
Stream beast raw data from a TCP server, convert to mode-s messages
'''
from __future__ import print_function, division
import os import os
import sys import sys
import socket
import time import time
import pyModeS as pms import pyModeS as pms
from threading import Thread
import traceback import traceback
import zmq
if (sys.version_info > (3, 0)): if sys.version_info > (3, 0):
PY_VERSION = 3 PY_VERSION = 3
else: else:
PY_VERSION = 2 PY_VERSION = 2
class BaseClient(Thread):
def __init__(self, host, port, rawtype): class TcpClient(object):
Thread.__init__(self) def __init__(self, host, port, datatype):
super(TcpClient, self).__init__()
self.host = host self.host = host
self.port = port self.port = port
self.buffer = [] self.buffer = []
self.rawtype = rawtype self.socket = None
if self.rawtype not in ['avr', 'beast', 'skysense']: self.datatype = datatype
print("rawtype must be either avr, beast or skysense") if self.datatype not in ["raw", "beast", "skysense"]:
print("datatype must be either raw, beast or skysense")
os._exit(1) os._exit(1)
self.raw_pipe_in = None
self.stop_flag = False
def connect(self): def connect(self):
while True: self.socket = zmq.Context().socket(zmq.STREAM)
try: self.socket.setsockopt(zmq.LINGER, 0)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setsockopt(zmq.RCVTIMEO, 10000)
s.settimeout(10) # 10 second timeout self.socket.connect("tcp://%s:%s" % (self.host, self.port))
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 stop(self):
self.socket.disconnect()
def read_avr_buffer(self): def read_raw_buffer(self):
# -- testing -- """ Read raw ADS-B data type.
# for b in self.buffer:
# print(chr(b), b)
# Append message with 0-9,A-F,a-f, until stop sign
String strats with "*" and ends with ";". For example:
*5d484ba898f8c6;
*8d400cd5990d7e9a10043e5e6da0;
*a0001498be800030aa0000c7a75f;
"""
messages = [] messages = []
msg_stop = False msg_stop = False
self.current_msg = ""
for b in self.buffer: for b in self.buffer:
if b == 59: if b == 59:
msg_stop = True msg_stop = True
@@ -57,9 +56,9 @@ class BaseClient(Thread):
messages.append([self.current_msg, ts]) messages.append([self.current_msg, ts])
if b == 42: if b == 42:
msg_stop = False msg_stop = False
self.current_msg = '' self.current_msg = ""
if (not msg_stop) and (48<=b<=57 or 65<=b<=70 or 97<=b<=102): 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.current_msg = self.current_msg + chr(b)
self.buffer = [] self.buffer = []
@@ -67,7 +66,8 @@ class BaseClient(Thread):
return messages return messages
def read_beast_buffer(self): def read_beast_buffer(self):
''' """Handle mode-s beast data type.
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level, <esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
2 byte Mode-AC 2 byte Mode-AC
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level, <esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
@@ -81,8 +81,7 @@ class BaseClient(Thread):
timestamp: timestamp:
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
''' """
messages_mlat = [] messages_mlat = []
msg = [] msg = []
i = 0 i = 0
@@ -91,16 +90,16 @@ class BaseClient(Thread):
# then, reset the self.buffer with the remainder # then, reset the self.buffer with the remainder
while i < len(self.buffer): while i < len(self.buffer):
if (self.buffer[i:i+2] == [0x1a, 0x1a]): if self.buffer[i : i + 2] == [0x1A, 0x1A]:
msg.append(0x1a) msg.append(0x1A)
i += 1 i += 1
elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1a): elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1A):
# special case where the last bit is 0x1a # special case where the last bit is 0x1a
msg.append(0x1a) msg.append(0x1A)
elif self.buffer[i] == 0x1a: elif self.buffer[i] == 0x1A:
if i == len(self.buffer) - 1: if i == len(self.buffer) - 1:
# special case where the last bit is 0x1a # special case where the last bit is 0x1a
msg.append(0x1a) msg.append(0x1A)
elif len(msg) > 0: elif len(msg) > 0:
messages_mlat.append(msg) messages_mlat.append(msg)
msg = [] msg = []
@@ -112,12 +111,12 @@ class BaseClient(Thread):
if len(msg) > 0: if len(msg) > 0:
reminder = [] reminder = []
for i, m in enumerate(msg): for i, m in enumerate(msg):
if (m == 0x1a) and (i < len(msg)-1): if (m == 0x1A) and (i < len(msg) - 1):
# rewind 0x1a, except when it is at the last bit # rewind 0x1a, except when it is at the last bit
reminder.extend([m, m]) reminder.extend([m, m])
else: else:
reminder.append(m) reminder.append(m)
self.buffer = [0x1a] + msg self.buffer = [0x1A] + msg
else: else:
self.buffer = [] self.buffer = []
@@ -131,14 +130,17 @@ class BaseClient(Thread):
if msgtype == 0x32: if msgtype == 0x32:
# Mode-S Short Message, 7 byte, 14-len hexstr # Mode-S Short Message, 7 byte, 14-len hexstr
msg = ''.join('%02X' % i for i in mm[8:15]) msg = "".join("%02X" % i for i in mm[8:15])
elif msgtype == 0x33: elif msgtype == 0x33:
# Mode-S Long Message, 14 byte, 28-len hexstr # Mode-S Long Message, 14 byte, 28-len hexstr
msg = ''.join('%02X' % i for i in mm[8:22]) msg = "".join("%02X" % i for i in mm[8:22])
else: else:
# Other message tupe # Other message tupe
continue continue
if len(msg) not in [14, 28]:
continue
df = pms.df(msg) df = pms.df(msg)
# skip incomplete message # skip incomplete message
@@ -167,7 +169,7 @@ class BaseClient(Thread):
Start character '$' Start character '$'
MS field - Payload MS field - Payload
Postion 1 through 14: Position 1 through 14:
14 bytes = 112 bits 14 bytes = 112 bits
Mode-S payload Mode-S payload
In case of DF types that only carry 7 bytes of information In case of DF types that only carry 7 bytes of information
@@ -215,25 +217,33 @@ class BaseClient(Thread):
messages = [] messages = []
while len(self.buffer) > SS_MSGLENGTH: while len(self.buffer) > SS_MSGLENGTH:
i = 0 i = 0
if self.buffer[i] == SS_STARTCHAR and self.buffer[i+SS_MSGLENGTH] == SS_STARTCHAR: if (
self.buffer[i] == SS_STARTCHAR
and self.buffer[i + SS_MSGLENGTH] == SS_STARTCHAR
):
i += 1 i += 1
if (self.buffer[i]>>7): if self.buffer[i] >> 7:
#Long message # Long message
payload = self.buffer[i:i+14] payload = self.buffer[i : i + 14]
else: else:
#Short message # Short message
payload = self.buffer[i:i+7] payload = self.buffer[i : i + 7]
msg = ''.join('%02X' % j for j in payload) msg = "".join("%02X" % j for j in payload)
i += 14 #Both message types use 14 bytes i += 14 # Both message types use 14 bytes
tsbin = self.buffer[i:i+6] tsbin = self.buffer[i : i + 6]
sec = ( (tsbin[0] & 0x7f) << 10) | (tsbin[1] << 2 ) | (tsbin[2] >> 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] nano = (
ts = sec + nano*1.0e-9 ((tsbin[2] & 0x3F) << 24)
| (tsbin[3] << 16)
| (tsbin[4] << 8)
| tsbin[5]
)
ts = sec + nano * 1.0e-9
i += 6 i += 6
#Signal and noise level - Don't care for now # Signal and noise level - Don't care for now
i += 3 i += 3
self.buffer = self.buffer[SS_MSGLENGTH:] self.buffer = self.buffer[SS_MSGLENGTH:]
messages.append( [msg,ts] ) messages.append([msg, ts])
else: else:
self.buffer = self.buffer[1:] self.buffer = self.buffer[1:]
return messages return messages
@@ -243,12 +253,14 @@ class BaseClient(Thread):
for msg, t in messages: for msg, t in messages:
print("%15.9f %s" % (t, msg)) print("%15.9f %s" % (t, msg))
def run(self): def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
sock = self.connect() self.raw_pipe_in = raw_pipe_in
self.stop_flag = stop_flag
self.connect()
while True: while True:
try: try:
received = sock.recv(1024) received = [i for i in self.socket.recv(4096)]
if PY_VERSION == 2: if PY_VERSION == 2:
received = [ord(i) for i in received] received = [ord(i) for i in received]
@@ -256,16 +268,11 @@ class BaseClient(Thread):
self.buffer.extend(received) self.buffer.extend(received)
# print(''.join(x.encode('hex') for x in self.buffer)) # print(''.join(x.encode('hex') for x in self.buffer))
# process self.buffer when it is longer enough if self.datatype == "beast":
# if len(self.buffer) < 2048:
# continue
# -- Removed!! Cause delay in low data rate scenario --
if self.rawtype == 'beast':
messages = self.read_beast_buffer() messages = self.read_beast_buffer()
elif self.rawtype == 'avr': elif self.datatype == "raw":
messages = self.read_avr_buffer() messages = self.read_raw_buffer()
elif self.rawtype == 'skysense': elif self.datatype == "skysense":
messages = self.read_skysense_buffer() messages = self.read_skysense_buffer()
if not messages: if not messages:
@@ -273,30 +280,18 @@ class BaseClient(Thread):
else: else:
self.handle_messages(messages) self.handle_messages(messages)
time.sleep(0.001) # raise RuntimeError("test exception")
except Exception as e: except Exception as e:
tb = traceback.format_exc()
# Provides the user an option to supply the environment exception_queue.put(tb)
# variable PYMODES_DEBUG to halt the execution raise e
# for debugging purposes
debug_intent = os.environ.get('PYMODES_DEBUG', 'false')
if debug_intent.lower() == 'true':
traceback.print_exc()
sys.exit()
else:
print("Unexpected Error:", e)
try:
sock = self.connect()
except Exception as e:
print("Unexpected Error:", e)
if __name__ == '__main__': if __name__ == "__main__":
# for testing purpose only # for testing purpose only
host = sys.argv[1] host = sys.argv[1]
port = int(sys.argv[2]) port = int(sys.argv[2])
rawtype = sys.argv[3] datatype = sys.argv[3]
client = BaseClient(host=host, port=port, rawtype=rawtype) client = TcpClient(host=host, port=port, datatype=datatype)
client.daemon = True
client.run() client.run()

288
pyModeS/streamer/decode.py Normal file
View File

@@ -0,0 +1,288 @@
import os
import time
import datetime
import csv
import pyModeS as pms
class Decode:
def __init__(self, latlon=None, dumpto=None):
self.acs = dict()
if latlon is not None:
self.lat0 = float(latlon[0])
self.lon0 = float(latlon[1])
else:
self.lat0 = None
self.lon0 = None
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_msg, commb_ts, commb_msg, tnow=None):
"""process a chunk of adsb and commb messages received 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_msg):
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_msg):
icao = pms.icao(msg)
if icao not in self.acs:
continue
self.acs[icao]["live"] = int(t)
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 memory"""
acs = self.acs
return acs
def run(self, raw_pipe_out, ac_pipe_in, exception_queue):
local_buffer = []
while True:
try:
while raw_pipe_out.poll():
data = raw_pipe_out.recv()
local_buffer.append(data)
for data in local_buffer:
self.process_raw(
data["adsb_ts"],
data["adsb_msg"],
data["commb_ts"],
data["commb_msg"],
)
local_buffer = []
acs = self.get_aircraft()
ac_pipe_in.send(acs)
time.sleep(0.001)
except Exception as e:
tb = traceback.format_exc()
exception_queue.put((e, tb))

View File

@@ -1,120 +1,154 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function, division
import os import os
import sys import sys
import time import time
import argparse import argparse
import curses import curses
from threading import Lock import signal
import pyModeS as pms import multiprocessing
from pyModeS.extra.tcpclient import BaseClient from pyModeS.streamer.decode import Decode
from pyModeS.streamer.stream import Stream
from pyModeS.streamer.screen import Screen from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource
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 # redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, 'w') sys.stdout = open(os.devnull, "w")
client = ModesClient(host=SERVER, port=PORT, rawtype=RAWTYPE) support_rawtypes = ["raw", "beast", "skysense"]
client.daemon = True
client.start()
stream = Stream(lat0=LAT0, lon0=LON0, dumpto=DUMPTO) parser = argparse.ArgumentParser()
parser.add_argument(
"--source",
help='Choose data source, "rtlsdr" or "net"',
required=True,
default="net",
)
parser.add_argument(
"--connect",
help="Define server, port and data type. Supported data types are: %s"
% support_rawtypes,
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
required=False,
)
parser.add_argument(
"--latlon",
help="Receiver latitude and longitude, needed for the surface position, default none",
nargs=2,
metavar=("LAT", "LON"),
default=None,
required=False,
)
parser.add_argument(
"--show-uncertainty",
dest="uncertainty",
help="Display uncertainty values, default off",
action="store_true",
required=False,
default=False,
)
parser.add_argument(
"--dumpto",
help="Folder to dump decoded output, default none",
required=False,
default=None,
)
args = parser.parse_args()
try: SOURCE = args.source
screen = Screen(uncertainty=UNCERTAINTY) LATLON = args.latlon
screen.daemon = True UNCERTAINTY = args.uncertainty
screen.start() DUMPTO = args.dumpto
while True: if SOURCE == "rtlsdr":
if len(ADSB_MSG) > 200: pass
LOCK.acquire() elif SOURCE == "net":
stream.process_raw(ADSB_TS, ADSB_MSG, COMMB_TS, COMMB_MSG) if args.connect is None:
ADSB_MSG = [] print("Error: --connect argument must not be empty.")
ADSB_TS = [] else:
COMMB_MSG = [] SERVER, PORT, DATATYPE = args.connect
COMMB_TS = [] if DATATYPE not in support_rawtypes:
LOCK.release() print("Data type not supported, available ones are %s" % support_rawtypes)
acs = stream.get_aircraft() else:
try: print('Source must be "rtlsdr" or "net".')
screen.update_data(acs) sys.exit(1)
screen.update()
time.sleep(0.02)
except KeyboardInterrupt:
raise
except:
continue
except KeyboardInterrupt: 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)
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
exception_queue = multiprocessing.Queue()
stop_flag = multiprocessing.Value("b", False)
if SOURCE == "net":
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
elif SOURCE == "rtlsdr":
source = RtlSdrSource()
recv_process = multiprocessing.Process(
target=source.run, args=(raw_pipe_in, stop_flag, exception_queue)
)
decode = Decode(latlon=LATLON, dumpto=DUMPTO)
decode_process = multiprocessing.Process(
target=decode.run, args=(raw_pipe_out, ac_pipe_in, exception_queue)
)
screen = Screen(uncertainty=UNCERTAINTY)
screen_process = multiprocessing.Process(
target=screen.run, args=(ac_pipe_out, exception_queue)
)
def shutdown():
stop_flag.value = True
curses.endwin()
sys.stdout = sys.__stdout__
recv_process.terminate()
decode_process.terminate()
screen_process.terminate()
recv_process.join()
decode_process.join()
screen_process.join()
def closeall(signal, frame):
print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal))
shutdown()
sys.exit(0) sys.exit(0)
finally:
curses.endwin() signal.signal(signal.SIGINT, closeall)
recv_process.start()
decode_process.start()
screen_process.start()
while True:
if (
(not recv_process.is_alive())
or (not decode_process.is_alive())
or (not screen_process.is_alive())
):
shutdown()
while not exception_queue.empty():
trackback = exception_queue.get()
print(trackback)
sys.exit(1)
time.sleep(0.01)

View File

@@ -1,46 +1,46 @@
from __future__ import print_function, division
import os
import curses import curses
import numpy as np import numpy as np
import time import time
from threading import Thread import threading
import traceback
COLUMNS = [ COLUMNS = [
('call', 10), ("call", 10),
('lat', 10), ("lat", 10),
('lon', 10), ("lon", 10),
('alt', 7), ("alt", 7),
('gs', 5), ("gs", 5),
('tas', 5), ("tas", 5),
('ias', 5), ("ias", 5),
('mach', 7), ("mach", 7),
('roc', 7), ("roc", 7),
('trk', 10), ("trk", 10),
('hdg', 10), ("hdg", 10),
('live', 6), ("live", 6),
] ]
UNCERTAINTY_COLUMNS = [ UNCERTAINTY_COLUMNS = [
('|', 5), ("|", 5),
('ver', 4), ("ver", 4),
('HPL', 5), ("HPL", 5),
('RCu', 5), ("RCu", 5),
('RCv', 5), ("RCv", 5),
('HVE', 5), ("HVE", 5),
('VVE', 5), ("VVE", 5),
('Rc', 4), ("Rc", 4),
('VPL', 5), ("VPL", 5),
('EPU', 5), ("EPU", 5),
('VEPU', 6), ("VEPU", 6),
('HFOMr', 7), ("HFOMr", 7),
('VFOMr', 7), ("VFOMr", 7),
('PE_RCu', 8), ("PE_RCu", 8),
('PE_VPL', 8), ("PE_VPL", 8),
] ]
class Screen(Thread):
class Screen(object):
def __init__(self, uncertainty=False): def __init__(self, uncertainty=False):
Thread.__init__(self) super(Screen, self).__init__()
self.screen = curses.initscr() self.screen = curses.initscr()
curses.noecho() curses.noecho()
curses.mousemask(1) curses.mousemask(1)
@@ -55,16 +55,20 @@ class Screen(Thread):
if uncertainty: if uncertainty:
self.columns.extend(UNCERTAINTY_COLUMNS) self.columns.extend(UNCERTAINTY_COLUMNS)
def reset_cursor_pos(self): def reset_cursor_pos(self):
self.screen.move(self.y, self.x) self.screen.move(self.y, self.x)
def update_data(self, acs): def update_ac(self, acs):
self.acs = acs self.acs = acs
def draw_frame(self): def draw_frame(self):
self.screen.border(0) self.screen.border(0)
self.screen.addstr(0, 2, "Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)" % len(self.acs)) self.screen.addstr(
0,
2,
"Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)"
% len(self.acs),
)
def update(self): def update(self):
if len(self.acs) == 0: if len(self.acs) == 0:
@@ -81,21 +85,20 @@ class Screen(Thread):
row = 1 row = 1
header = ' icao' header = " icao"
for c, cw in self.columns: for c, cw in self.columns:
header += (cw-len(c))*' ' + c header += (cw - len(c)) * " " + c
# fill end with spaces # fill end with spaces
header += (self.scr_w - 2 - len(header)) * ' ' header += (self.scr_w - 2 - len(header)) * " "
if len(header) > self.scr_w - 2: if len(header) > self.scr_w - 2:
header = header[:self.scr_w-3] + '>' header = header[: self.scr_w - 3] + ">"
self.screen.addstr(row, 1, header) self.screen.addstr(row, 1, header)
row +=1 row += 1
self.screen.addstr(row, 1, '-'*(self.scr_w-2)) self.screen.addstr(row, 1, "-" * (self.scr_w - 2))
icaos = np.array(list(self.acs.keys())) icaos = np.array(list(self.acs.keys()))
icaos = np.sort(icaos) icaos = np.sort(icaos)
@@ -105,10 +108,10 @@ class Screen(Thread):
idx = row + self.offset - 3 idx = row + self.offset - 3
if idx > len(icaos) - 1: if idx > len(icaos) - 1:
line = ' '*(self.scr_w-2) line = " " * (self.scr_w - 2)
else: else:
line = '' line = ""
icao = icaos[idx] icao = icaos[idx]
ac = self.acs[icao] ac = self.acs[icao]
@@ -116,22 +119,22 @@ class Screen(Thread):
line += icao line += icao
for c, cw in self.columns: for c, cw in self.columns:
if c=='|': if c == "|":
val = '|' val = "|"
elif c=='live': elif c == "live":
val = str(int(time.time() - ac[c]))+'s' val = str(ac[c] - int(time.time())) + "s"
elif ac[c] is None: elif ac[c] is None:
val = '' val = ""
else: else:
val = ac[c] val = ac[c]
val_str = str(val) val_str = str(val)
line += (cw-len(val_str))*' ' + val_str line += (cw - len(val_str)) * " " + val_str
# fill end with spaces # fill end with spaces
line += (self.scr_w - 2 - len(line)) * ' ' line += (self.scr_w - 2 - len(line)) * " "
if len(line) > self.scr_w - 2: if len(line) > self.scr_w - 2:
line = line[:self.scr_w-3] + '>' line = line[: self.scr_w - 3] + ">"
if (icao is not None) and (self.lock_icao == icao): if (icao is not None) and (self.lock_icao == icao):
self.screen.addstr(row, 1, line, curses.A_STANDOUT) self.screen.addstr(row, 1, line, curses.A_STANDOUT)
@@ -140,15 +143,15 @@ class Screen(Thread):
else: else:
self.screen.addstr(row, 1, line) self.screen.addstr(row, 1, line)
self.screen.addstr(self.scr_h-3, 1, '-'*(self.scr_w-2)) self.screen.addstr(self.scr_h - 3, 1, "-" * (self.scr_w - 2))
total_page = len(icaos) // (self.scr_h - 4) + 1 total_page = len(icaos) // (self.scr_h - 4) + 1
current_page = self.offset // (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.screen.addstr(self.scr_h - 2, 1, "(%d / %d)" % (current_page, total_page))
self.reset_cursor_pos() self.reset_cursor_pos()
def run(self): def kye_handling(self):
self.draw_frame() self.draw_frame()
self.scr_h, self.scr_w = self.screen.getmaxyx() self.scr_h, self.scr_w = self.screen.getmaxyx()
@@ -168,7 +171,7 @@ class Screen(Thread):
self.offset = offset_intent self.offset = offset_intent
else: else:
self.offset = 0 self.offset = 0
elif c == curses.KEY_DOWN : elif c == curses.KEY_DOWN:
y_intent = self.y + 1 y_intent = self.y + 1
if y_intent < self.scr_h - 3: if y_intent < self.scr_h - 3:
self.y = y_intent self.y = y_intent
@@ -178,6 +181,38 @@ class Screen(Thread):
self.y = y_intent self.y = y_intent
elif c == curses.KEY_ENTER or c == 10 or c == 13: elif c == curses.KEY_ENTER or c == 10 or c == 13:
self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode() self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode()
elif c == 27: # escape key
self.lock_icao = None
elif c == curses.KEY_F5: elif c == curses.KEY_F5:
self.screen.refresh() self.screen.refresh()
self.draw_frame() self.draw_frame()
def run(self, ac_pipe_out, exception_queue):
local_buffer = []
key_thread = threading.Thread(target=self.kye_handling)
key_thread.daemon = True
key_thread.start()
while True:
try:
# raise RuntimeError("test exception")
while ac_pipe_out.poll():
acs = ac_pipe_out.recv()
local_buffer.append(acs)
for acs in local_buffer:
self.update_ac(acs)
local_buffer = []
self.update()
except curses.error:
pass
except Exception as e:
tb = traceback.format_exc()
exception_queue.put(tb)
time.sleep(0.1)
raise e
time.sleep(0.001)

View File

@@ -0,0 +1,91 @@
import pyModeS as pms
from pyModeS.extra.tcpclient import TcpClient
from pyModeS.extra.rtlreader import RtlReader
class NetSource(TcpClient):
def __init__(self, host, port, rawtype):
super(NetSource, self).__init__(host, port, rawtype)
self.reset_local_buffer()
def reset_local_buffer(self):
self.local_buffer_adsb_msg = []
self.local_buffer_adsb_ts = []
self.local_buffer_commb_msg = []
self.local_buffer_commb_ts = []
def handle_messages(self, messages):
if self.stop_flag.value is True:
self.stop()
return
for msg, t in messages:
if len(msg) < 28: # only process long messages
continue
df = pms.df(msg)
if df == 17 or df == 18:
self.local_buffer_adsb_msg.append(msg)
self.local_buffer_adsb_ts.append(t)
elif df == 20 or df == 21:
self.local_buffer_commb_msg.append(msg)
self.local_buffer_commb_ts.append(t)
else:
continue
if len(self.local_buffer_adsb_msg) > 1:
self.raw_pipe_in.send(
{
"adsb_ts": self.local_buffer_adsb_ts,
"adsb_msg": self.local_buffer_adsb_msg,
"commb_ts": self.local_buffer_commb_ts,
"commb_msg": self.local_buffer_commb_msg,
}
)
self.reset_local_buffer()
class RtlSdrSource(RtlReader):
def __init__(self):
super(RtlSdrSource, self).__init__()
self.reset_local_buffer()
def reset_local_buffer(self):
self.local_buffer_adsb_msg = []
self.local_buffer_adsb_ts = []
self.local_buffer_commb_msg = []
self.local_buffer_commb_ts = []
def handle_messages(self, messages):
if self.stop_flag.value is True:
self.stop()
return
for msg, t in messages:
if len(msg) < 28: # only process long messages
continue
df = pms.df(msg)
if df == 17 or df == 18:
self.local_buffer_adsb_msg.append(msg)
self.local_buffer_adsb_ts.append(t)
elif df == 20 or df == 21:
self.local_buffer_commb_msg.append(msg)
self.local_buffer_commb_ts.append(t)
else:
continue
if len(self.local_buffer_adsb_msg) > 1:
self.raw_pipe_in.send(
{
"adsb_ts": self.local_buffer_adsb_ts,
"adsb_msg": self.local_buffer_adsb_msg,
"commb_ts": self.local_buffer_commb_ts,
"commb_msg": self.local_buffer_commb_msg,
}
)
self.reset_local_buffer()

View File

@@ -1,255 +0,0 @@
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

View File

@@ -4,9 +4,9 @@ See:
https://packaging.python.org/en/latest/distributing.html https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject https://github.com/pypa/sampleproject
Steps for deploying a new verison: Steps for deploying a new version:
1. Increase the version number 1. Increase the version number
2. remove the old deployment under [dist] folder 2. remove the old deployment under [dist] and [build] folder
3. run: python setup.py sdist 3. run: python setup.py sdist
run: python setup.py bdist_wheel --universal run: python setup.py bdist_wheel --universal
4. twine upload dist/* 4. twine upload dist/*
@@ -15,6 +15,13 @@ Steps for deploying a new verison:
# Always prefer setuptools over distutils # Always prefer setuptools over distutils
from setuptools import setup, find_packages from setuptools import setup, find_packages
# Compile some parts
from setuptools.extension import Extension
from Cython.Build import cythonize
extensions = [Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"])]
# To use a consistent encoding # To use a consistent encoding
from codecs import open from codecs import open
from os import path from os import path
@@ -30,7 +37,7 @@ setup(
# Versions should comply with PEP440. For a discussion on single-sourcing # Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see # the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html # https://packaging.python.org/en/latest/single_source_version.html
version="2.2", version="2.5",
description="Python Mode-S and ADS-B Decoder", description="Python Mode-S and ADS-B Decoder",
long_description=long_description, long_description=long_description,
# The project's main homepage. # The project's main homepage.
@@ -49,21 +56,17 @@ setup(
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
# Indicate who your project is intended for # Indicate who your project is intended for
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries",
# Pick your license as you wish (should match "license" above) # Pick your license as you wish (should match "license" above)
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
# Specify the Python versions you support here. In particular, ensure # Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both. # that you indicate whether you support Python 2, Python 3 or both.
"Programming Language :: Python :: 2", # "Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
], ],
ext_modules=cythonize(extensions),
# What does your project relate to? # What does your project relate to?
keywords="Mode-S ADS-B EHS decoding", keywords="Mode-S ADS-B EHS ELS Comm-B",
# You can just specify the packages manually here if your project is # You can just specify the packages manually here if your project is
# simple. Or you can use find_packages(). # simple. Or you can use find_packages().
packages=find_packages(exclude=["contrib", "docs", "tests"]), packages=find_packages(exclude=["contrib", "docs", "tests"]),
@@ -74,7 +77,7 @@ setup(
# your project is installed. For an analysis of "install_requires" vs pip's # your project is installed. For an analysis of "install_requires" vs pip's
# requirements files see: # requirements files see:
# https://packaging.python.org/en/latest/requirements.html # https://packaging.python.org/en/latest/requirements.html
install_requires=["numpy", "argparse"], install_requires=["numpy", "pyzmq", "pyrtlsdr"],
# List additional groups of dependencies here (e.g. development # List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax, # dependencies). You can install these using the following syntax,
# for example: # for example:

130
tests/benchmark.py Normal file
View File

@@ -0,0 +1,130 @@
import sys
import time
import pandas as pd
from tqdm import tqdm
from pyModeS.decoder import adsb
fin = sys.argv[1]
df = pd.read_csv(fin, names=["ts", "df", "icao", "msg"])
df_adsb = df[df["df"] == 17].copy()
total = df_adsb.shape[0]
def native():
from pyModeS.decoder import common
# airborne position
m_air_0 = None
m_air_1 = None
# surface position
m_surf_0 = None
m_surf_1 = None
for i, r in tqdm(df_adsb.iterrows(), total=total):
ts = r.ts
m = r.msg
downlink_format = common.df(m)
crc = common.crc(m)
icao = adsb.icao(m)
tc = adsb.typecode(m)
if 1 <= tc <= 4:
category = adsb.category(m)
callsign = adsb.callsign(m)
if tc == 19:
velocity = adsb.velocity(m)
if 5 <= tc <= 8:
if adsb.oe_flag(m):
m_surf_1 = m
t1 = ts
else:
m_surf_0 = m
t0 = ts
if m_surf_0 and m_surf_1:
position = adsb.surface_position(
m_surf_0, m_surf_1, t0, t1, 50.01, 4.35
)
altitude = adsb.altitude(m)
if 9 <= tc <= 18:
if adsb.oe_flag(m):
m_air_1 = m
t1 = ts
else:
m_air_0 = m
t0 = ts
if m_air_0 and m_air_1:
position = adsb.position(m_air_0, m_air_1, t0, t1)
altitude = adsb.altitude(m)
def cython():
from pyModeS.decoder import c_common as common
# airborne position
m_air_0 = None
m_air_1 = None
# surface position
m_surf_0 = None
m_surf_1 = None
for i, r in tqdm(df_adsb.iterrows(), total=total):
ts = r.ts
m = r.msg
downlink_format = common.df(m)
crc = common.crc(m)
icao = adsb.icao(m)
tc = adsb.typecode(m)
if 1 <= tc <= 4:
category = adsb.category(m)
callsign = adsb.callsign(m)
if tc == 19:
velocity = adsb.velocity(m)
if 5 <= tc <= 8:
if adsb.oe_flag(m):
m_surf_1 = m
t1 = ts
else:
m_surf_0 = m
t0 = ts
if m_surf_0 and m_surf_1:
position = adsb.surface_position(
m_surf_0, m_surf_1, t0, t1, 50.01, 4.35
)
altitude = adsb.altitude(m)
if 9 <= tc <= 18:
if adsb.oe_flag(m):
m_air_1 = m
t1 = ts
else:
m_air_0 = m
t0 = ts
if m_air_0 and m_air_1:
position = adsb.position(m_air_0, m_air_1, t0, t1)
altitude = adsb.altitude(m)
if __name__ == "__main__":
t1 = time.time()
native()
dt1 = time.time() - t1
t2 = time.time()
cython()
dt2 = time.time() - t2

View File

@@ -1,25 +1,28 @@
from __future__ import print_function import sys
from pyModeS import adsb, ehs import time
import csv
if len(sys.argv) > 1 and sys.argv[1] == "cython":
from pyModeS.c_decoder import adsb
else:
from pyModeS.decoder import adsb
# === Decode sample data file === print("===== Decode ADS-B sample data=====")
def adsb_decode_all(n=None): f = open("tests/data/sample_data_adsb.csv", "rt")
print("===== Decode ADS-B sample data=====")
import csv
f = open('tests/data/sample_data_adsb.csv', 'rt')
msg0 = None msg0 = None
msg1 = None msg1 = None
for i, r in enumerate(csv.reader(f)): tstart = time.time()
if n and i > n: for i, r in enumerate(csv.reader(f)):
break
ts = int(r[0])
m = r[1].encode()
ts = r[0]
m = r[1]
icao = adsb.icao(m) icao = adsb.icao(m)
tc = adsb.typecode(m) tc = adsb.typecode(m)
if 1 <= tc <= 4: if 1 <= tc <= 4:
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m)) print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
if tc == 19: if tc == 19:
@@ -37,5 +40,7 @@ def adsb_decode_all(n=None):
alt = adsb.altitude(m) alt = adsb.altitude(m)
print(ts, m, icao, tc, pos, alt) print(ts, m, icao, tc, pos, alt)
if __name__ == '__main__':
adsb_decode_all(n=100) dt = time.time() - tstart
print("Execution time: {} seconds".format(dt))

View File

@@ -1,14 +1,14 @@
from __future__ import print_function
from pyModeS import commb, common, bds from pyModeS import commb, common, bds
# === Decode sample data file === # === Decode sample data file ===
def bds_info(BDS, m): def bds_info(BDS, m):
if BDS == "BDS10": if BDS == "BDS10":
info = [commb.ovc10(m)] info = [commb.ovc10(m)]
elif BDS == "BDS17": elif BDS == "BDS17":
info = ([i[-2:] for i in commb.cap17(m)]) info = [i[-2:] for i in commb.cap17(m)]
elif BDS == "BDS20": elif BDS == "BDS20":
info = [commb.cs20(m)] info = [commb.cs20(m)]
@@ -20,13 +20,30 @@ def bds_info(BDS, m):
info = (commb.wind44(m), commb.temp44(m), commb.p44(m), commb.hum44(m)) info = (commb.wind44(m), commb.temp44(m), commb.p44(m), commb.hum44(m))
elif BDS == "BDS44REV": elif BDS == "BDS44REV":
info = (commb.wind44(m, rev=True), commb.temp44(m, rev=True), commb.p44(m, rev=True), commb.hum44(m, rev=True)) info = (
commb.wind44(m, rev=True),
commb.temp44(m, rev=True),
commb.p44(m, rev=True),
commb.hum44(m, rev=True),
)
elif BDS == "BDS50": elif BDS == "BDS50":
info = (commb.roll50(m), commb.trk50(m), commb.gs50(m), commb.rtrk50(m), commb.tas50(m)) info = (
commb.roll50(m),
commb.trk50(m),
commb.gs50(m),
commb.rtrk50(m),
commb.tas50(m),
)
elif BDS == "BDS60": elif BDS == "BDS60":
info = (commb.hdg60(m), commb.ias60(m), commb.mach60(m), commb.vr60baro(m), commb.vr60ins(m)) info = (
commb.hdg60(m),
commb.ias60(m),
commb.mach60(m),
commb.vr60baro(m),
commb.vr60ins(m),
)
else: else:
info = None info = None
@@ -39,8 +56,7 @@ def commb_decode_all(df, n=None):
print("===== Decode Comm-B sample data (DF=%s)=====" % df) print("===== Decode Comm-B sample data (DF=%s)=====" % df)
f = open('tests/data/sample_data_commb_df%s.csv' % df, 'rt') f = open("tests/data/sample_data_commb_df%s.csv" % df, "rt")
for i, r in enumerate(csv.reader(f)): for i, r in enumerate(csv.reader(f)):
if n and i > n: if n and i > n:
@@ -55,21 +71,21 @@ def commb_decode_all(df, n=None):
code = common.altcode(m) if df == 20 else common.idcode(m) code = common.altcode(m) if df == 20 else common.idcode(m)
if not BDS: if not BDS:
print(ts, m, icao, df, '%5s'%code, 'UNKNOWN') print(ts, m, icao, df, "%5s" % code, "UNKNOWN")
continue continue
if len(BDS.split(",")) > 1: if len(BDS.split(",")) > 1:
print(ts, m, icao, df, '%5s' % code, end=' ') print(ts, m, icao, df, "%5s" % code, end=" ")
for i, _bds in enumerate(BDS.split(",")): for i, _bds in enumerate(BDS.split(",")):
if i == 0: if i == 0:
print(_bds, *bds_info(_bds, m)) print(_bds, *bds_info(_bds, m))
else: else:
print(' ' * 55, _bds, *bds_info(_bds, m)) print(" " * 55, _bds, *bds_info(_bds, m))
else: else:
print(ts, m, icao, df, '%5s'%code, BDS, *bds_info(BDS, m)) print(ts, m, icao, df, "%5s" % code, BDS, *bds_info(BDS, m))
if __name__ == '__main__': if __name__ == "__main__":
commb_decode_all(df=20, n=100) commb_decode_all(df=20, n=100)
commb_decode_all(df=21, n=100) commb_decode_all(df=21, n=100)

View File

@@ -2,12 +2,13 @@ from pyModeS import adsb
# === TEST ADS-B package === # === TEST ADS-B package ===
def test_adsb_icao(): def test_adsb_icao():
assert adsb.icao("8D406B902015A678D4D220AA4BDA") == "406B90" assert adsb.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
def test_adsb_category(): def test_adsb_category():
assert adsb.category("8D406B902015A678D4D220AA4BDA") == 5 assert adsb.category("8D406B902015A678D4D220AA4BDA") == 0
def test_adsb_callsign(): def test_adsb_callsign():
@@ -15,9 +16,22 @@ def test_adsb_callsign():
def test_adsb_position(): def test_adsb_position():
pos = adsb.position("8D40058B58C901375147EFD09357", pos = adsb.position(
"8D40058B58C901375147EFD09357",
"8D40058B58C904A87F402D3B8C59", "8D40058B58C904A87F402D3B8C59",
1446332400, 1446332405) 1446332400,
1446332405,
)
assert pos == (49.81755, 6.08442)
def test_adsb_position_swap_odd_even():
pos = adsb.position(
"8D40058B58C904A87F402D3B8C59",
"8D40058B58C901375147EFD09357",
1446332405,
1446332400,
)
assert pos == (49.81755, 6.08442) assert pos == (49.81755, 6.08442)
@@ -29,27 +43,29 @@ def test_adsb_position_with_ref():
def test_adsb_airborne_position_with_ref(): def test_adsb_airborne_position_with_ref():
pos = adsb.airborne_position_with_ref("8D40058B58C901375147EFD09357", pos = adsb.airborne_position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0)
49.0, 6.0)
assert pos == (49.82410, 6.06785) assert pos == (49.82410, 6.06785)
pos = adsb.airborne_position_with_ref("8D40058B58C904A87F402D3B8C59", pos = adsb.airborne_position_with_ref("8D40058B58C904A87F402D3B8C59", 49.0, 6.0)
49.0, 6.0)
assert pos == (49.81755, 6.08442) assert pos == (49.81755, 6.08442)
def test_adsb_surface_position_with_ref(): def test_adsb_surface_position_with_ref():
pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B000000", pos = adsb.surface_position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5)
-43.5, 172.5)
assert pos == (-43.48564, 172.53942) assert pos == (-43.48564, 172.53942)
def test_adsb_surface_position(): def test_adsb_surface_position():
pos = adsb.surface_position("8CC8200A3AC8F009BCDEF2000000", pos = adsb.surface_position(
"8CC8200A3AC8F009BCDEF2000000",
"8FC8200A3AB8F5F893096B000000", "8FC8200A3AB8F5F893096B000000",
0, 2, 0,
-43.496, 172.558) 2,
-43.496,
172.558,
)
assert pos == (-43.48564, 172.53942) assert pos == (-43.48564, 172.53942)
def test_adsb_alt(): def test_adsb_alt():
assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000 assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000
@@ -58,10 +74,10 @@ def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F") vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F") vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000") vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, 182.88, -832, 'GS') assert vgs == (159, 182.88, -832, "GS")
assert vas == (375, 243.98, -2304, 'TAS') assert vas == (375, 243.98, -2304, "TAS")
assert vgs_surface == (19.0, 42.2, 0 , 'GS') assert vgs_surface == (19.0, 42.2, 0, "GS")
assert adsb.altitude_diff('8D485020994409940838175B284F') == 550 assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
# def test_nic(): # def test_nic():

View File

@@ -1,20 +1,36 @@
from pyModeS import bds 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' def test_bds_infer():
assert bds.infer("A0000638FA81C10000000081A92F") == 'BDS17' assert bds.infer("8D406B902015A678D4D220AA4BDA") == "BDS08"
assert bds.infer("A0001838201584F23468207CDFA5") == 'BDS20' assert bds.infer("8FC8200A3AB8F5F893096B000000") == "BDS06"
assert bds.infer("A0001839CA3800315800007448D9") == 'BDS40' assert bds.infer("8D40058B58C901375147EFD09357") == "BDS05"
assert bds.infer("A000139381951536E024D4CCF6B5") == 'BDS50' assert bds.infer("8D485020994409940838175B284F") == "BDS09"
assert bds.infer("A00004128F39F91A7E27C46ADC21") == 'BDS60'
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(): def test_bds_is50or60():
assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
assert bds.is50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == 'BDS50' assert bds.is50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == "BDS50"
assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == 'BDS60' assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == "BDS60"
def test_surface_position():
msg0 = "8FE48C033A9FA184B934E744C6FD"
msg1 = "8FE48C033A9FA68F7C3D39B1C2F0"
t0 = 1565608663102
t1 = 1565608666214
lat_ref = -23.4265448
lon_ref = -46.4816258
lat, lon = bds.bds06.surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
assert abs(lon_ref - lon) < 0.05

58
tests/test_c_common.py Normal file
View File

@@ -0,0 +1,58 @@
try:
from pyModeS.decoder import c_common as common
def test_conversions():
assert common.hex2bin("6E406B") == "011011100100000001101011"
def test_crc_decode():
assert common.crc("8D406B902015A678D4D220AA4BDA") == 0
assert common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
assert common.crc("8d45cab390c39509496ca9a32912") == 0
assert common.crc("8d74802958c904e6ef4ba0184d5c") == 0
assert common.crc("8d4400cd9b0000b4f87000e71a10") == 0
assert common.crc("8d4065de58a1054a7ef0218e226a") == 0
assert common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
assert common.crc("a800089d8094e33a6004e4b8a522") == 4805588
assert common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
assert common.crc("a0000410bc900010a40000f5f477") == 11727682
assert common.crc("8d4ca251204994b1c36e60a5343d") == 16
assert common.crc("b0001718c65632b0a82040715b65") == 353333
def test_crc_encode():
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert parity == 11160538
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
except:
pass

View File

@@ -1,12 +1,14 @@
from pyModeS import bds, commb from pyModeS import bds, commb
# from pyModeS import ehs, els # deprecated # 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_' def test_bds20_callsign():
assert commb.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_' 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(): def test_bds40_functions():

View File

@@ -2,57 +2,61 @@ from pyModeS import common
def test_conversions(): def test_conversions():
assert common.hex2bin('6E406B') == "011011100100000001101011" assert common.hex2bin("6E406B") == "011011100100000001101011"
assert common.bin2hex('011011100100000001101011') == "6E406B"
assert common.int2hex(11160538) == "AA4BDA"
def test_crc_decode(): def test_crc_decode():
assert common.crc_legacy("8D406B902015A678D4D220AA4BDA") == 0 assert common.crc_legacy("8D406B902015A678D4D220AA4BDA") == 0
assert common.crc("8D406B902015A678D4D220AA4BDA") == 0 assert common.crc("8D406B902015A678D4D220AA4BDA") == 0
assert common.crc('8d8960ed58bf053cf11bc5932b7d') == 0 assert common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
assert common.crc('8d45cab390c39509496ca9a32912') == 0 assert common.crc("8d45cab390c39509496ca9a32912") == 0
assert common.crc('8d49d3d4e1089d00000000744c3b') == 0 assert common.crc("8d49d3d4e1089d00000000744c3b") == 0
assert common.crc('8d74802958c904e6ef4ba0184d5c') == 0 assert common.crc("8d74802958c904e6ef4ba0184d5c") == 0
assert common.crc('8d4400cd9b0000b4f87000e71a10') == 0 assert common.crc("8d4400cd9b0000b4f87000e71a10") == 0
assert common.crc('8d4065de58a1054a7ef0218e226a') == 0 assert common.crc("8d4065de58a1054a7ef0218e226a") == 0
assert common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
assert common.crc("a800089d8094e33a6004e4b8a522") == 4805588
assert common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
assert common.crc("a0000410bc900010a40000f5f477") == 11727682
assert common.crc("8d4ca251204994b1c36e60a5343d") == 16
assert common.crc("b0001718c65632b0a82040715b65") == 353333
assert common.crc('c80b2dca34aa21dd821a04cb64d4') == 10719924
assert common.crc('a800089d8094e33a6004e4b8a522') == 4805588
assert common.crc('a8000614a50b6d32bed000bbe0ed') == 5659991
assert common.crc('a0000410bc900010a40000f5f477') == 11727682
assert common.crc('8d4ca251204994b1c36e60a5343d') == 16
assert common.crc('b0001718c65632b0a82040715b65') == 353333
def test_crc_encode(): def test_crc_encode():
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True) parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert common.int2hex(parity) == "AA4BDA" assert parity == 11160538
def test_icao(): def test_icao():
assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90" assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
assert common.icao("A0001839CA3800315800007448D9") == '400940' assert common.icao("A0001839CA3800315800007448D9") == "400940"
assert common.icao("A000139381951536E024D4CCF6B5") == '3C4DD2' assert common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2"
assert common.icao("A000029CFFBAA11E2004727281F1") == '4243D0' assert common.icao("A000029CFFBAA11E2004727281F1") == "4243D0"
def test_modes_altcode(): def test_modes_altcode():
assert common.altcode("A02014B400000000000000F9D514") == 32300 assert common.altcode("A02014B400000000000000F9D514") == 32300
def test_modes_idcode(): def test_modes_idcode():
assert common.idcode("A800292DFFBBA9383FFCEB903D01") == '1346' assert common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346"
def test_graycode_to_altitude(): def test_graycode_to_altitude():
assert common.gray2alt('00000000010') == -1000 assert common.gray2alt("00000000010") == -1000
assert common.gray2alt('00000001010') == -500 assert common.gray2alt("00000001010") == -500
assert common.gray2alt('00000011011') == -100 assert common.gray2alt("00000011011") == -100
assert common.gray2alt('00000011010') == 0 assert common.gray2alt("00000011010") == 0
assert common.gray2alt('00000011110') == 100 assert common.gray2alt("00000011110") == 100
assert common.gray2alt('00000010011') == 600 assert common.gray2alt("00000010011") == 600
assert common.gray2alt('00000110010') == 1000 assert common.gray2alt("00000110010") == 1000
assert common.gray2alt('00001001001') == 5800 assert common.gray2alt("00001001001") == 5800
assert common.gray2alt('00011100100') == 10300 assert common.gray2alt("00011100100") == 10300
assert common.gray2alt('01100011010') == 32000 assert common.gray2alt("01100011010") == 32000
assert common.gray2alt('01110000100') == 46300 assert common.gray2alt("01110000100") == 46300
assert common.gray2alt('01010101100') == 50200 assert common.gray2alt("01010101100") == 50200
assert common.gray2alt('11011110100') == 73200 assert common.gray2alt("11011110100") == 73200
assert common.gray2alt('10000000011') == 126600 assert common.gray2alt("10000000011") == 126600
assert common.gray2alt('10000000001') == 126700 assert common.gray2alt("10000000001") == 126700

23
tests/test_encoder.py Normal file
View File

@@ -0,0 +1,23 @@
from pyModeS import encoder
def test_identification():
msg = encoder.encode_adsb(
icao="406B90", typecode=4, capability=5, category=0, callsign="EZY85MH"
)
assert msg == "8D406B902015A678D4D220AA4BDA"
def test_speed():
msg = encoder.encode_adsb(
icao="485020",
typecode=19,
capability=5,
speed_type="gs",
speed=159,
angle=182.88,
vertical_rate=-832,
vertical_rate_source="gnss",
gnss_baro_alt_diff=550,
)
assert msg == "8D485020994409940838175B284F"

20
tests/test_tell.py Normal file
View File

@@ -0,0 +1,20 @@
from pyModeS.decoder import tell
messages = [
"8D406B902015A678D4D220AA4BDA",
"8FC8200A3AB8F5F893096B000000",
"8D40058B58C901375147EFD09357",
"8D485020994409940838175B284F",
"A000083E202CC371C31DE0AA1CCF",
"A8001E2520053332C1A820363386",
"A000029C85E42F313000007047D3",
"A5DC282C2A0108372CA6DA9693B0",
"A00015B8C26A00328400004242DA",
"A000139381951536E024D4CCF6B5",
"A00004128F39F91A7E27C46ADC21",
]
print("-" * 70)
for m in messages:
tell(m)
print("-" * 70)

View File

@@ -1,9 +0,0 @@
[tox]
toxworkdir = /tmp/tox
envlist = py2,py3
[testenv]
deps =
pytest
numpy
commands = py.test