Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff19b54868 | ||
|
|
a30565b4ef | ||
|
|
64923438a1 | ||
|
|
f9225bf375 | ||
|
|
67ced74c0a | ||
|
|
5a703a0d3c | ||
|
|
90f35a8f29 | ||
|
|
87e6f50486 | ||
|
|
0667c1b3c3 | ||
|
|
9d82f9b9c9 | ||
|
|
51222372b1 | ||
|
|
d3607a16c1 | ||
|
|
6a427c08b5 | ||
|
|
afd53af87b | ||
|
|
6b4a5bdaf3 | ||
|
|
3e0d7ceec5 | ||
|
|
130491e641 | ||
|
|
1f620d6534 | ||
|
|
b4d4ef8f1f | ||
|
|
bb8f83e832 | ||
|
|
b90932e51d | ||
|
|
b36540e2a1 | ||
|
|
c02414417d | ||
|
|
13f4fe77bb | ||
|
|
401f3f5e95 | ||
|
|
73c3a75234 | ||
|
|
06941bac2e | ||
|
|
6e33ac0288 | ||
|
|
65ce1a62c4 | ||
|
|
5dc74a5548 | ||
|
|
e7edba9e25 | ||
|
|
ce5adbafe5 | ||
|
|
b52431ca51 | ||
|
|
00f04a8886 | ||
|
|
0eb2d1f6a2 | ||
|
|
26ef5d3ad9 | ||
|
|
b90c2e740d | ||
|
|
e39baf38ed | ||
|
|
b9471d7fcb | ||
|
|
a350050e6e | ||
|
|
01574a9f01 | ||
|
|
cdb7aef82e | ||
|
|
a0a8c9b2f7 | ||
|
|
0b2648bfe0 | ||
|
|
69ce39ab39 | ||
|
|
83e22892ba | ||
|
|
7434fc9ed3 | ||
|
|
7260bff7e9 | ||
|
|
dd1fd596f8 | ||
|
|
b9089d55d2 | ||
|
|
6ab147bffe | ||
|
|
a308b9a7e0 | ||
|
|
96f49a00e4 | ||
|
|
b8f8f4dbc0 | ||
|
|
a4ce3bfaf1 | ||
|
|
3bb8c361e9 | ||
|
|
89e67fae31 | ||
|
|
50cef7d424 | ||
|
|
20801883d5 | ||
|
|
56d70b03d2 | ||
|
|
46ffb79234 | ||
|
|
47de4d1163 | ||
|
|
2978329d03 | ||
|
|
4471c836d9 | ||
|
|
d16484bf72 | ||
|
|
436e9ce2c8 | ||
|
|
4a6341bcc0 | ||
|
|
90e60a74b5 | ||
|
|
f65ffd13f7 | ||
|
|
a3e5e7e141 | ||
|
|
668d6580d0 | ||
|
|
eacc828b75 | ||
|
|
4ba900f500 | ||
|
|
35c0bfa513 | ||
|
|
beb50fc788 | ||
|
|
fc135eacef | ||
|
|
84c6255567 | ||
|
|
224a0b8a67 | ||
|
|
c3976f1ca3 | ||
|
|
dd3a869c08 | ||
|
|
7e966090ab | ||
|
|
7a4c465f7d | ||
|
|
0ced97e3eb | ||
|
|
2bdd638cef | ||
|
|
93fc536926 | ||
|
|
1f15a953e3 | ||
|
|
0d7f628f3c | ||
|
|
7348a10f1b | ||
|
|
2e9833148b | ||
|
|
c25c9d6b96 | ||
|
|
c9d2c4c6bc | ||
|
|
0c5539be3a | ||
|
|
1cf4d37e56 | ||
|
|
2fc376abae | ||
|
|
9c660578ad | ||
|
|
ba84cd7c39 | ||
|
|
2085a2c432 | ||
|
|
4126dedd19 | ||
|
|
13f2071bf0 | ||
|
|
3d0c4f0240 | ||
|
|
643f14e725 | ||
|
|
7892aac4de | ||
|
|
7dcff01910 | ||
|
|
228306dd5f | ||
|
|
1c6b39322f | ||
|
|
9dde1b63d6 | ||
|
|
560b737739 | ||
|
|
d50f05d00d | ||
|
|
11a560dbb5 | ||
|
|
b51facbdc6 | ||
|
|
d77d1f7f6b | ||
|
|
5286355bf6 | ||
|
|
fe9e033a64 | ||
|
|
57307109e6 | ||
|
|
dc8f194e6e | ||
|
|
6571fe6fc0 | ||
|
|
aa64d4e7a9 | ||
|
|
db57d7419f | ||
|
|
ed18352c0c | ||
|
|
a75d6fd050 | ||
|
|
0ff628bb8e | ||
|
|
ea7653ef79 | ||
|
|
2046b1de07 | ||
|
|
768b80df8e | ||
|
|
e52d43f963 | ||
|
|
b60b31d4fb | ||
|
|
cb67c63326 | ||
|
|
bddaf9aec6 | ||
|
|
00fc1475ff | ||
|
|
489c405de0 | ||
|
|
ec2721cfdc | ||
|
|
2f13524a7c | ||
|
|
fb24d4f25c | ||
|
|
9ad535dc93 | ||
|
|
9a89766766 | ||
|
|
26173b4038 | ||
|
|
3619d52760 | ||
|
|
e2ece806c2 | ||
|
|
a683e40c41 | ||
|
|
0c1a3b06e1 | ||
|
|
f960cd71bc | ||
|
|
bd54ac1d10 | ||
|
|
2c1db13122 | ||
|
|
695fc34988 | ||
|
|
0eb333ba8c | ||
|
|
d058e9f8b3 | ||
|
|
3d99deb049 | ||
|
|
03f81d120b | ||
|
|
b9b95320d8 | ||
|
|
0ea2cf7ade | ||
|
|
458b02028d | ||
|
|
b062bdf998 | ||
|
|
cfcd21b692 | ||
|
|
86f302f05e | ||
|
|
28a6e53d49 | ||
|
|
81d7cef6e8 | ||
|
|
4906a49e9c | ||
|
|
7cb75ea8ca | ||
|
|
785584aff5 | ||
|
|
fdc34497c0 | ||
|
|
4a3c9438f7 | ||
|
|
b723374337 | ||
|
|
8f4dff5b30 | ||
|
|
7267859548 | ||
|
|
7a3bd089c4 | ||
|
|
17fdaca1c7 | ||
|
|
6f6b50776d | ||
|
|
0b7b9ad3dd | ||
|
|
24806f7e88 | ||
|
|
57ad40ec57 | ||
|
|
d230bdc4a9 | ||
|
|
4a6d3334a7 | ||
|
|
9f371fe86b | ||
|
|
0ea4969393 | ||
|
|
312c77629b | ||
|
|
6159691c3d | ||
|
|
7edbf3fd30 | ||
|
|
e19d0cd945 | ||
|
|
6fc68841ce | ||
|
|
f27fe6c8aa | ||
|
|
81a46e5070 | ||
|
|
6c2adbe990 | ||
|
|
3c14579040 | ||
|
|
fc9b05b6f1 | ||
|
|
b813e41343 | ||
|
|
c9159feb7d | ||
|
|
8cd5655a04 | ||
|
|
8ded3500d4 | ||
|
|
c348a2295d | ||
|
|
040a1879c9 | ||
|
|
61b55531e8 | ||
|
|
abdafd7dea | ||
|
|
b97299ce1b | ||
|
|
652ef65bbb | ||
|
|
bfdb8221a0 | ||
|
|
32e6ee3904 | ||
|
|
d82dfd5537 |
12
.coveragerc
Normal file
12
.coveragerc
Normal file
@@ -0,0 +1,12 @@
|
||||
[run]
|
||||
branch = True
|
||||
include = */pyModeS/*
|
||||
omit = *tests*
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
coverage: ignore
|
||||
raise NotImplementedError
|
||||
if TYPE_CHECKING:
|
||||
|
||||
ignore_errors = True
|
||||
29
.github/workflows/pypi-publish.yml
vendored
Normal file
29
.github/workflows/pypi-publish.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# This workflows will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: PyPI Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
56
.github/workflows/run-tests.yml
vendored
Normal file
56
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -U pip numpy cython mypy
|
||||
pip install -U pytest codecov pytest-cov
|
||||
pip install .
|
||||
|
||||
- name: Type checking
|
||||
if: ${{ env.PYTHON_VERSION != '3.7' }}
|
||||
run: |
|
||||
mypy pyModeS
|
||||
|
||||
- name: Run tests (without Cython)
|
||||
run: |
|
||||
pytest tests --cov --cov-report term-missing
|
||||
|
||||
- name: Install with Cython
|
||||
run: |
|
||||
pip install -U cython
|
||||
pip uninstall -y pymodes
|
||||
pip install .
|
||||
|
||||
- name: Run tests (with Cython)
|
||||
run: |
|
||||
pytest tests
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: ${{ github.event_name != 'pull_request_target' && env.PYTHON_VERSION == '3.10' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
env_vars: PYTHON_VERSION
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -58,3 +58,9 @@ target/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
|
||||
26
Makefile
Normal file
26
Makefile
Normal file
@@ -0,0 +1,26 @@
|
||||
install:
|
||||
pip install . --upgrade
|
||||
|
||||
uninstall:
|
||||
pip uninstall pyModeS -y
|
||||
|
||||
ext:
|
||||
python setup.py build_ext --inplace
|
||||
|
||||
test:
|
||||
make clean
|
||||
@echo ""
|
||||
@echo "[Test with py_common]"
|
||||
python -m pytest tests
|
||||
@echo ""
|
||||
@echo "[Test with c_common]"
|
||||
python setup.py build_ext --inplace
|
||||
python -m pytest tests
|
||||
|
||||
clean:
|
||||
find pyModeS -type f -name '*.c' -delete
|
||||
find pyModeS -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/*
|
||||
298
README.rst
298
README.rst
@@ -1,107 +1,152 @@
|
||||
The Python ADS-B/Mode-S Decoder
|
||||
==========================================
|
||||
===============================
|
||||
|
||||
Python library for ADS-B/Mode-S message decoding. Supported Downlink Formats (DF) are:
|
||||
PyModeS is a Python library designed to decode Mode-S (including ADS-B) messages. It can be imported to your python project or used as a standalone tool to view and save live traffic data.
|
||||
|
||||
**DF17 / DF18: Automatic Dependent Surveillance - Broadcast (ADS-B)**
|
||||
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.
|
||||
|
||||
- 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]
|
||||
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
|
||||
|
||||
|
||||
**DF20 / DF21: Mode-S Comm-B replies**
|
||||
|
||||
- BDS 1,0: Data link capability report
|
||||
- BDS 1,7: Common usage GICB capability report
|
||||
- BDS 2,0: Aircraft identification
|
||||
- BDS 2,1: Aircraft and airline registration markings
|
||||
- BDS 3,0: ACAS active resolution advisory
|
||||
- BDS 4,0: Selected vertical intention
|
||||
- BDS 4,4: Meteorological routine air report
|
||||
- BDS 5,0: Track and turn report
|
||||
- BDS 5,3: Air-referenced state vector
|
||||
- BDS 6,0: Heading and speed report
|
||||
If you find this project useful for your research, please considering cite this tool as::
|
||||
|
||||
@article{sun2019pymodes,
|
||||
author={J. {Sun} and H. {V\^u} and J. {Ellerbroek} and J. M. {Hoekstra}},
|
||||
journal={IEEE Transactions on Intelligent Transportation Systems},
|
||||
title={pyModeS: Decoding Mode-S Surveillance Data for Open Air Transportation Research},
|
||||
year={2019},
|
||||
doi={10.1109/TITS.2019.2914770},
|
||||
ISSN={1524-9050},
|
||||
}
|
||||
|
||||
|
||||
**DF4 / DF20: Altitude code**
|
||||
|
||||
**DF5 / DF21: Identity code (squawk code)**
|
||||
|
||||
Detailed manual on Mode-S decoding is published by the author, at:
|
||||
https://mode-s.org/decode
|
||||
|
||||
|
||||
New features in v2.0
|
||||
---------------------
|
||||
- New structure of the libraries
|
||||
- ADS-B and Comm-B data streaming
|
||||
- Active aircraft viewing (terminal curses)
|
||||
- Improved BDS identification
|
||||
- Optimizing decoding speed
|
||||
|
||||
|
||||
Source code
|
||||
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
|
||||
|
||||
API documentation at:
|
||||
http://pymodes.readthedocs.io
|
||||
[To be updated]
|
||||
Detailed manual on Mode-S decoding is published at:
|
||||
https://mode-s.org/decode
|
||||
|
||||
The API documentation of pyModeS is at:
|
||||
https://mode-s.org/api
|
||||
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
To install latest version from the GitHub:
|
||||
Basic installation
|
||||
-------------------
|
||||
|
||||
::
|
||||
Installation examples::
|
||||
|
||||
# stable version
|
||||
pip install pyModeS
|
||||
|
||||
# conda (compiled) version
|
||||
conda install -c conda-forge pymodes
|
||||
|
||||
# development version
|
||||
pip install git+https://github.com/junzis/pyModeS
|
||||
|
||||
|
||||
To install the stable version (2.0) from pip:
|
||||
Dependencies ``numpy``, and ``pyzmq`` are installed automatically during previous installations processes.
|
||||
|
||||
::
|
||||
If you need to connect pyModeS to a RTL-SDR receiver, ``pyrtlsdr`` need to be installed manually::
|
||||
|
||||
pip install pyModeS
|
||||
pip install pyrtlsdr
|
||||
|
||||
|
||||
Advanced installation (using c modules)
|
||||
------------------------------------------
|
||||
|
||||
Live view traffic (modeslive)
|
||||
If you want to make use of the (faster) c module, install ``pyModeS`` as follows::
|
||||
|
||||
# conda (compiled) version
|
||||
conda install -c conda-forge pymodes
|
||||
|
||||
# stable version (to be compiled on your side)
|
||||
pip install pyModeS[fast]
|
||||
|
||||
# development version
|
||||
git clone https://github.com/junzis/pyModeS
|
||||
cd pyModeS
|
||||
pip install .[fast]
|
||||
|
||||
|
||||
View live 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:
|
||||
-h, --help show this help message and exit
|
||||
--server SERVER server address or IP
|
||||
--port PORT raw data port
|
||||
--rawtype RAWTYPE beast, avr or skysense
|
||||
--latlon LAT LON receiver position
|
||||
--show-uncertainty display uncertaint values, default off
|
||||
--dumpto folder to dump decoded output
|
||||
arguments:
|
||||
-h, --help show this help message and exit
|
||||
--source SOURCE Choose data source, "rtlsdr" or "net"
|
||||
--connect SERVER PORT DATATYPE
|
||||
Define server, port and data type. Supported data
|
||||
types are: ['raw', 'beast', 'skysense']
|
||||
--latlon LAT LON Receiver latitude and longitude, needed for the surface
|
||||
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 connected to your computer, you can use the ``rtlsdr`` source switch (require ``pyrtlsdr`` package), with command::
|
||||
|
||||
$ 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:
|
||||
|
||||
.. image:: https://github.com/junzis/pyModeS/raw/master/doc/modeslive-screenshot.png
|
||||
:width: 700px
|
||||
|
||||
|
||||
Use the library
|
||||
---------------
|
||||
|
||||
@@ -122,7 +167,7 @@ Common functions
|
||||
pms.hex2bin(str) # Convert hexadecimal string to binary string
|
||||
pms.bin2int(str) # Convert binary string to integer
|
||||
pms.hex2int(str) # Convert hexadecimal string to integer
|
||||
pms.gray2int(str) # Convert grey code to interger
|
||||
pms.gray2int(str) # Convert grey code to integer
|
||||
|
||||
|
||||
Core functions for ADS-B decoding
|
||||
@@ -136,10 +181,11 @@ Core functions for ADS-B decoding
|
||||
# Typecode 1-4
|
||||
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.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_velocity(msg)
|
||||
|
||||
pms.adsb.position_with_ref(msg, lat_ref, lon_ref)
|
||||
pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref)
|
||||
@@ -150,15 +196,10 @@ Core functions for ADS-B decoding
|
||||
# Typecode: 19
|
||||
pms.adsb.velocity(msg) # Handles both surface & airborne messages
|
||||
pms.adsb.speed_heading(msg) # Handles both surface & airborne messages
|
||||
pms.adsb.surface_velocity(msg)
|
||||
pms.adsb.airborne_velocity(msg)
|
||||
|
||||
|
||||
Note: When you have a fix position of the aircraft, it is convenient to
|
||||
use `position_with_ref()` method to decode with only one position message
|
||||
(either odd or even). This works with both airborne and surface position
|
||||
messages. But the reference position shall be with in 180NM (airborne)
|
||||
or 45NM (surface) of the true position.
|
||||
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.
|
||||
|
||||
|
||||
Decode altitude replies in DF4 / DF20
|
||||
@@ -214,19 +255,19 @@ Mode-S Enhanced Surveillance (EHS)
|
||||
|
||||
.. code:: python
|
||||
|
||||
# For BDS register 4,0
|
||||
pms.commb.alt40mcp(msg) # MCP/FCU selected altitude (ft)
|
||||
pms.commb.alt40fms(msg) # FMS selected altitude (ft)
|
||||
# BDS 4,0
|
||||
pms.commb.selalt40mcp(msg) # MCP/FCU selected altitude (ft)
|
||||
pms.commb.selalt40fms(msg) # FMS selected altitude (ft)
|
||||
pms.commb.p40baro(msg) # Barometric pressure (mb)
|
||||
|
||||
# For BDS register 5,0
|
||||
# BDS 5,0
|
||||
pms.commb.roll50(msg) # Roll angle (deg)
|
||||
pms.commb.trk50(msg) # True track angle (deg)
|
||||
pms.commb.gs50(msg) # Ground speed (kt)
|
||||
pms.commb.rtrk50(msg) # Track angle rate (deg/sec)
|
||||
pms.commb.tas50(msg) # True airspeed (kt)
|
||||
|
||||
# For BDS register 6,0
|
||||
# BDS 6,0
|
||||
pms.commb.hdg60(msg) # Magnetic heading (deg)
|
||||
pms.commb.ias60(msg) # Indicated airspeed (kt)
|
||||
pms.commb.mach60(msg) # Mach number (-)
|
||||
@@ -234,22 +275,103 @@ Mode-S Enhanced Surveillance (EHS)
|
||||
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
|
||||
|
||||
|
||||
Meteorological routine air report (MRAR) [Experimental]
|
||||
*******************************************************
|
||||
Meteorological reports [Experimental]
|
||||
**************************************
|
||||
|
||||
To identify BDS 4,4 and 4,5 codes, you must set ``mrar`` argument to ``True`` in the ``infer()`` function:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# For BDS register 4,4
|
||||
pms.commb.wind44(msg, rev=False) # Wind speed (kt) and direction (true) (deg)
|
||||
pms.commb.temp44(msg, rev=False) # Static air temperature (C)
|
||||
pms.commb.p44(msg, rev=False) # Average static pressure (hPa)
|
||||
pms.commb.hum44(msg, rev=False) # Humidity (%)
|
||||
pms.bds.infer(msg. mrar=True)
|
||||
|
||||
Once the correct MRAR and MHR messages are identified, decode them as follows:
|
||||
|
||||
|
||||
Developement
|
||||
------------
|
||||
To perform unit tests. First install ``tox`` through pip, Then, run the following commands:
|
||||
Meteorological routine air report (MRAR)
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. code:: bash
|
||||
.. code:: python
|
||||
|
||||
$ tox
|
||||
# BDS 4,4
|
||||
pms.commb.wind44(msg) # Wind speed (kt) and direction (true) (deg)
|
||||
pms.commb.temp44(msg) # Static air temperature (C)
|
||||
pms.commb.p44(msg) # Average static pressure (hPa)
|
||||
pms.commb.hum44(msg) # Humidity (%)
|
||||
|
||||
|
||||
Meteorological hazard air report (MHR)
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. code:: python
|
||||
|
||||
# BDS 4,5
|
||||
pms.commb.turb45(msg) # Turbulence level (0-3)
|
||||
pms.commb.ws45(msg) # Wind shear level (0-3)
|
||||
pms.commb.mb45(msg) # Microburst level (0-3)
|
||||
pms.commb.ic45(msg) # Icing level (0-3)
|
||||
pms.commb.wv45(msg) # Wake vortex level (0-3)
|
||||
pms.commb.temp45(msg) # Static air temperature (C)
|
||||
pms.commb.p45(msg) # Average static pressure (hPa)
|
||||
pms.commb.rh45(msg) # Radio height (ft)
|
||||
|
||||
|
||||
|
||||
Customize the streaming module
|
||||
******************************
|
||||
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.
|
||||
|
||||
Here is an example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pyModeS as pms
|
||||
from pyModeS.extra.tcpclient import TcpClient
|
||||
|
||||
# define your custom class by extending the TcpClient
|
||||
# - implement your handle_messages() methods
|
||||
class ADSBClient(TcpClient):
|
||||
def __init__(self, host, port, rawtype):
|
||||
super(ADSBClient, self).__init__(host, port, rawtype)
|
||||
|
||||
def handle_messages(self, messages):
|
||||
for msg, ts in messages:
|
||||
if len(msg) != 28: # wrong data length
|
||||
continue
|
||||
|
||||
df = pms.df(msg)
|
||||
|
||||
if df != 17: # not ADSB
|
||||
continue
|
||||
|
||||
if pms.crc(msg) !=0: # CRC fail
|
||||
continue
|
||||
|
||||
icao = pms.adsb.icao(msg)
|
||||
tc = pms.adsb.typecode(msg)
|
||||
|
||||
# TODO: write you magic code here
|
||||
print(ts, icao, tc, msg)
|
||||
|
||||
# run new client, change the host, port, and rawtype if needed
|
||||
client = ADSBClient(host='127.0.0.1', port=30005, rawtype='beast')
|
||||
client.run()
|
||||
|
||||
|
||||
Unit test
|
||||
---------
|
||||
To perform unit tests, ``pytest`` must be install first.
|
||||
|
||||
Build Cython extensions
|
||||
::
|
||||
|
||||
$ make ext
|
||||
|
||||
Run unit tests
|
||||
::
|
||||
|
||||
$ make test
|
||||
|
||||
Clean build files
|
||||
::
|
||||
|
||||
$ make clean
|
||||
|
||||
3
doc/.gitignore
vendored
3
doc/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
_build
|
||||
_static
|
||||
_templates
|
||||
228
doc/Makefile
228
doc/Makefile
@@ -1,225 +1,21 @@
|
||||
# Makefile for Sphinx documentation
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " epub3 to make an epub3"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
@echo " dummy to check syntax errors of document sources"
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
.PHONY: help Makefile
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyModeS.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyModeS.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyModeS"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyModeS"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: epub3
|
||||
epub3:
|
||||
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||
@echo
|
||||
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
||||
.PHONY: dummy
|
||||
dummy:
|
||||
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||
@echo
|
||||
@echo "Build finished. Dummy builder generates no files."
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
rm -f source/pyModeS*.rst source/modules.rst
|
||||
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)
|
||||
|
||||
7
doc/README.rst
Normal file
7
doc/README.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
How to generate the apidoc
|
||||
====================================
|
||||
|
||||
::
|
||||
|
||||
cd doc
|
||||
make html
|
||||
337
doc/conf.py
337
doc/conf.py
@@ -1,337 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# pyModeS documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Aug 16 15:47:05 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'pyModeS'
|
||||
copyright = u'2016, Junzi Sun'
|
||||
author = u'Junzi Sun'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'1.0.5'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'1.0.5'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#
|
||||
# today = ''
|
||||
#
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
# html_theme = 'alabaster'
|
||||
|
||||
# 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
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents.
|
||||
# "<project> v<release> documentation" by default.
|
||||
#
|
||||
# html_title = u'pyModeS v1.0.5'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#
|
||||
# html_extra_path = []
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# bottom, using the given strftime format.
|
||||
# The empty string is equivalent to '%b %d, %Y'.
|
||||
#
|
||||
# html_last_updated_fmt = None
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
|
||||
#
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# 'ja' uses this config value.
|
||||
# 'zh' user can custom change `jieba` dictionary path.
|
||||
#
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'pyModeSdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'pyModeS.tex', u'pyModeS Documentation',
|
||||
u'Junzi Sun', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'pymodes', u'pyModeS Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'pyModeS', u'pyModeS Documentation',
|
||||
author, 'pyModeS', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#
|
||||
# texinfo_no_detailmenu = False
|
||||
@@ -1,40 +0,0 @@
|
||||
.. pyModeS documentation master file, created by
|
||||
sphinx-quickstart on Tue Aug 16 15:47:05 2016.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
|
||||
pyModeS APIs
|
||||
=====================
|
||||
|
||||
This document contains all the functions within pyModeS package.
|
||||
|
||||
Source code and user guide: https://github.com/junzis/pyModeS
|
||||
|
||||
|
||||
pyModeS.adsb module
|
||||
-------------------
|
||||
|
||||
.. automodule:: pyModeS.adsb
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
pyModeS.ehs module
|
||||
------------------
|
||||
|
||||
.. automodule:: pyModeS.ehs
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
pyModeS.util module
|
||||
-------------------
|
||||
|
||||
.. automodule:: pyModeS.util
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pyModeS==1.1.0
|
||||
17
doc/source/_templates/layout.html
Normal file
17
doc/source/_templates/layout.html
Normal 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 %}
|
||||
190
doc/source/conf.py
Normal file
190
doc/source/conf.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "pyModeS"
|
||||
copyright = "2019, Junzi Sun"
|
||||
author = "Junzi Sun"
|
||||
|
||||
# The short X.Y version
|
||||
version = ""
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = ""
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.mathjax",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.githubpages",
|
||||
"sphinx.ext.napoleon",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = []
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = None
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
# 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
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "pyModeSdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, "pyModeS.tex", "pyModeS Documentation", "Junzi Sun", "manual")
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [(master_doc, "pymodes", "pyModeS Documentation", [author], 1)]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"pyModeS",
|
||||
"pyModeS Documentation",
|
||||
author,
|
||||
"pyModeS",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
68
doc/source/index.rst
Normal file
68
doc/source/index.rst
Normal file
@@ -0,0 +1,68 @@
|
||||
.. pyModeS documentation master file, created by
|
||||
sphinx-quickstart on Mon Apr 1 13:13:10 2019.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to pyModeS documentation!
|
||||
===================================
|
||||
|
||||
The source code can be found at: https://github.com/junzis/pyModeS
|
||||
|
||||
.. toctree::
|
||||
: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
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
.. include:: ../../README.rst
|
||||
|
||||
----
|
||||
|
||||
Indices and tables
|
||||
**********************
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
7
doc/source/modules.rst
Normal file
7
doc/source/modules.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS
|
||||
=======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS
|
||||
7
doc/source/pyModeS.c_common.rst
Normal file
7
doc/source/pyModeS.c_common.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.c\_common module
|
||||
========================
|
||||
|
||||
.. automodule:: pyModeS.c_common
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.common.rst
Normal file
7
doc/source/pyModeS.common.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.common module
|
||||
=====================
|
||||
|
||||
.. automodule:: pyModeS.common
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.acas.rst
Normal file
7
doc/source/pyModeS.decoder.acas.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.acas module
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.acas
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.adsb.rst
Normal file
7
doc/source/pyModeS.decoder.adsb.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.adsb module
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.adsb
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.allcall.rst
Normal file
7
doc/source/pyModeS.decoder.allcall.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.allcall module
|
||||
==============================
|
||||
|
||||
.. automodule:: pyModeS.decoder.allcall
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds05.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds05.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds05 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds05
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds06.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds06.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds06 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds06
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds08.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds08.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds08 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds08
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds09.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds09.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds09 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds09
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds10.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds10.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds10 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds10
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds17.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds17.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds17 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds17
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds20.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds20.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds20 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds20
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds30.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds30.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds30 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds30
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds40.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds40.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds40 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds40
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds44.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds44.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds44 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds44
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds45.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds45.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds45 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds45
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds50.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds50.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds50 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds50
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds53.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds53.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds53 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds53
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds60.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds60.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds60 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds60
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
28
doc/source/pyModeS.decoder.bds.rst
Normal file
28
doc/source/pyModeS.decoder.bds.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pyModeS.decoder.bds package
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS.decoder.bds.bds05
|
||||
pyModeS.decoder.bds.bds06
|
||||
pyModeS.decoder.bds.bds08
|
||||
pyModeS.decoder.bds.bds09
|
||||
pyModeS.decoder.bds.bds10
|
||||
pyModeS.decoder.bds.bds17
|
||||
pyModeS.decoder.bds.bds20
|
||||
pyModeS.decoder.bds.bds30
|
||||
pyModeS.decoder.bds.bds40
|
||||
pyModeS.decoder.bds.bds44
|
||||
pyModeS.decoder.bds.bds45
|
||||
pyModeS.decoder.bds.bds50
|
||||
pyModeS.decoder.bds.bds53
|
||||
pyModeS.decoder.bds.bds60
|
||||
7
doc/source/pyModeS.decoder.commb.rst
Normal file
7
doc/source/pyModeS.decoder.commb.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.commb module
|
||||
============================
|
||||
|
||||
.. automodule:: pyModeS.decoder.commb
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
29
doc/source/pyModeS.decoder.rst
Normal file
29
doc/source/pyModeS.decoder.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
pyModeS.decoder package
|
||||
=======================
|
||||
|
||||
.. automodule:: pyModeS.decoder
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS.decoder.bds
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS.decoder.acas
|
||||
pyModeS.decoder.adsb
|
||||
pyModeS.decoder.allcall
|
||||
pyModeS.decoder.commb
|
||||
pyModeS.decoder.surv
|
||||
pyModeS.decoder.uncertainty
|
||||
pyModeS.decoder.uplink
|
||||
7
doc/source/pyModeS.decoder.surv.rst
Normal file
7
doc/source/pyModeS.decoder.surv.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.surv module
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.surv
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.uncertainty.rst
Normal file
7
doc/source/pyModeS.decoder.uncertainty.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.uncertainty module
|
||||
==================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.uncertainty
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.uplink.rst
Normal file
7
doc/source/pyModeS.decoder.uplink.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.uplink module
|
||||
=============================
|
||||
|
||||
.. automodule:: pyModeS.decoder.uplink
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
24
doc/source/pyModeS.rst
Normal file
24
doc/source/pyModeS.rst
Normal file
@@ -0,0 +1,24 @@
|
||||
pyModeS package
|
||||
===============
|
||||
|
||||
.. automodule:: pyModeS
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS.decoder
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS.c_common
|
||||
pyModeS.common
|
||||
0
doc/warnings
Normal file
0
doc/warnings
Normal file
@@ -1,15 +1,36 @@
|
||||
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 Exception:
|
||||
from . import py_common as common # type: ignore
|
||||
from .py_common import * # type: ignore
|
||||
|
||||
from .decoder import tell
|
||||
from .decoder import adsb
|
||||
from .decoder import acas
|
||||
from .decoder import commb
|
||||
from .decoder import common
|
||||
from .decoder import allcall
|
||||
from .decoder import surv
|
||||
from .decoder import bds
|
||||
from .extra import aero
|
||||
from .extra import tcpclient
|
||||
|
||||
# from .decoder import els # depricated
|
||||
# from .decoder import ehs # depricated
|
||||
__all__ = [
|
||||
"common",
|
||||
"tell",
|
||||
"adsb",
|
||||
"commb",
|
||||
"allcall",
|
||||
"surv",
|
||||
"bds",
|
||||
"aero",
|
||||
"tcpclient",
|
||||
]
|
||||
|
||||
|
||||
warnings.simplefilter("once", DeprecationWarning)
|
||||
|
||||
import os
|
||||
dirpath = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
28
pyModeS/c_common.pxd
Normal file
28
pyModeS/c_common.pxd
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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 hexstr)
|
||||
cpdef str bin2hex(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 str squawk(str binstr)
|
||||
|
||||
cpdef int altcode(str msg)
|
||||
cpdef int altitude(str binstr)
|
||||
|
||||
cpdef str data(str msg)
|
||||
cpdef bint allzeros(str msg)
|
||||
18
pyModeS/c_common.pyi
Normal file
18
pyModeS/c_common.pyi
Normal file
@@ -0,0 +1,18 @@
|
||||
def hex2bin(hexstr: str) -> str: ...
|
||||
def bin2int(binstr: str) -> int: ...
|
||||
def hex2int(hexstr: str) -> int: ...
|
||||
def bin2hex(binstr: str) -> str: ...
|
||||
def df(msg: str) -> int: ...
|
||||
def crc(msg: str, encode: bool = False) -> int: ...
|
||||
def floor(x: float) -> float: ...
|
||||
def icao(msg: str) -> str: ...
|
||||
def is_icao_assigned(icao: str) -> bool: ...
|
||||
def typecode(msg: str) -> int: ...
|
||||
def cprNL(lat: float) -> int: ...
|
||||
def idcode(msg: str) -> str: ...
|
||||
def squawk(binstr: str) -> str: ...
|
||||
def altcode(msg: str) -> int: ...
|
||||
def altitude(binstr: str) -> int: ...
|
||||
def data(msg: str) -> str: ...
|
||||
def allzeros(msg: str) -> bool: ...
|
||||
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ...
|
||||
405
pyModeS/c_common.pyx
Normal file
405
pyModeS/c_common.pyx
Normal file
@@ -0,0 +1,405 @@
|
||||
# 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 hexadecimal 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 str bin2hex(str binstr):
|
||||
return "{0:X}".format(int(binstr, 2))
|
||||
|
||||
|
||||
@cython.boundscheck(False)
|
||||
cpdef unsigned char df(str msg):
|
||||
"""Decode Downlink Format value, 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."""
|
||||
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"""
|
||||
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 * 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)."""
|
||||
if df(msg) not in [5, 21]:
|
||||
raise RuntimeError("Message must be Downlink Format 5 or 21.")
|
||||
|
||||
squawk_code = squawk(hex2bin(msg)[19:32])
|
||||
return squawk_code
|
||||
|
||||
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
cpdef str squawk(str binstr):
|
||||
"""Compute identity (squawk code)."""
|
||||
|
||||
if len(binstr) != 13 or set(binstr) != set('01'):
|
||||
raise RuntimeError("Input must be 13 bits binary string")
|
||||
|
||||
cdef bytearray _mbin = bytearray(binstr.encode())
|
||||
cdef unsigned char[:] mbin = _mbin
|
||||
|
||||
cdef bytearray _idcode = bytearray(4)
|
||||
cdef unsigned char[:] idcode = _idcode
|
||||
|
||||
cdef unsigned char C1 = mbin[0]
|
||||
cdef unsigned char A1 = mbin[1]
|
||||
cdef unsigned char C2 = mbin[2]
|
||||
cdef unsigned char A2 = mbin[3]
|
||||
cdef unsigned char C4 = mbin[4]
|
||||
cdef unsigned char A4 = mbin[5]
|
||||
# X = mbin[6]
|
||||
cdef unsigned char B1 = mbin[7]
|
||||
cdef unsigned char D1 = mbin[8]
|
||||
cdef unsigned char B2 = mbin[9]
|
||||
cdef unsigned char D2 = mbin[10]
|
||||
cdef unsigned char B4 = mbin[11]
|
||||
cdef unsigned char D4 = mbin[12]
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
cpdef int altcode(str msg):
|
||||
"""Compute the altitude."""
|
||||
if df(msg) not in [0, 4, 16, 20]:
|
||||
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
|
||||
|
||||
alt = altitude(hex2bin(msg)[19:32])
|
||||
return alt
|
||||
|
||||
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
cpdef int altitude(str binstr):
|
||||
|
||||
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
|
||||
raise RuntimeError("Input must be 13 bits binary string")
|
||||
|
||||
cdef bytearray _mbin = bytearray(binstr.encode())
|
||||
cdef unsigned char[:] mbin = _mbin
|
||||
|
||||
cdef char Mbit = binstr[6]
|
||||
cdef char Qbit = binstr[8]
|
||||
|
||||
cdef int alt = 0
|
||||
cdef bytearray vbin
|
||||
cdef bytearray _graybytes = bytearray(11)
|
||||
cdef unsigned char[:] graybytes = _graybytes
|
||||
|
||||
if bin2int(binstr) == 0:
|
||||
# altitude unknown or invalid
|
||||
alt = -9999
|
||||
|
||||
elif Mbit == 48: # unit in ft, "0" -> 48
|
||||
if Qbit == 49: # 25ft interval, "1" -> 49
|
||||
vbin = _mbin[:6] + _mbin[7:8] + _mbin[9:]
|
||||
alt = bin2int(vbin.decode()) * 25 - 1000
|
||||
if Qbit == 48: # 100ft interval, above 50175ft, "0" -> 48
|
||||
graybytes[8] = mbin[0]
|
||||
graybytes[2] = mbin[1]
|
||||
graybytes[9] = mbin[2]
|
||||
graybytes[3] = mbin[3]
|
||||
graybytes[10] = mbin[4]
|
||||
graybytes[4] = mbin[5]
|
||||
# M = mbin[6]
|
||||
graybytes[5] = mbin[7]
|
||||
# Q = mbin[8]
|
||||
graybytes[6] = mbin[9]
|
||||
graybytes[0] = mbin[10]
|
||||
graybytes[7] = mbin[11]
|
||||
graybytes[1] = mbin[12]
|
||||
|
||||
alt = gray2alt(_graybytes.decode())
|
||||
|
||||
elif Mbit == 49: # unit in meter, "1" -> 49
|
||||
vbin = _mbin[:6] + _mbin[7:]
|
||||
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."""
|
||||
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
|
||||
22
pyModeS/common.pyi
Normal file
22
pyModeS/common.pyi
Normal file
@@ -0,0 +1,22 @@
|
||||
from typing import Optional
|
||||
|
||||
def hex2bin(hexstr: str) -> str: ...
|
||||
def bin2int(binstr: str) -> int: ...
|
||||
def hex2int(hexstr: str) -> int: ...
|
||||
def bin2hex(binstr: str) -> str: ...
|
||||
def df(msg: str) -> int: ...
|
||||
def crc(msg: str, encode: bool = False) -> int: ...
|
||||
def floor(x: float) -> float: ...
|
||||
def icao(msg: str) -> Optional[str]: ...
|
||||
def is_icao_assigned(icao: str) -> bool: ...
|
||||
def typecode(msg: str) -> Optional[int]: ...
|
||||
def cprNL(lat: float) -> int: ...
|
||||
def idcode(msg: str) -> str: ...
|
||||
def squawk(binstr: str) -> str: ...
|
||||
def altcode(msg: str) -> Optional[int]: ...
|
||||
def altitude(binstr: str) -> Optional[int]: ...
|
||||
def gray2alt(binstr: str) -> Optional[int]: ...
|
||||
def gray2int(binstr: str) -> int: ...
|
||||
def data(msg: str) -> str: ...
|
||||
def allzeros(msg: str) -> bool: ...
|
||||
def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ...
|
||||
@@ -1,3 +1,237 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
def tell(msg: str) -> None:
|
||||
from .. import common, adsb, commb, bds
|
||||
|
||||
from pyModeS.decoder import *
|
||||
def _print(label, value, unit=None):
|
||||
print("%28s: " % 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 tc is None:
|
||||
_print("ERROR", "Unknown typecode")
|
||||
return
|
||||
|
||||
if 1 <= tc <= 4: # callsign
|
||||
callsign = adsb.callsign(msg)
|
||||
_print("Type", "Identification 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")
|
||||
velocity = adsb.velocity(msg)
|
||||
if velocity is not None:
|
||||
spd, trk, vr, t = velocity
|
||||
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 tc == 29: # target state and status
|
||||
_print("Type", "Target State and Status")
|
||||
subtype = common.bin2int((common.hex2bin(msg)[32:])[5:7])
|
||||
_print("Subtype", subtype)
|
||||
tcas_operational = adsb.tcas_operational(msg)
|
||||
types_29 = {0: "Not Engaged", 1: "Engaged"}
|
||||
tcas_operational_types = {0: "Not Operational", 1: "Operational"}
|
||||
if subtype == 0:
|
||||
emergency_types = {
|
||||
0: "No emergency",
|
||||
1: "General emergency",
|
||||
2: "Lifeguard/medical emergency",
|
||||
3: "Minimum fuel",
|
||||
4: "No communications",
|
||||
5: "Unlawful interference",
|
||||
6: "Downed aircraft",
|
||||
7: "Reserved",
|
||||
}
|
||||
vertical_horizontal_types = {
|
||||
1: "Acquiring mode",
|
||||
2: "Capturing/Maintaining mode",
|
||||
}
|
||||
tcas_ra_types = {0: "Not active", 1: "Active"}
|
||||
alt, alt_source, alt_ref = adsb.target_altitude(msg)
|
||||
angle, angle_type, angle_source = adsb.target_angle(msg)
|
||||
vertical_mode = adsb.vertical_mode(msg)
|
||||
horizontal_mode = adsb.horizontal_mode(msg)
|
||||
tcas_ra = adsb.tcas_ra(msg)
|
||||
emergency_status = adsb.emergency_status(msg)
|
||||
_print("Target altitude", alt, "feet")
|
||||
_print("Altitude source", alt_source)
|
||||
_print("Altitude reference", alt_ref)
|
||||
_print("Angle", angle, "°")
|
||||
_print("Angle Type", angle_type)
|
||||
_print("Angle Source", angle_source)
|
||||
if vertical_mode is not None:
|
||||
_print(
|
||||
"Vertical mode",
|
||||
vertical_horizontal_types[vertical_mode],
|
||||
)
|
||||
if horizontal_mode is not None:
|
||||
_print(
|
||||
"Horizontal mode",
|
||||
vertical_horizontal_types[horizontal_mode],
|
||||
)
|
||||
_print(
|
||||
"TCAS/ACAS",
|
||||
tcas_operational_types[tcas_operational]
|
||||
if tcas_operational
|
||||
else None,
|
||||
)
|
||||
_print("TCAS/ACAS RA", tcas_ra_types[tcas_ra])
|
||||
_print("Emergency status", emergency_types[emergency_status])
|
||||
else:
|
||||
alt, alt_source = adsb.selected_altitude(msg) # type: ignore
|
||||
baro = adsb.baro_pressure_setting(msg)
|
||||
hdg = adsb.selected_heading(msg)
|
||||
autopilot = adsb.autopilot(msg)
|
||||
vnav = adsb.vnav_mode(msg)
|
||||
alt_hold = adsb.altitude_hold_mode(msg)
|
||||
app = adsb.approach_mode(msg)
|
||||
lnav = adsb.lnav_mode(msg)
|
||||
_print("Selected altitude", alt, "feet")
|
||||
_print("Altitude source", alt_source)
|
||||
_print(
|
||||
"Barometric pressure setting",
|
||||
baro,
|
||||
"" if baro is None else "millibars",
|
||||
)
|
||||
_print("Selected Heading", hdg, "°")
|
||||
if not (common.bin2int((common.hex2bin(msg)[32:])[46]) == 0):
|
||||
_print(
|
||||
"Autopilot", types_29[autopilot] if autopilot else None
|
||||
)
|
||||
_print("VNAV mode", types_29[vnav] if vnav else None)
|
||||
_print(
|
||||
"Altitude hold mode",
|
||||
types_29[alt_hold] if alt_hold else None,
|
||||
)
|
||||
_print("Approach mode", types_29[app] if app else None)
|
||||
_print(
|
||||
"TCAS/ACAS",
|
||||
tcas_operational_types[tcas_operational]
|
||||
if tcas_operational
|
||||
else None,
|
||||
)
|
||||
_print("LNAV mode", types_29[lnav] if lnav else None)
|
||||
|
||||
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 is not None and 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")
|
||||
|
||||
@@ -1,21 +1,158 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Decoding Air-Air Surveillance (ACAS) DF=0/16
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import common
|
||||
import warnings
|
||||
|
||||
warnings.simplefilter("always", UserWarning)
|
||||
|
||||
|
||||
def isACAS(msg: str) -> bool:
|
||||
"""Check if the message is an ACAS coordination message.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: if VDS is 3,1
|
||||
"""
|
||||
mv = common.hex2bin(common.data(msg))
|
||||
|
||||
vds = mv[0:8]
|
||||
if vds == "00110000":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def rac(msg: str) -> None | str:
|
||||
"""Resolution Advisory Complement.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: RACs
|
||||
"""
|
||||
if not isACAS(msg):
|
||||
warnings.warn("Not an ACAS coordination message.")
|
||||
return None
|
||||
|
||||
RAC = []
|
||||
|
||||
mv = common.hex2bin(common.data(msg))
|
||||
|
||||
if mv[22] == "1":
|
||||
RAC.append("do not pass below")
|
||||
|
||||
if mv[23] == "1":
|
||||
RAC.append("do not pass above")
|
||||
|
||||
if mv[24] == "1":
|
||||
RAC.append("do not pass left")
|
||||
|
||||
if mv[25] == "1":
|
||||
RAC.append("do not pass right")
|
||||
|
||||
return "; ".join(RAC)
|
||||
|
||||
|
||||
def rat(msg: str) -> None | int:
|
||||
"""RA terminated indicator
|
||||
|
||||
Mode S transponder is still required to report RA 18 seconds after
|
||||
it is terminated by ACAS. Hence, the RAT filed is used.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: if RA has been terminated
|
||||
"""
|
||||
if not isACAS(msg):
|
||||
warnings.warn("Not an ACAS coordination message.")
|
||||
return None
|
||||
|
||||
mv = common.hex2bin(common.data(msg))
|
||||
mte = int(mv[26])
|
||||
return mte
|
||||
|
||||
|
||||
def mte(msg: str) -> None | int:
|
||||
"""Multiple threat encounter.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: if there are multiple threats
|
||||
"""
|
||||
if not isACAS(msg):
|
||||
warnings.warn("Not an ACAS coordination message.")
|
||||
return None
|
||||
|
||||
mv = common.hex2bin(common.data(msg))
|
||||
mte = int(mv[27])
|
||||
return mte
|
||||
|
||||
|
||||
def ara(msg: str) -> None | str:
|
||||
"""Decode active resolution advisory.
|
||||
|
||||
:param msg: 28 bytes hexadecimal message string
|
||||
:return: RA charactristics
|
||||
"""
|
||||
if not isACAS(msg):
|
||||
warnings.warn("Not an ACAS coordination message.")
|
||||
return None
|
||||
mv = common.hex2bin(common.data(msg))
|
||||
|
||||
mte = int(mv[27])
|
||||
|
||||
ara_b1 = int(mv[8])
|
||||
ara_b2 = int(mv[9])
|
||||
ara_b3 = int(mv[10])
|
||||
ara_b4 = int(mv[11])
|
||||
ara_b5 = int(mv[12])
|
||||
ara_b6 = int(mv[14])
|
||||
ara_b7 = int(mv[15])
|
||||
# ACAS III are bits 15-22
|
||||
|
||||
RA = []
|
||||
|
||||
if ara_b1 == 1:
|
||||
if ara_b2:
|
||||
RA.append("corrective")
|
||||
else:
|
||||
RA.append("preventive")
|
||||
|
||||
if ara_b3:
|
||||
RA.append("downward sense")
|
||||
else:
|
||||
RA.append("upward sense")
|
||||
|
||||
if ara_b4:
|
||||
RA.append("increased rate")
|
||||
|
||||
if ara_b5:
|
||||
RA.append("sense reversal")
|
||||
|
||||
if ara_b6:
|
||||
RA.append("altitude crossing")
|
||||
|
||||
if ara_b7:
|
||||
RA.append("positive")
|
||||
else:
|
||||
RA.append("vertical speed limit")
|
||||
|
||||
if ara_b1 == 0 and mte == 1:
|
||||
if ara_b2:
|
||||
RA.append("requires a correction in the upward sense")
|
||||
|
||||
if ara_b3:
|
||||
RA.append("requires a positive climb")
|
||||
|
||||
if ara_b4:
|
||||
RA.append("requires a correction in downward sense")
|
||||
|
||||
if ara_b5:
|
||||
RA.append("requires a positive descent")
|
||||
|
||||
if ara_b6:
|
||||
RA.append("requires a crossing")
|
||||
|
||||
if ara_b7:
|
||||
RA.append("requires a sense reversal")
|
||||
|
||||
return "; ".join(RA)
|
||||
|
||||
@@ -1,89 +1,177 @@
|
||||
# Copyright (C) 2015 Junzi Sun (TU Delft)
|
||||
"""ADS-B module.
|
||||
|
||||
# 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.
|
||||
The ADS-B module also imports functions from the following modules:
|
||||
|
||||
# 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/>.
|
||||
- bds05: ``airborne_position()``, ``airborne_position_with_ref()``,
|
||||
``altitude()``
|
||||
- bds06: ``surface_position()``, ``surface_position_with_ref()``,
|
||||
``surface_velocity()``
|
||||
- bds08: ``category()``, ``callsign()``
|
||||
- bds09: ``airborne_velocity()``, ``altitude_diff()``
|
||||
|
||||
"""
|
||||
The wrapper for decoding ADS-B messages
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
import pyModeS as pms
|
||||
from pyModeS.decoder import common
|
||||
from pyModeS.decoder import uncertainty
|
||||
from .. import common
|
||||
from . import uncertainty
|
||||
from .bds.bds05 import airborne_position, airborne_position_with_ref
|
||||
from .bds.bds05 import altitude as altitude05
|
||||
from .bds.bds06 import (
|
||||
surface_position,
|
||||
surface_position_with_ref,
|
||||
surface_velocity,
|
||||
)
|
||||
from .bds.bds08 import callsign, category
|
||||
from .bds.bds09 import airborne_velocity, altitude_diff
|
||||
from .bds.bds61_st1 import emergency_squawk, emergency_state, is_emergency
|
||||
from .bds.bds62 import (
|
||||
altitude_hold_mode,
|
||||
approach_mode,
|
||||
autopilot,
|
||||
baro_pressure_setting,
|
||||
emergency_status,
|
||||
horizontal_mode,
|
||||
lnav_mode,
|
||||
selected_altitude,
|
||||
selected_heading,
|
||||
target_altitude,
|
||||
target_angle,
|
||||
tcas_operational,
|
||||
tcas_ra,
|
||||
vertical_mode,
|
||||
vnav_mode,
|
||||
)
|
||||
|
||||
# from pyModeS.decoder.bds import bds05, bds06, bds09
|
||||
from pyModeS.decoder.bds.bds05 import airborne_position, airborne_position_with_ref, altitude
|
||||
from pyModeS.decoder.bds.bds06 import surface_position, surface_position_with_ref, surface_velocity
|
||||
from pyModeS.decoder.bds.bds08 import category, callsign
|
||||
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
|
||||
__all__ = [
|
||||
"airborne_position",
|
||||
"airborne_position_with_ref",
|
||||
"altitude05",
|
||||
"surface_position",
|
||||
"surface_position_with_ref",
|
||||
"surface_velocity",
|
||||
"callsign",
|
||||
"category",
|
||||
"airborne_velocity",
|
||||
"altitude_diff",
|
||||
"emergency_squawk",
|
||||
"emergency_state",
|
||||
"is_emergency",
|
||||
"df",
|
||||
"icao",
|
||||
"typecode",
|
||||
"position",
|
||||
"position_with_ref",
|
||||
"altitude",
|
||||
"velocity",
|
||||
"speed_heading",
|
||||
"oe_flag",
|
||||
"version",
|
||||
"nuc_p",
|
||||
"nuc_v",
|
||||
"nic_v1",
|
||||
"nic_v2",
|
||||
"nic_s",
|
||||
"nic_a_c",
|
||||
"nic_b",
|
||||
"nac_p",
|
||||
"nac_v",
|
||||
"sil",
|
||||
"selected_altitude",
|
||||
"target_altitude",
|
||||
"vertical_mode",
|
||||
"horizontal_mode",
|
||||
"selected_heading",
|
||||
"target_angle",
|
||||
"baro_pressure_setting",
|
||||
"autopilot",
|
||||
"vnav_mode",
|
||||
"altitude_hold_mode",
|
||||
"approach_mode",
|
||||
"lnav_mode",
|
||||
"tcas_operational",
|
||||
"tcas_ra",
|
||||
"emergency_status",
|
||||
]
|
||||
|
||||
def df(msg):
|
||||
|
||||
def df(msg: str) -> int:
|
||||
return common.df(msg)
|
||||
|
||||
def icao(msg):
|
||||
|
||||
def icao(msg: str) -> None | str:
|
||||
return common.icao(msg)
|
||||
|
||||
def typecode(msg):
|
||||
|
||||
def typecode(msg: str) -> None | int:
|
||||
return common.typecode(msg)
|
||||
|
||||
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
||||
"""Decode position from a pair of even and odd position message
|
||||
(works with both airborne and surface position messages)
|
||||
|
||||
def position(
|
||||
msg0: str,
|
||||
msg1: str,
|
||||
t0: int | datetime,
|
||||
t1: int | datetime,
|
||||
lat_ref: None | float = None,
|
||||
lon_ref: None | float = None,
|
||||
) -> None | tuple[float, float]:
|
||||
"""Decode surface or airborne position from a pair of even and odd
|
||||
position messages.
|
||||
|
||||
Note, that to decode surface position using the position message pair,
|
||||
the reference position has to be provided.
|
||||
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
msg0 (string): even message (28 hexdigits)
|
||||
msg1 (string): odd message (28 hexdigits)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
lat_ref (float): latitude of reference position
|
||||
lon_ref (float): longitude of reference position
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
|
||||
"""
|
||||
tc0 = typecode(msg0)
|
||||
tc1 = typecode(msg1)
|
||||
|
||||
if (5<=tc0<=8 and 5<=tc1<=8):
|
||||
if (not lat_ref) or (not lon_ref):
|
||||
raise RuntimeError("Surface position encountered, a reference \
|
||||
position lat/lon required. Location of \
|
||||
receiver can be used.")
|
||||
if tc0 is None or tc1 is None:
|
||||
raise RuntimeError("Incorrect or inconsistent message types")
|
||||
|
||||
if 5 <= tc0 <= 8 and 5 <= tc1 <= 8:
|
||||
if lat_ref is None or lon_ref is None:
|
||||
raise RuntimeError(
|
||||
"Surface position encountered, a reference position"
|
||||
" lat/lon required. Location of receiver can be used."
|
||||
)
|
||||
else:
|
||||
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
|
||||
|
||||
elif (9<=tc0<=18 and 9<=tc1<=18):
|
||||
elif 9 <= tc0 <= 18 and 9 <= tc1 <= 18:
|
||||
# Airborne position with barometric height
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
elif (20<=tc0<=22 and 20<=tc1<=22):
|
||||
elif 20 <= tc0 <= 22 and 20 <= tc1 <= 22:
|
||||
# Airborne position with GNSS height
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
raise RuntimeError("Incorrect or inconsistent message types")
|
||||
|
||||
|
||||
def position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode position with only one message,
|
||||
knowing reference nearby location, such as previously
|
||||
calculated location, ground station, or airport location, etc.
|
||||
Works with both airborne and surface position messages.
|
||||
The reference position shall be with in 180NM (airborne) or 45NM (surface)
|
||||
def position_with_ref(msg: str, lat_ref: float, lon_ref: float) -> tuple[float, float]:
|
||||
"""Decode position with only one message.
|
||||
|
||||
A reference position is required, which can be previously
|
||||
calculated location, ground station, or airport location.
|
||||
The function works with both airborne and surface position messages.
|
||||
The reference position shall be within 180NM (airborne) or 45NM (surface)
|
||||
of the true position.
|
||||
|
||||
Args:
|
||||
msg (string): even message (28 bytes hexadecimal string)
|
||||
msg (str): even message (28 hexdigits)
|
||||
lat_ref: previous known latitude
|
||||
lon_ref: previous known longitude
|
||||
|
||||
@@ -93,86 +181,104 @@ def position_with_ref(msg, lat_ref, lon_ref):
|
||||
|
||||
tc = typecode(msg)
|
||||
|
||||
if 5<=tc<=8:
|
||||
if tc is None:
|
||||
raise RuntimeError("incorrect or inconsistent message types")
|
||||
|
||||
if 5 <= tc <= 8:
|
||||
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)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types")
|
||||
raise RuntimeError("incorrect or inconsistent message types")
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
def altitude(msg: str) -> None | float:
|
||||
"""Decode aircraft altitude.
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc<5 or tc==19 or tc>22:
|
||||
if tc is None or tc < 5 or tc == 19 or tc > 22:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
if tc>=5 and tc<=8:
|
||||
elif tc >= 5 and tc <= 8:
|
||||
# surface position, altitude 0
|
||||
return 0
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
q = msgbin[47]
|
||||
if q:
|
||||
n = common.bin2int(msgbin[40:47]+msgbin[48:52])
|
||||
alt = n * 25 - 1000
|
||||
return alt
|
||||
else:
|
||||
return None
|
||||
# airborn position
|
||||
return altitude05(msg)
|
||||
|
||||
|
||||
def velocity(msg):
|
||||
def velocity(
|
||||
msg: str, source: bool = False
|
||||
) -> None | tuple[None | float, None | float, None | int, str]:
|
||||
"""Calculate the speed, heading, and vertical rate
|
||||
(handles both airborne or surface message)
|
||||
(handles both airborne or surface message).
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
source (boolean): Include direction and vertical rate sources in return.
|
||||
Default to False.
|
||||
If set to True, the function will return six value instead of four.
|
||||
|
||||
Returns:
|
||||
(int, float, int, string): speed (kt), ground track or heading (degree),
|
||||
rate of climb/descend (ft/min), and speed type
|
||||
('GS' for ground speed, 'AS' for airspeed)
|
||||
int, float, int, string, [string], [string]:
|
||||
- Speed (kt)
|
||||
- Angle (degree), either ground track or heading
|
||||
- Vertical rate (ft/min)
|
||||
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
||||
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
|
||||
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
||||
|
||||
For surface messages, vertical rate and its respective sources are set
|
||||
to None.
|
||||
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
error = "incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
|
||||
if tc is None:
|
||||
raise RuntimeError(error)
|
||||
|
||||
if 5 <= typecode(msg) <= 8:
|
||||
return surface_velocity(msg)
|
||||
if 5 <= tc <= 8:
|
||||
return surface_velocity(msg, source)
|
||||
|
||||
elif typecode(msg) == 19:
|
||||
return airborne_velocity(msg)
|
||||
elif tc == 19:
|
||||
return airborne_velocity(msg, source)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistant message types, expecting 4<TC<9 or TC=19")
|
||||
raise RuntimeError(error)
|
||||
|
||||
|
||||
def speed_heading(msg):
|
||||
def speed_heading(msg: str) -> None | tuple[None | float, None | float]:
|
||||
"""Get speed and ground track (or heading) from the velocity message
|
||||
(handles both airborne or surface message)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
(int, float): speed (kt), ground track or heading (degree)
|
||||
"""
|
||||
spd, trk_or_hdg, rocd, tag = velocity(msg)
|
||||
decoded = velocity(msg)
|
||||
if decoded is None:
|
||||
return None
|
||||
spd, trk_or_hdg, rocd, tag = decoded
|
||||
return spd, trk_or_hdg
|
||||
|
||||
|
||||
def oe_flag(msg):
|
||||
def oe_flag(msg: str) -> int:
|
||||
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
Returns:
|
||||
int: 0 or 1, for even or odd frame
|
||||
"""
|
||||
@@ -180,11 +286,11 @@ def oe_flag(msg):
|
||||
return int(msgbin[53])
|
||||
|
||||
|
||||
def version(msg):
|
||||
def version(msg: str) -> int:
|
||||
"""ADS-B Version
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 31
|
||||
msg (str): 28 hexdigits string, TC = 31
|
||||
|
||||
Returns:
|
||||
int: version number
|
||||
@@ -192,7 +298,9 @@ def version(msg):
|
||||
tc = typecode(msg)
|
||||
|
||||
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)
|
||||
version = common.bin2int(msgbin[72:75])
|
||||
@@ -200,13 +308,15 @@ def version(msg):
|
||||
return version
|
||||
|
||||
|
||||
def nuc_p(msg):
|
||||
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
|
||||
def nuc_p(msg: str) -> tuple[int, None | float, None | int, None | int]:
|
||||
"""Calculate NUCp, Navigation Uncertainty Category - Position
|
||||
(ADS-B version 1)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string,
|
||||
msg (str): 28 hexdigits string,
|
||||
|
||||
Returns:
|
||||
int: NUCp, Navigation Uncertainty Category (position)
|
||||
int: Horizontal Protection Limit
|
||||
int: 95% Containment Radius - Horizontal (meters)
|
||||
int: 95% Containment Radius - Vertical (meters)
|
||||
@@ -214,131 +324,149 @@ def nuc_p(msg):
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
if tc is None or tc < 5 or tc is None or tc > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)" % msg
|
||||
or airborne position with GNSS height (20<TC<22)"
|
||||
% msg
|
||||
)
|
||||
|
||||
try:
|
||||
NUCp = uncertainty.TC_NUCp_lookup[tc]
|
||||
HPL = uncertainty.NUCp[NUCp]['HPL']
|
||||
RCu = uncertainty.NUCp[NUCp]['RCu']
|
||||
RCv = uncertainty.NUCp[NUCp]['RCv']
|
||||
except KeyError:
|
||||
NUCp = uncertainty.TC_NUCp_lookup[tc]
|
||||
index = uncertainty.NUCp.get(NUCp, None)
|
||||
|
||||
if index is not None:
|
||||
HPL = index["HPL"]
|
||||
RCu = index["RCu"]
|
||||
RCv = index["RCv"]
|
||||
else:
|
||||
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
|
||||
|
||||
RCv = uncertainty.NA
|
||||
|
||||
if tc in [20, 21]:
|
||||
RCv = uncertainty.NA
|
||||
# RCv only available for GNSS height
|
||||
if tc == 20:
|
||||
RCv = 4
|
||||
elif tc == 21:
|
||||
RCv = 15
|
||||
|
||||
return HPL, RCu, RCv
|
||||
return NUCp, HPL, RCu, RCv
|
||||
|
||||
|
||||
def nuc_v(msg):
|
||||
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
|
||||
def nuc_v(msg: str) -> tuple[int, None | float, None | float]:
|
||||
"""Calculate NUCv, Navigation Uncertainty Category - Velocity
|
||||
(ADS-B version 1)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string,
|
||||
msg (str): 28 hexdigits string,
|
||||
|
||||
Returns:
|
||||
int: NUCv, Navigation Uncertainty Category (velocity)
|
||||
int or string: 95% Horizontal Velocity Error
|
||||
int or string: 95% Vertical Velocity Error
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg)
|
||||
|
||||
raise RuntimeError(
|
||||
"%s: Not an airborne velocity message, expecting TC = 19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
NUCv = common.bin2int(msgbin[42:45])
|
||||
index = uncertainty.NUCv.get(NUCv, None)
|
||||
|
||||
try:
|
||||
HVE = uncertainty.NUCv[NUCv]['HVE']
|
||||
VVE = uncertainty.NUCv[NUCv]['VVE']
|
||||
except KeyError:
|
||||
if index is not None:
|
||||
HVE = index["HVE"]
|
||||
VVE = index["VVE"]
|
||||
else:
|
||||
HVE, VVE = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return HVE, VVE
|
||||
return NUCv, HVE, VVE
|
||||
|
||||
|
||||
def nic_v1(msg, NICs):
|
||||
def nic_v1(msg: str, NICs: int) -> tuple[int, None | float, None | float]:
|
||||
"""Calculate NIC, navigation integrity category, for ADS-B version 1
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
NICs (int or string): NIC supplement
|
||||
|
||||
Returns:
|
||||
int: NIC, Navigation Integrity Category
|
||||
int or string: Horizontal Radius of Containment
|
||||
int or string: Vertical Protection Limit
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
tc = typecode(msg)
|
||||
if tc is None or tc < 5 or tc > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)" % msg
|
||||
or airborne position with GNSS height (20<TC<22)"
|
||||
% msg
|
||||
)
|
||||
|
||||
tc = typecode(msg)
|
||||
NIC = uncertainty.TC_NICv1_lookup[tc]
|
||||
|
||||
if isinstance(NIC, dict):
|
||||
NIC = NIC[NICs]
|
||||
|
||||
try:
|
||||
Rc = uncertainty.NICv1[NIC][NICs]['Rc']
|
||||
VPL = uncertainty.NICv1[NIC][NICs]['VPL']
|
||||
except KeyError:
|
||||
Rc, VPL = uncertainty.NA, uncertainty.NA
|
||||
d_index = uncertainty.NICv1.get(NIC, None)
|
||||
Rc, VPL = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return Rc, VPL
|
||||
if d_index is not None:
|
||||
index = d_index.get(NICs, None)
|
||||
if index is not None:
|
||||
Rc = index["Rc"]
|
||||
VPL = index["VPL"]
|
||||
|
||||
return NIC, Rc, VPL
|
||||
|
||||
|
||||
def nic_v2(msg, NICa, NICbc):
|
||||
def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int, int]:
|
||||
"""Calculate NIC, navigation integrity category, for ADS-B version 2
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
NICa (int or string): NIC supplement - A
|
||||
NICbc (int or srting): NIC supplement - B or C
|
||||
NICbc (int or string): NIC supplement - B or C
|
||||
|
||||
Returns:
|
||||
int: NIC, Navigation Integrity Category
|
||||
int or string: Horizontal Radius of Containment
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
tc = typecode(msg)
|
||||
if tc is None or tc < 5 or tc > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)" % msg
|
||||
or airborne position with GNSS height (20<TC<22)"
|
||||
% msg
|
||||
)
|
||||
|
||||
tc = typecode(msg)
|
||||
NIC = uncertainty.TC_NICv2_lookup[tc]
|
||||
|
||||
if 20<=tc<=22:
|
||||
if 20 <= tc <= 22:
|
||||
NICs = 0
|
||||
else:
|
||||
NICs = NICa*2 + NICbc
|
||||
NICs = NICa * 2 + NICbc
|
||||
|
||||
try:
|
||||
if isinstance(NIC, dict):
|
||||
NIC = NIC[NICs]
|
||||
|
||||
Rc = uncertainty.NICv2[NIC][NICs]['Rc']
|
||||
Rc = uncertainty.NICv2[NIC][NICs]["Rc"]
|
||||
except KeyError:
|
||||
Rc = uncertainty.NA
|
||||
|
||||
return Rc
|
||||
return NIC, Rc # type: ignore
|
||||
|
||||
|
||||
def nic_s(msg):
|
||||
def nic_s(msg: str) -> int:
|
||||
"""Obtain NIC supplement bit, TC=31 message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: NICs number (0 or 1)
|
||||
@@ -346,7 +474,9 @@ def nic_s(msg):
|
||||
tc = typecode(msg)
|
||||
|
||||
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)
|
||||
nic_s = int(msgbin[75])
|
||||
@@ -354,11 +484,11 @@ def nic_s(msg):
|
||||
return nic_s
|
||||
|
||||
|
||||
def nic_a_c(msg):
|
||||
def nic_a_c(msg: str) -> tuple[int, int]:
|
||||
"""Obtain NICa/c, navigation integrity category supplements a and c
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
(int, int): NICa and NICc number (0 or 1)
|
||||
@@ -366,7 +496,9 @@ def nic_a_c(msg):
|
||||
tc = typecode(msg)
|
||||
|
||||
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)
|
||||
nic_a = int(msgbin[75])
|
||||
@@ -375,19 +507,21 @@ def nic_a_c(msg):
|
||||
return nic_a, nic_c
|
||||
|
||||
|
||||
def nic_b(msg):
|
||||
def nic_b(msg: str) -> int:
|
||||
"""Obtain NICb, navigation integrity category supplement-b
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: NICb number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc < 9 or tc > 18:
|
||||
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
|
||||
if tc is None or tc < 9 or tc > 18:
|
||||
raise RuntimeError(
|
||||
"%s: Not a airborne position message, expecting 8<TC<19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_b = int(msgbin[39])
|
||||
@@ -395,21 +529,27 @@ def nic_b(msg):
|
||||
return nic_b
|
||||
|
||||
|
||||
def nac_p(msg):
|
||||
def nac_p(msg: str) -> tuple[int, int | None, int | None]:
|
||||
"""Calculate NACp, Navigation Accuracy Category - Position
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 29 or 31
|
||||
msg (str): 28 hexdigits string, TC = 29 or 31
|
||||
|
||||
Returns:
|
||||
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
|
||||
int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
|
||||
int: NACp, Navigation Accuracy Category (position)
|
||||
int or string: 95% horizontal accuracy bounds,
|
||||
Estimated Position Uncertainty
|
||||
int or string: 95% vertical accuracy bounds,
|
||||
Vertical Estimated Position Uncertainty
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc not in [29, 31]:
|
||||
raise RuntimeError("%s: Not a target state and status message, \
|
||||
or operation status message, expecting TC = 29 or 31" % msg)
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, \
|
||||
or operation status message, expecting TC = 29 or 31"
|
||||
% msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
@@ -419,57 +559,70 @@ def nac_p(msg):
|
||||
NACp = common.bin2int(msgbin[76:80])
|
||||
|
||||
try:
|
||||
EPU = uncertainty.NACp[NACp]['EPU']
|
||||
VEPU = uncertainty.NACp[NACp]['VEPU']
|
||||
EPU = uncertainty.NACp[NACp]["EPU"]
|
||||
VEPU = uncertainty.NACp[NACp]["VEPU"]
|
||||
except KeyError:
|
||||
EPU, VEPU = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return EPU, VEPU
|
||||
return NACp, EPU, VEPU
|
||||
|
||||
|
||||
def nac_v(msg):
|
||||
def nac_v(msg: str) -> tuple[int, float | None, float | None]:
|
||||
"""Calculate NACv, Navigation Accuracy Category - Velocity
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 19
|
||||
msg (str): 28 hexdigits string, TC = 19
|
||||
|
||||
Returns:
|
||||
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
|
||||
int or string: 95% vertical accuracy bounds for velocity, Vertical Figure of Merit
|
||||
int: NACv, Navigation Accuracy Category (velocity)
|
||||
int or string: 95% horizontal accuracy bounds for velocity,
|
||||
Horizontal Figure of Merit
|
||||
int or string: 95% vertical accuracy bounds for velocity,
|
||||
Vertical Figure of Merit
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg)
|
||||
raise RuntimeError(
|
||||
"%s: Not an airborne velocity message, expecting TC = 19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
NACv = common.bin2int(msgbin[42:45])
|
||||
|
||||
try:
|
||||
HFOMr = uncertainty.NACv[NACv]['HFOMr']
|
||||
VFOMr = uncertainty.NACv[NACv]['VFOMr']
|
||||
HFOMr = uncertainty.NACv[NACv]["HFOMr"]
|
||||
VFOMr = uncertainty.NACv[NACv]["VFOMr"]
|
||||
except KeyError:
|
||||
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return HFOMr, VFOMr
|
||||
return NACv, HFOMr, VFOMr
|
||||
|
||||
|
||||
def sil(msg, version):
|
||||
def sil(
|
||||
msg: str,
|
||||
version: None | int,
|
||||
) -> tuple[float | None, float | None, str]:
|
||||
"""Calculate SIL, Surveillance Integrity Level
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string with TC = 29, 31
|
||||
msg (str): 28 hexdigits string with TC = 29, 31
|
||||
|
||||
Returns:
|
||||
int or string: Probability of exceeding Horizontal Radius of Containment RCu
|
||||
int or string: Probability of exceeding Vertical Integrity Containment Region VPL
|
||||
int or string:
|
||||
Probability of exceeding Horizontal Radius of Containment RCu
|
||||
int or string:
|
||||
Probability of exceeding Vertical Integrity Containment Region VPL
|
||||
string: SIL supplement based on per "hour" or "sample", or 'unknown'
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc not in [29, 31]:
|
||||
raise RuntimeError("%s: Not a target state and status messag, \
|
||||
or operation status message, expecting TC = 29 or 31" % msg)
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, \
|
||||
or operation status message, expecting TC = 29 or 31"
|
||||
% msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
@@ -479,12 +632,12 @@ def sil(msg, version):
|
||||
SIL = common.bin2int(msgbin[82:84])
|
||||
|
||||
try:
|
||||
PE_RCu = uncertainty.SIL[SIL]['PE_RCu']
|
||||
PE_VPL = uncertainty.SIL[SIL]['PE_VPL']
|
||||
PE_RCu = uncertainty.SIL[SIL]["PE_RCu"]
|
||||
PE_VPL = uncertainty.SIL[SIL]["PE_VPL"]
|
||||
except KeyError:
|
||||
PE_RCu, PE_VPL = uncertainty.NA, uncertainty.NA
|
||||
|
||||
base = 'unknown'
|
||||
base = "unknown"
|
||||
|
||||
if version == 2:
|
||||
if tc == 29:
|
||||
|
||||
@@ -1,21 +1,98 @@
|
||||
# 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
|
||||
Decode all-call reply messages, with downlink format 11
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
from .. import common
|
||||
|
||||
T = TypeVar("T")
|
||||
F = Callable[[str], T]
|
||||
|
||||
|
||||
def _checkdf(func: F[T]) -> F[T]:
|
||||
|
||||
"""Ensure downlink format is 11."""
|
||||
|
||||
def wrapper(msg: str) -> T:
|
||||
df = common.df(msg)
|
||||
if df != 11:
|
||||
raise RuntimeError(
|
||||
"Incorrect downlink format, expect 11, got {}".format(df)
|
||||
)
|
||||
return func(msg)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@_checkdf
|
||||
def icao(msg: str) -> None | str:
|
||||
"""Decode transponder code (ICAO address).
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
string: ICAO address
|
||||
|
||||
"""
|
||||
return common.icao(msg)
|
||||
|
||||
|
||||
@_checkdf
|
||||
def interrogator(msg: str) -> str:
|
||||
"""Decode interrogator identifier code.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int: interrogator identifier code
|
||||
|
||||
"""
|
||||
# the CRC remainder contains the CL and IC field.
|
||||
# the top three bits are CL field and last four bits are IC field.
|
||||
remainder = common.crc(msg)
|
||||
if remainder > 79:
|
||||
IC = "corrupt IC"
|
||||
elif remainder < 16:
|
||||
IC = "II" + str(remainder)
|
||||
else:
|
||||
IC = "SI" + str(remainder - 16)
|
||||
return IC
|
||||
|
||||
|
||||
@_checkdf
|
||||
def capability(msg: str) -> tuple[int, None | str]:
|
||||
"""Decode transponder capability.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: transponder capability, description
|
||||
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
ca = common.bin2int(msgbin[5:8])
|
||||
|
||||
if ca == 0:
|
||||
text = "level 1 transponder"
|
||||
elif ca == 4:
|
||||
text = "level 2 transponder, ability to set CA to 7, on ground"
|
||||
elif ca == 5:
|
||||
text = "level 2 transponder, ability to set CA to 7, airborne"
|
||||
elif ca == 6:
|
||||
text = (
|
||||
"evel 2 transponder, ability to set CA to 7, "
|
||||
"either airborne or ground"
|
||||
)
|
||||
elif ca == 7:
|
||||
text = (
|
||||
"Downlink Request value is 0, "
|
||||
"or the Flight Status is 2, 3, 4 or 5, "
|
||||
"either airborne or on the ground"
|
||||
)
|
||||
else:
|
||||
text = None
|
||||
|
||||
return ca, text
|
||||
|
||||
@@ -18,59 +18,83 @@
|
||||
Common functions for Mode-S decoding
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pyModeS.extra import aero
|
||||
from pyModeS.decoder import common
|
||||
from pyModeS.decoder.bds import bds05, bds06, bds08, bds09, \
|
||||
bds10, bds17, bds20, bds30, bds40, bds44, bds50, bds53, bds60
|
||||
from ... import common
|
||||
from ...extra import aero
|
||||
from . import ( # noqa: F401
|
||||
bds10,
|
||||
bds17,
|
||||
bds20,
|
||||
bds30,
|
||||
bds40,
|
||||
bds44,
|
||||
bds45,
|
||||
bds50,
|
||||
bds60,
|
||||
bds61_st1,
|
||||
bds62,
|
||||
)
|
||||
|
||||
|
||||
def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
||||
"""Use reference ground speed and trk to determine BDS50 and DBS60
|
||||
def is50or60(msg: str, spd_ref: float, trk_ref: float, alt_ref: float) -> Optional[str]:
|
||||
"""Use reference ground speed and trk to determine BDS50 and DBS60.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
spd_ref (float): reference speed (ADS-B ground speed), kts
|
||||
trk_ref (float): reference track (ADS-B track angle), deg
|
||||
alt_ref (float): reference altitude (ADS-B altitude), ft
|
||||
|
||||
Returns:
|
||||
String or None: BDS version, or possible versions, or None if nothing matches.
|
||||
String or None: BDS version, or possible versions,
|
||||
or None if nothing matches.
|
||||
|
||||
"""
|
||||
|
||||
def vxy(v, angle):
|
||||
vx = v * np.sin(np.radians(angle))
|
||||
vy = v * np.cos(np.radians(angle))
|
||||
return vx, vy
|
||||
|
||||
# message must be both BDS 50 and 60 before processing
|
||||
if not (bds50.is50(msg) and bds60.is60(msg)):
|
||||
return None
|
||||
|
||||
h50 = bds50.trk50(msg)
|
||||
v50 = bds50.gs50(msg)
|
||||
|
||||
if h50 is None or v50 is None:
|
||||
return 'BDS50,BDS60'
|
||||
|
||||
# --- assuming BDS60 ---
|
||||
h60 = bds60.hdg60(msg)
|
||||
m60 = bds60.mach60(msg)
|
||||
i60 = bds60.ias60(msg)
|
||||
|
||||
# additional check now knowing the altitude
|
||||
if (m60 is not None) and (i60 is not None):
|
||||
ias_ = aero.mach2cas(m60, alt_ref * aero.ft) / aero.kts
|
||||
if abs(i60 - ias_) > 20:
|
||||
return "BDS50"
|
||||
|
||||
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
|
||||
i60 = np.nan if i60 is None else i60
|
||||
|
||||
XY5 = vxy(v50*aero.kts, h50)
|
||||
XY6m = vxy(aero.mach2tas(m60, alt_ref*aero.ft), h60)
|
||||
XY6i = vxy(aero.cas2tas(i60*aero.kts, alt_ref*aero.ft), h60)
|
||||
# --- assuming BDS50 ---
|
||||
h50 = bds50.trk50(msg)
|
||||
v50 = bds50.gs50(msg)
|
||||
|
||||
allbds = ['BDS50', 'BDS60', 'BDS60']
|
||||
if h50 is None or v50 is None:
|
||||
return "BDS50,BDS60"
|
||||
|
||||
XY5 = vxy(v50 * aero.kts, h50)
|
||||
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
|
||||
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
|
||||
|
||||
allbds = ["BDS50", "BDS60", "BDS60"]
|
||||
|
||||
X = np.array([XY5, XY6m, XY6i])
|
||||
Mu = np.array(vxy(spd_ref*aero.kts, trk_ref))
|
||||
Mu = np.array(vxy(spd_ref * aero.kts, trk_ref))
|
||||
|
||||
# compute Mahalanobis distance matrix
|
||||
# Cov = [[20**2, 0], [0, 20**2]]
|
||||
@@ -80,51 +104,56 @@ def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
||||
# since the covariance matrix is identity matrix,
|
||||
# M-dist is same as eculidian distance
|
||||
try:
|
||||
dist = np.linalg.norm(X-Mu, axis=1)
|
||||
dist = np.linalg.norm(X - Mu, axis=1)
|
||||
BDS = allbds[np.nanargmin(dist)]
|
||||
except ValueError:
|
||||
return 'BDS50,BDS60'
|
||||
return "BDS50,BDS60"
|
||||
|
||||
return BDS
|
||||
|
||||
|
||||
def infer(msg):
|
||||
"""Estimate the most likely BDS code of an message
|
||||
def infer(msg: str, mrar: bool = False) -> Optional[str]:
|
||||
"""Estimate the most likely BDS code of an message.
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
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.
|
||||
|
||||
"""
|
||||
df = common.df(msg)
|
||||
|
||||
if common.allzeros(msg):
|
||||
return 'EMPTY'
|
||||
return "EMPTY"
|
||||
|
||||
# For ADS-B / Mode-S extended squitter
|
||||
if df == 17:
|
||||
tc = common.typecode(msg)
|
||||
if tc is None:
|
||||
return None
|
||||
|
||||
if 1 <= tc <= 4:
|
||||
return 'BDS08' # indentification and category
|
||||
return "BDS08" # identification and category
|
||||
if 5 <= tc <= 8:
|
||||
return 'BDS06' # surface movement
|
||||
return "BDS06" # surface movement
|
||||
if 9 <= tc <= 18:
|
||||
return 'BDS05' # airborne position, baro-alt
|
||||
return "BDS05" # airborne position, baro-alt
|
||||
if tc == 19:
|
||||
return 'BDS09' # airborne velocity
|
||||
return "BDS09" # airborne velocity
|
||||
if 20 <= tc <= 22:
|
||||
return 'BDS05' # airborne position, gnss-alt
|
||||
return "BDS05" # airborne position, gnss-alt
|
||||
if tc == 28:
|
||||
return 'BDS61' # aircraft status
|
||||
return "BDS61" # aircraft status
|
||||
if tc == 29:
|
||||
return 'BDS62' # target state and status
|
||||
return "BDS62" # target state and status
|
||||
if tc == 31:
|
||||
return 'BDS65' # operational status
|
||||
return "BDS65" # operational status
|
||||
|
||||
# For Comm-B replies, ELS + EHS only
|
||||
# For Comm-B replies
|
||||
IS10 = bds10.is10(msg)
|
||||
IS17 = bds17.is17(msg)
|
||||
IS20 = bds20.is20(msg)
|
||||
@@ -132,14 +161,31 @@ def infer(msg):
|
||||
IS40 = bds40.is40(msg)
|
||||
IS50 = bds50.is50(msg)
|
||||
IS60 = bds60.is60(msg)
|
||||
IS44 = bds44.is44(msg)
|
||||
IS45 = bds45.is45(msg)
|
||||
|
||||
allbds = np.array([
|
||||
"BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60"
|
||||
])
|
||||
if mrar:
|
||||
allbds = np.array(
|
||||
[
|
||||
"BDS10",
|
||||
"BDS17",
|
||||
"BDS20",
|
||||
"BDS30",
|
||||
"BDS40",
|
||||
"BDS44",
|
||||
"BDS45",
|
||||
"BDS50",
|
||||
"BDS60",
|
||||
]
|
||||
)
|
||||
mask = [IS10, IS17, IS20, IS30, IS40, IS44, IS45, IS50, IS60]
|
||||
else:
|
||||
allbds = np.array(
|
||||
["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:
|
||||
return None
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
# ------------------------------------------
|
||||
# BDS 0,5
|
||||
# ADS-B TC=9-18
|
||||
# Airborne position
|
||||
# ------------------------------------------
|
||||
|
||||
# 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.
|
||||
from __future__ import annotations
|
||||
|
||||
# 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.
|
||||
from datetime import datetime
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from ... import common
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,5
|
||||
ADS-B TC=9-18
|
||||
Airborn position
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
def airborne_position(msg0, msg1, t0, t1):
|
||||
"""Decode airborn position from a pair of even and odd position message
|
||||
def airborne_position(
|
||||
msg0: str, msg1: str, t0: int | datetime, t1: int | datetime
|
||||
) -> None | tuple[float, float]:
|
||||
"""Decode airborne position from a pair of even and odd position message
|
||||
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
msg0 (string): even message (28 hexdigits)
|
||||
msg1 (string): odd message (28 hexdigits)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
|
||||
@@ -41,14 +29,24 @@ def airborne_position(msg0, msg1, t0, t1):
|
||||
mb0 = common.hex2bin(msg0)[32:]
|
||||
mb1 = common.hex2bin(msg1)[32:]
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
|
||||
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
|
||||
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
|
||||
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
|
||||
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.")
|
||||
|
||||
air_d_lat_even = 360.0 / 60
|
||||
air_d_lat_odd = 360.0 / 59
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(mb0[22:39]) / 131072
|
||||
cprlon_even = common.bin2int(mb0[39:56]) / 131072
|
||||
cprlat_odd = common.bin2int(mb1[22:39]) / 131072
|
||||
cprlon_odd = common.bin2int(mb1[39:56]) / 131072
|
||||
|
||||
air_d_lat_even = 360 / 60
|
||||
air_d_lat_odd = 360 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
@@ -67,18 +65,19 @@ def airborne_position(msg0, msg1, t0, t1):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
# (people pass int+int or datetime+datetime)
|
||||
if t0 > t1: # type: ignore
|
||||
lat = lat_even
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat)- 0, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_even)
|
||||
ni = max(common.cprNL(lat) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat) - 1, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_odd)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
if lon > 180:
|
||||
lon = lon - 360
|
||||
@@ -86,14 +85,16 @@ def airborne_position(msg0, msg1, t0, t1):
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
def airborne_position_with_ref(
|
||||
msg: str, lat_ref: float, lon_ref: float
|
||||
) -> tuple[float, float]:
|
||||
"""Decode airborne position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 180NM of the true position.
|
||||
be within 180NM of the true position.
|
||||
|
||||
Args:
|
||||
msg (string): even message (28 bytes hexadecimal string)
|
||||
msg (str): even message (28 hexdigits)
|
||||
lat_ref: previous known latitude
|
||||
lon_ref: previous known longitude
|
||||
|
||||
@@ -101,40 +102,41 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072
|
||||
|
||||
i = int(mb[21])
|
||||
d_lat = 360.0/59 if i else 360.0/60
|
||||
d_lat = 360 / 59 if i else 360 / 60
|
||||
|
||||
j = common.floor(lat_ref / d_lat) \
|
||||
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
|
||||
j = common.floor(lat_ref / d_lat) + common.floor(
|
||||
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
||||
)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = common.cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 360.0 / ni
|
||||
d_lon = 360 / ni
|
||||
else:
|
||||
d_lon = 360.0
|
||||
d_lon = 360
|
||||
|
||||
m = common.floor(lon_ref / d_lon) \
|
||||
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
m = common.floor(lon_ref / d_lon) + common.floor(
|
||||
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
||||
)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
def altitude(msg: str) -> None | int:
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
@@ -142,21 +144,14 @@ def altitude(msg):
|
||||
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if tc<9 or tc==19 or tc>22:
|
||||
raise RuntimeError("%s: Not a airborn position message" % msg)
|
||||
if tc is None or tc < 9 or tc == 19 or tc > 22:
|
||||
raise RuntimeError("%s: Not an airborne position message" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
altbin = mb[8:20]
|
||||
|
||||
if tc < 19:
|
||||
# barometric altitude
|
||||
q = mb[15]
|
||||
if q:
|
||||
n = common.bin2int(mb[8:15]+mb[16:20])
|
||||
alt = n * 25 - 1000
|
||||
else:
|
||||
alt = None
|
||||
altcode = altbin[0:6] + "0" + altbin[6:]
|
||||
return common.altitude(altcode)
|
||||
else:
|
||||
# GNSS altitude, meters -> feet
|
||||
alt = common.bin2int(mb[8:20]) * 3.28084
|
||||
|
||||
return alt
|
||||
return common.bin2int(altbin) * 3.28084 # type: ignore
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
# ------------------------------------------
|
||||
# BDS 0,6
|
||||
# ADS-B TC=5-8
|
||||
# Surface movement
|
||||
# ------------------------------------------
|
||||
|
||||
# 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.
|
||||
from __future__ import annotations
|
||||
|
||||
# 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.
|
||||
from datetime import datetime
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from ... import common
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,6
|
||||
ADS-B TC=5-8
|
||||
Surface position
|
||||
------------------------------------------
|
||||
"""
|
||||
def surface_position(
|
||||
msg0: str,
|
||||
msg1: str,
|
||||
t0: int | datetime,
|
||||
t1: int | datetime,
|
||||
lat_ref: float,
|
||||
lon_ref: float,
|
||||
) -> None | tuple[float, float]:
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
import math
|
||||
|
||||
|
||||
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
"""Decode surface position from a pair of even and odd position message,
|
||||
the lat/lon of receiver must be provided to yield the correct solution.
|
||||
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
msg0 (string): even message (28 hexdigits)
|
||||
msg1 (string): odd message (28 hexdigits)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
lat_ref (float): latitude of the receiver
|
||||
@@ -47,13 +39,13 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
msgbin1 = common.hex2bin(msg1)
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
|
||||
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
|
||||
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
|
||||
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
|
||||
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072
|
||||
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072
|
||||
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072
|
||||
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072
|
||||
|
||||
air_d_lat_even = 90.0 / 60
|
||||
air_d_lat_odd = 90.0 / 59
|
||||
air_d_lat_even = 90 / 60
|
||||
air_d_lat_odd = 90 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
@@ -63,8 +55,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_s = lat_even_n - 90.0
|
||||
lat_odd_s = lat_odd_n - 90.0
|
||||
lat_even_s = lat_even_n - 90
|
||||
lat_odd_s = lat_odd_n - 90
|
||||
|
||||
# chose which solution corrispondes to receiver location
|
||||
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
|
||||
@@ -75,38 +67,44 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
# (people pass int+int or datetime+datetime)
|
||||
if t0 > t1: # type: ignore
|
||||
lat = lat_even
|
||||
nl = common.cprNL(lat_even)
|
||||
ni = max(common.cprNL(lat_even) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_even)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = common.cprNL(lat_odd)
|
||||
ni = max(common.cprNL(lat_odd) - 1, 1)
|
||||
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_odd)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
# four possible longitude solutions
|
||||
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
|
||||
lons = [lon, lon + 90, lon + 180, lon + 270]
|
||||
|
||||
# make sure lons are between -180 and 180
|
||||
lons = [(lon + 180) % 360 - 180 for lon in lons]
|
||||
|
||||
# the closest solution to receiver is the correct one
|
||||
dls = [abs(lon_ref - l) for l in lons]
|
||||
dls = [abs(lon_ref - lon) for lon in lons]
|
||||
imin = min(range(4), key=dls.__getitem__)
|
||||
lon = lons[imin]
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
def surface_position_with_ref(
|
||||
msg: str, lat_ref: float, lon_ref: float
|
||||
) -> tuple[float, float]:
|
||||
"""Decode surface position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 45NM of the true position.
|
||||
be within 45NM of the true position.
|
||||
|
||||
Args:
|
||||
msg (string): even message (28 bytes hexadecimal string)
|
||||
msg (str): even message (28 hexdigits)
|
||||
lat_ref: previous known latitude
|
||||
lon_ref: previous known longitude
|
||||
|
||||
@@ -114,47 +112,59 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072
|
||||
|
||||
i = int(mb[21])
|
||||
d_lat = 90.0/59 if i else 90.0/60
|
||||
d_lat = 90 / 59 if i else 90 / 60
|
||||
|
||||
j = common.floor(lat_ref / d_lat) \
|
||||
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
|
||||
j = common.floor(lat_ref / d_lat) + common.floor(
|
||||
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
||||
)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = common.cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 90.0 / ni
|
||||
d_lon = 90 / ni
|
||||
else:
|
||||
d_lon = 90.0
|
||||
d_lon = 90
|
||||
|
||||
m = common.floor(lon_ref / d_lon) \
|
||||
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
|
||||
m = common.floor(lon_ref / d_lon) + common.floor(
|
||||
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
||||
)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_velocity(msg):
|
||||
"""Decode surface velocity from from a surface position message
|
||||
def surface_velocity(
|
||||
msg: str, source: bool = False
|
||||
) -> tuple[None | float, float, int, str]:
|
||||
"""Decode surface velocity from a surface position message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
source (boolean): Include direction and vertical rate sources in return.
|
||||
Default to False.
|
||||
If set to True, the function will return six value instead of four.
|
||||
|
||||
Returns:
|
||||
(int, float, int, string): speed (kt), ground track (degree),
|
||||
rate of climb/descend (ft/min), and speed type
|
||||
('GS' for ground speed, 'AS' for airspeed)
|
||||
"""
|
||||
int, float, int, string, [string], [string]:
|
||||
- Speed (kt)
|
||||
- Angle (degree), ground track
|
||||
- Vertical rate, always 0
|
||||
- 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:
|
||||
"""
|
||||
tc = common.typecode(msg)
|
||||
if tc is None or tc < 5 or tc > 8:
|
||||
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
@@ -162,26 +172,28 @@ def surface_velocity(msg):
|
||||
# ground track
|
||||
trk_status = int(mb[12])
|
||||
if trk_status == 1:
|
||||
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
|
||||
trk = common.bin2int(mb[13:20]) * 360 / 128
|
||||
trk = round(trk, 1)
|
||||
else:
|
||||
trk = None
|
||||
|
||||
# ground movment / speed
|
||||
# ground movement / speed
|
||||
mov = common.bin2int(mb[5:12])
|
||||
|
||||
if mov == 0 or mov > 124:
|
||||
spd = None
|
||||
elif mov == 1:
|
||||
spd = 0
|
||||
spd = 0.0
|
||||
elif mov == 124:
|
||||
spd = 175
|
||||
spd = 175.0
|
||||
else:
|
||||
movs = [2, 9, 13, 39, 94, 109, 124]
|
||||
kts = [0.125, 1, 2, 15, 70, 100, 175]
|
||||
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
|
||||
step = (kts[i] - kts[i-1]) * 1.0 / (movs[i]-movs[i-1])
|
||||
spd = kts[i-1] + (mov-movs[i-1]) * step
|
||||
spd = round(spd, 2)
|
||||
mov_lb = [2, 9, 13, 39, 94, 109, 124]
|
||||
kts_lb: list[float] = [0.125, 1, 2, 15, 70, 100, 175]
|
||||
step: list[float] = [0.125, 0.25, 0.5, 1, 2, 5]
|
||||
i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov)
|
||||
spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1]
|
||||
|
||||
return spd, trk, 0, 'GS'
|
||||
if source:
|
||||
return spd, trk, 0, "GS", "TRUE_NORTH", None # type: ignore
|
||||
else:
|
||||
return spd, trk, 0, "GS"
|
||||
|
||||
@@ -1,65 +1,50 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
# ------------------------------------------
|
||||
# BDS 0,8
|
||||
# ADS-B TC=1-4
|
||||
# Aircraft identification and category
|
||||
# ------------------------------------------
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from ... import common
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,8
|
||||
ADS-B TC=1-4
|
||||
Aircraft identitification and category
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
def category(msg):
|
||||
def category(msg: str) -> int:
|
||||
"""Aircraft category number
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: category number
|
||||
"""
|
||||
|
||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
||||
tc = common.typecode(msg)
|
||||
if tc is None or tc < 1 or tc > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % 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: str) -> str:
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
string: callsign
|
||||
"""
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
||||
if tc is None or tc < 1 or tc > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
|
||||
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
||||
msgbin = common.hex2bin(msg)
|
||||
csbin = msgbin[40:96]
|
||||
|
||||
cs = ''
|
||||
cs = ""
|
||||
cs += chars[common.bin2int(csbin[0:6])]
|
||||
cs += chars[common.bin2int(csbin[6:12])]
|
||||
cs += chars[common.bin2int(csbin[12:18])]
|
||||
@@ -71,5 +56,5 @@ def callsign(msg):
|
||||
|
||||
# clean string, remove spaces and marks, if any.
|
||||
# cs = cs.replace('_', '')
|
||||
cs = cs.replace('#', '')
|
||||
cs = cs.replace("#", "")
|
||||
return cs
|
||||
|
||||
@@ -1,46 +1,41 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
# ------------------------------------------
|
||||
# BDS 0,9
|
||||
# ADS-B TC=19
|
||||
# Aircraft Airborne velocity
|
||||
# ------------------------------------------
|
||||
|
||||
# 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.
|
||||
from __future__ import annotations
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 0,9
|
||||
ADS-B TC=19
|
||||
Aircraft Airborn velocity
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
import math
|
||||
|
||||
from ... import common
|
||||
|
||||
def airborne_velocity(msg):
|
||||
"""Calculate the speed, track (or heading), and vertical rate
|
||||
|
||||
def airborne_velocity(
|
||||
msg: str, source: bool = False
|
||||
) -> None | tuple[None | int, None | float, None | int, str]:
|
||||
"""Decode airborne velocity.
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
source (boolean): Include direction and vertical rate sources in return.
|
||||
Default to False.
|
||||
If set to True, the function will return six value instead of four.
|
||||
|
||||
Returns:
|
||||
(int, float, int, string): speed (kt), ground track or heading (degree),
|
||||
rate of climb/descend (ft/min), and speed type
|
||||
('GS' for ground speed, 'AS' for airspeed)
|
||||
"""
|
||||
int, float, int, string, [string], [string]:
|
||||
- Speed (kt)
|
||||
- Angle (degree), either ground track or heading
|
||||
- Vertical rate (ft/min)
|
||||
- Speed type ('GS' for ground speed, 'AS' for airspeed)
|
||||
- [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH')
|
||||
- [Optional] Vertical rate source ('BARO' or 'GNSS')
|
||||
|
||||
"""
|
||||
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
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
@@ -49,63 +44,100 @@ def airborne_velocity(msg):
|
||||
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
|
||||
return None
|
||||
|
||||
trk_or_hdg: None | float
|
||||
spd: None | float
|
||||
|
||||
if subtype in (1, 2):
|
||||
v_ew_sign = -1 if mb[13]=='1' else 1
|
||||
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
|
||||
|
||||
v_ns_sign = -1 if mb[24]=='1' else 1
|
||||
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
|
||||
v_ew = common.bin2int(mb[14:24])
|
||||
v_ns = common.bin2int(mb[25:35])
|
||||
|
||||
v_we = v_ew_sign * v_ew
|
||||
v_sn = v_ns_sign * v_ns
|
||||
if v_ew == 0 or v_ns == 0:
|
||||
spd = None
|
||||
trk_or_hdg = None
|
||||
vs = None
|
||||
else:
|
||||
v_ew_sign = -1 if mb[13] == "1" else 1
|
||||
v_ew = v_ew - 1 # east-west velocity
|
||||
if subtype == 2: # Supersonic
|
||||
v_ew *= 4
|
||||
|
||||
spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts
|
||||
spd = int(spd)
|
||||
v_ns_sign = -1 if mb[24] == "1" else 1
|
||||
v_ns = v_ns - 1 # north-south velocity
|
||||
if subtype == 2: # Supersonic
|
||||
v_ns *= 4
|
||||
|
||||
trk = math.atan2(v_we, v_sn)
|
||||
trk = math.degrees(trk) # convert to degrees
|
||||
trk = trk if trk >= 0 else trk + 360 # no negative val
|
||||
v_we = v_ew_sign * v_ew
|
||||
v_sn = v_ns_sign * v_ns
|
||||
|
||||
tag = 'GS'
|
||||
trk_or_hdg = round(trk, 2)
|
||||
spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts
|
||||
spd = int(spd)
|
||||
|
||||
trk = math.atan2(v_we, v_sn)
|
||||
trk = math.degrees(trk) # convert to degrees
|
||||
trk = trk if trk >= 0 else trk + 360 # no negative val
|
||||
|
||||
trk_or_hdg = round(trk, 2)
|
||||
|
||||
spd_type = "GS"
|
||||
dir_type = "TRUE_NORTH"
|
||||
|
||||
else:
|
||||
if mb[13] == '0':
|
||||
if mb[13] == "0":
|
||||
hdg = None
|
||||
else:
|
||||
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
|
||||
hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
|
||||
hdg = round(hdg, 2)
|
||||
|
||||
trk_or_hdg = hdg
|
||||
|
||||
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 and spd is not None: # Supersonic
|
||||
spd *= 4
|
||||
|
||||
if mb[24]=='0':
|
||||
tag = 'IAS'
|
||||
if mb[24] == "0":
|
||||
spd_type = "IAS"
|
||||
else:
|
||||
tag = 'TAS'
|
||||
spd_type = "TAS"
|
||||
|
||||
vr_sign = -1 if mb[36]=='1' else 1
|
||||
dir_type = "MAGNETIC_NORTH"
|
||||
|
||||
vr_source = "GNSS" if mb[35] == "0" else "BARO"
|
||||
vr_sign = -1 if mb[36] == "1" else 1
|
||||
vr = common.bin2int(mb[37:46])
|
||||
rocd = None if vr==0 else int(vr_sign*(vr-1)*64)
|
||||
vs = None if vr == 0 else int(vr_sign * (vr - 1) * 64)
|
||||
|
||||
return spd, trk_or_hdg, rocd, tag
|
||||
if source:
|
||||
return ( # type: ignore
|
||||
spd,
|
||||
trk_or_hdg,
|
||||
vs,
|
||||
spd_type,
|
||||
dir_type,
|
||||
vr_source,
|
||||
)
|
||||
else:
|
||||
return spd, trk_or_hdg, vs, spd_type
|
||||
|
||||
def altitude_diff(msg):
|
||||
"""Decode the differece between GNSS and barometric altitude
|
||||
|
||||
def altitude_diff(msg: str) -> None | float:
|
||||
"""Decode the differece between GNSS and barometric altitude.
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC=19
|
||||
msg (str): 28 hexdigits string, TC=19
|
||||
|
||||
Returns:
|
||||
int: Altitude difference in ft. Negative value indicates GNSS altitude
|
||||
below barometric altitude.
|
||||
int: Altitude difference in feet. Negative value indicates GNSS altitude
|
||||
below barometric altitude.
|
||||
|
||||
"""
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
||||
if tc is None or tc != 19:
|
||||
raise RuntimeError(
|
||||
"%s: Not a airborne velocity message, expecting TC=19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
sign = -1 if int(msgbin[80]) else 1
|
||||
@@ -114,4 +146,4 @@ def altitude_diff(msg):
|
||||
if value == 0 or value == 127:
|
||||
return None
|
||||
else:
|
||||
return sign * (value - 1) * 25 # in ft.
|
||||
return sign * (value - 1) * 25 # in ft.
|
||||
|
||||
@@ -1,66 +1,53 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 1,0
|
||||
# Data link capability report
|
||||
# ------------------------------------------
|
||||
|
||||
def is10(msg):
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is10(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 1,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
# first 8 bits must be 0x10
|
||||
if d[0:8] != '00010000':
|
||||
if d[0:8] != "00010000":
|
||||
return False
|
||||
|
||||
# bit 10 to 14 are reserved
|
||||
if bin2int(d[9:14]) != 0:
|
||||
if common.bin2int(d[9:14]) != 0:
|
||||
return False
|
||||
|
||||
# overlay capabilty conflict
|
||||
if d[14] == '1' and bin2int(d[16:23]) < 5:
|
||||
# overlay capability conflict
|
||||
if d[14] == "1" and common.bin2int(d[16:23]) < 5:
|
||||
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 True
|
||||
|
||||
def ovc10(msg):
|
||||
|
||||
def ovc10(msg: str) -> int:
|
||||
"""Return the overlay control capability
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Whether the transponder is OVC capable
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
return int(d[14])
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
# ------------------------------------------
|
||||
# BDS 1,7
|
||||
# Common usage GICB capability report
|
||||
# ------------------------------------------
|
||||
|
||||
# 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.
|
||||
from typing import List
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from ... import common
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
"""
|
||||
------------------------------------------
|
||||
BDS 1,7
|
||||
Common usage GICB capability report
|
||||
------------------------------------------
|
||||
"""
|
||||
|
||||
def is17(msg):
|
||||
def is17(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 1,7
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if bin2int(d[28:56]) != 0:
|
||||
if common.bin2int(d[24:56]) != 0:
|
||||
return False
|
||||
|
||||
caps = cap17(msg)
|
||||
@@ -50,26 +34,50 @@ def is17(msg):
|
||||
# return False
|
||||
|
||||
# at least you can respond who you are
|
||||
if 'BDS20' not in caps:
|
||||
if "BDS20" not in caps:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def cap17(msg):
|
||||
|
||||
def cap17(msg: str) -> List[str]:
|
||||
"""Extract capacities from BDS 1,7 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
list: list of suport BDS codes
|
||||
list: list of supported BDS codes
|
||||
"""
|
||||
allbds = ['05', '06', '07', '08', '09', '0A', '20', '21', '40', '41',
|
||||
'42', '43', '44', '45', '48', '50', '51', '52', '53', '54',
|
||||
'55', '56', '5F', '60', 'NA', 'NA', 'E1', 'E2']
|
||||
allbds = [
|
||||
"05",
|
||||
"06",
|
||||
"07",
|
||||
"08",
|
||||
"09",
|
||||
"0A",
|
||||
"20",
|
||||
"21",
|
||||
"40",
|
||||
"41",
|
||||
"42",
|
||||
"43",
|
||||
"44",
|
||||
"45",
|
||||
"48",
|
||||
"50",
|
||||
"51",
|
||||
"52",
|
||||
"53",
|
||||
"54",
|
||||
"55",
|
||||
"56",
|
||||
"5F",
|
||||
"60",
|
||||
]
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
idx = [i for i, v in enumerate(d[:28]) if v=='1']
|
||||
capacity = ['BDS'+allbds[i] for i in idx if allbds[i] is not 'NA']
|
||||
d = common.hex2bin(common.data(msg))
|
||||
idx = [i for i, v in enumerate(d[:24]) if v == "1"]
|
||||
capacity = ["BDS" + allbds[i] for i in idx]
|
||||
|
||||
return capacity
|
||||
|
||||
@@ -1,73 +1,60 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 2,0
|
||||
# Aircraft identification
|
||||
# ------------------------------------------
|
||||
|
||||
def is20(msg):
|
||||
from ... import common
|
||||
|
||||
|
||||
def is20(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 2,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[0:8] != '00100000':
|
||||
if d[0:8] != "00100000":
|
||||
return False
|
||||
|
||||
cs = cs20(msg)
|
||||
# allow empty callsign
|
||||
if common.bin2int(d[8:56]) == 0:
|
||||
return True
|
||||
|
||||
if '#' in cs:
|
||||
if "#" in cs20(msg):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def cs20(msg):
|
||||
def cs20(msg: str) -> str:
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
string: callsign, max. 8 chars
|
||||
"""
|
||||
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
|
||||
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
cs = ''
|
||||
cs += chars[bin2int(d[8:14])]
|
||||
cs += chars[bin2int(d[14:20])]
|
||||
cs += chars[bin2int(d[20:26])]
|
||||
cs += chars[bin2int(d[26:32])]
|
||||
cs += chars[bin2int(d[32:38])]
|
||||
cs += chars[bin2int(d[38:44])]
|
||||
cs += chars[bin2int(d[44:50])]
|
||||
cs += chars[bin2int(d[50:56])]
|
||||
cs = ""
|
||||
cs += chars[common.bin2int(d[8:14])]
|
||||
cs += chars[common.bin2int(d[14:20])]
|
||||
cs += chars[common.bin2int(d[20:26])]
|
||||
cs += chars[common.bin2int(d[26:32])]
|
||||
cs += chars[common.bin2int(d[32:38])]
|
||||
cs += chars[common.bin2int(d[38:44])]
|
||||
cs += chars[common.bin2int(d[44:50])]
|
||||
cs += chars[common.bin2int(d[50:56])]
|
||||
|
||||
return cs
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 3,0
|
||||
# ACAS active resolution advisory
|
||||
# ------------------------------------------
|
||||
|
||||
def is30(msg):
|
||||
"""Check if a message is likely to be BDS code 2,0
|
||||
from ... import common
|
||||
|
||||
|
||||
def is30(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 3,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[0:8] != '00110000':
|
||||
if d[0:8] != "00110000":
|
||||
return False
|
||||
|
||||
# threat type 3 not assigned
|
||||
if d[28:30] == '11':
|
||||
if d[28:30] == "11":
|
||||
return False
|
||||
|
||||
# reserved for ACAS III, in far future
|
||||
if bin2int(d[15:22]) >= 48:
|
||||
if common.bin2int(d[15:22]) >= 48:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,119 +1,124 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,0
|
||||
# Selected vertical intention
|
||||
# ------------------------------------------
|
||||
|
||||
def is40(msg):
|
||||
import warnings
|
||||
from typing import Optional
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is40(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 4,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
# status bit 1, 14, and 27
|
||||
|
||||
if wrongstatus(d, 1, 2, 13):
|
||||
if common.wrongstatus(d, 1, 2, 13):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 14, 15, 26):
|
||||
if common.wrongstatus(d, 14, 15, 26):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 27, 28, 39):
|
||||
if common.wrongstatus(d, 27, 28, 39):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 48, 49, 51):
|
||||
|
||||
if common.wrongstatus(d, 48, 49, 51):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 54, 55, 56):
|
||||
|
||||
if common.wrongstatus(d, 54, 55, 56):
|
||||
return False
|
||||
|
||||
# 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
|
||||
|
||||
if bin2int(d[51:53]) != 0:
|
||||
if common.bin2int(d[51:53]) != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def alt40mcp(msg):
|
||||
def selalt40mcp(msg: str) -> Optional[int]:
|
||||
"""Selected altitude, MCP/FCU
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[0] == '0':
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
alt = bin2int(d[1:13]) * 16 # ft
|
||||
alt = common.bin2int(d[1:13]) * 16 # ft
|
||||
return alt
|
||||
|
||||
|
||||
def alt40fms(msg):
|
||||
def selalt40fms(msg: str) -> Optional[int]:
|
||||
"""Selected altitude, FMS
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[13] == '0':
|
||||
if d[13] == "0":
|
||||
return None
|
||||
|
||||
alt = bin2int(d[14:26]) * 16 # ft
|
||||
alt = common.bin2int(d[14:26]) * 16 # ft
|
||||
return alt
|
||||
|
||||
|
||||
def p40baro(msg):
|
||||
def p40baro(msg: str) -> Optional[float]:
|
||||
"""Barometric pressure setting
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: pressure in millibar
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[26] == '0':
|
||||
if d[26] == "0":
|
||||
return None
|
||||
|
||||
p = bin2int(d[27:39]) * 0.1 + 800 # millibar
|
||||
p = common.bin2int(d[27:39]) * 0.1 + 800 # millibar
|
||||
return p
|
||||
|
||||
|
||||
def alt40mcp(msg: str) -> Optional[int]:
|
||||
warnings.warn(
|
||||
"""alt40mcp() has been renamed to selalt40mcp().
|
||||
It will be removed in the future.""",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return selalt40mcp(msg)
|
||||
|
||||
|
||||
def alt40fms(msg: str) -> Optional[int]:
|
||||
warnings.warn(
|
||||
"""alt40fms() has been renamed to selalt40fms().
|
||||
It will be removed in the future.""",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return selalt40fms(msg)
|
||||
|
||||
@@ -1,218 +1,163 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,4
|
||||
# Meteorological routine air report
|
||||
# ------------------------------------------
|
||||
|
||||
def is44(msg, rev=False):
|
||||
"""Check if a message is likely to be BDS code 4,4
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is44(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 4,4.
|
||||
|
||||
Meteorological routine air report
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
rev (bool): using revised version
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
|
||||
if not rev:
|
||||
# status bit 5, 35, 47, 50
|
||||
if wrongstatus(d, 5, 6, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 35, 36, 46):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 47, 48, 49):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 50, 51, 56):
|
||||
return False
|
||||
|
||||
# Bits 1-4 indicate source, values > 4 reserved and should not occur
|
||||
if bin2int(d[0:4]) > 4:
|
||||
return False
|
||||
else:
|
||||
# status bit 5, 15, 24, 36, 49
|
||||
if wrongstatus(d, 5, 6, 14):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 15, 16, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 35):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 36, 37, 47):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 49, 50, 56):
|
||||
return False
|
||||
|
||||
# Bits 1-4 are reserved and should be zero
|
||||
if bin2int(d[0:4]) != 0:
|
||||
return False
|
||||
|
||||
vw = wind44(msg, rev=rev)
|
||||
if vw is not None and vw[0] > 250:
|
||||
# status bit 5, 35, 47, 50
|
||||
if common.wrongstatus(d, 5, 6, 23):
|
||||
return False
|
||||
|
||||
if temp44(msg):
|
||||
if temp44(msg) > 60 or temp44(msg) < -80:
|
||||
return False
|
||||
if common.wrongstatus(d, 35, 36, 46):
|
||||
return False
|
||||
|
||||
elif temp44(msg) == 0:
|
||||
if common.wrongstatus(d, 47, 48, 49):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 50, 51, 56):
|
||||
return False
|
||||
|
||||
# Bits 1-4 indicate source, values > 4 reserved and should not occur
|
||||
if common.bin2int(d[0:4]) > 4:
|
||||
return False
|
||||
|
||||
vw, dw = wind44(msg)
|
||||
if vw is not None and vw > 250:
|
||||
return False
|
||||
|
||||
temp, temp2 = temp44(msg)
|
||||
if min(temp, temp2) > 60 or max(temp, temp2) < -80:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def wind44(msg, rev=False):
|
||||
"""reported wind speed and direction
|
||||
def wind44(msg: str) -> Tuple[Optional[int], Optional[float]]:
|
||||
"""Wind speed and direction.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS44) string
|
||||
rev (bool): using revised version
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
(int, float): speed (kt), direction (degree)
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if not rev:
|
||||
status = int(d[4])
|
||||
if not status:
|
||||
return None
|
||||
status = int(d[4])
|
||||
if not status:
|
||||
return None, None
|
||||
|
||||
speed = bin2int(d[5:14]) # knots
|
||||
direction = bin2int(d[14:23]) * 180.0 / 256.0 # degree
|
||||
|
||||
else:
|
||||
spd_status = int(d[4])
|
||||
dir_status = int(d[14])
|
||||
|
||||
if (not spd_status) or (not dir_status):
|
||||
return None
|
||||
|
||||
speed = bin2int(d[5:14]) # knots
|
||||
direction = bin2int(d[15:23]) * 180.0 / 128.0 # degree
|
||||
speed = common.bin2int(d[5:14]) # knots
|
||||
direction = common.bin2int(d[14:23]) * 180 / 256 # degree
|
||||
|
||||
return round(speed, 0), round(direction, 1)
|
||||
|
||||
|
||||
def temp44(msg, rev=False):
|
||||
"""reported air temperature
|
||||
def temp44(msg: str) -> Tuple[float, float]:
|
||||
"""Static air temperature.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS44) string
|
||||
rev (bool): using revised version
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: tmeperature in Celsius degree
|
||||
float, float: temperature and alternative temperature in Celsius degree.
|
||||
Note: Two values returns due to what seems to be an inconsistency
|
||||
error in ICAO 9871 (2008) Appendix A-67.
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if not rev:
|
||||
# if d[22] == '0':
|
||||
# return None
|
||||
sign = int(d[23])
|
||||
value = common.bin2int(d[24:34])
|
||||
|
||||
sign = int(d[23])
|
||||
value = bin2int(d[24:34])
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
temp = value * 0.25 # celsius
|
||||
temp = round(temp, 2)
|
||||
|
||||
temp = value * 0.125 # celsius
|
||||
temp = round(temp, 1)
|
||||
else:
|
||||
# if d[23] == '0':
|
||||
# return None
|
||||
temp_alternative = value * 0.125 # celsius
|
||||
temp_alternative = round(temp_alternative, 3)
|
||||
|
||||
sign = int(d[24])
|
||||
value = bin2int(d[25:35])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
temp = value * 0.125 # celsius
|
||||
temp = round(temp, 1)
|
||||
|
||||
return temp
|
||||
return temp, temp_alternative
|
||||
|
||||
|
||||
def p44(msg, rev=False):
|
||||
"""reported average static pressure
|
||||
def p44(msg: str) -> Optional[int]:
|
||||
"""Static pressure.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS44) string
|
||||
rev (bool): using revised version
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: static pressure in hPa
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if not rev:
|
||||
if d[34] == '0':
|
||||
return None
|
||||
if d[34] == "0":
|
||||
return None
|
||||
|
||||
p = bin2int(d[35:46]) # hPa
|
||||
|
||||
else:
|
||||
if d[35] == '0':
|
||||
return None
|
||||
|
||||
p = bin2int(d[36:47]) # hPa
|
||||
p = common.bin2int(d[35:46]) # hPa
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def hum44(msg, rev=False):
|
||||
"""reported humidity
|
||||
def hum44(msg: str) -> Optional[float]:
|
||||
"""humidity
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS44) string
|
||||
rev (bool): using revised version
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: percentage of humidity, [0 - 100] %
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if not rev:
|
||||
if d[49] == '0':
|
||||
return None
|
||||
if d[49] == "0":
|
||||
return None
|
||||
|
||||
hm = bin2int(d[50:56]) * 100.0 / 64 # %
|
||||
|
||||
else:
|
||||
if d[48] == '0':
|
||||
return None
|
||||
|
||||
hm = bin2int(d[49:56]) # %
|
||||
hm = common.bin2int(d[50:56]) * 100 / 64 # %
|
||||
|
||||
return round(hm, 1)
|
||||
|
||||
|
||||
def turb44(msg: str) -> Optional[int]:
|
||||
"""Turbulence.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[46] == "0":
|
||||
return None
|
||||
|
||||
turb = common.bin2int(d[47:49])
|
||||
|
||||
return turb
|
||||
|
||||
210
pyModeS/decoder/bds/bds45.py
Normal file
210
pyModeS/decoder/bds/bds45.py
Normal file
@@ -0,0 +1,210 @@
|
||||
# ------------------------------------------
|
||||
# BDS 4,5
|
||||
# Meteorological hazard report
|
||||
# ------------------------------------------
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is45(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 4,5.
|
||||
|
||||
Meteorological hazard report
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
|
||||
"""
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
# status bit 1, 4, 7, 10, 13, 16, 27, 39
|
||||
if common.wrongstatus(d, 1, 2, 3):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 4, 5, 6):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 7, 8, 9):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 10, 11, 12):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 13, 14, 15):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 16, 17, 26):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 27, 28, 38):
|
||||
return False
|
||||
|
||||
if common.wrongstatus(d, 39, 40, 51):
|
||||
return False
|
||||
|
||||
# reserved
|
||||
if common.bin2int(d[51:56]) != 0:
|
||||
return False
|
||||
|
||||
temp = temp45(msg)
|
||||
if temp:
|
||||
if temp > 60 or temp < -80:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def turb45(msg: str) -> Optional[int]:
|
||||
"""Turbulence.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
turb = common.bin2int(d[1:3])
|
||||
return turb
|
||||
|
||||
|
||||
def ws45(msg: str) -> Optional[int]:
|
||||
"""Wind shear.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Wind shear level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[3] == "0":
|
||||
return None
|
||||
|
||||
ws = common.bin2int(d[4:6])
|
||||
return ws
|
||||
|
||||
|
||||
def mb45(msg: str) -> Optional[int]:
|
||||
"""Microburst.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Microburst level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[6] == "0":
|
||||
return None
|
||||
|
||||
mb = common.bin2int(d[7:9])
|
||||
return mb
|
||||
|
||||
|
||||
def ic45(msg: str) -> Optional[int]:
|
||||
"""Icing.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Icing level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[9] == "0":
|
||||
return None
|
||||
|
||||
ic = common.bin2int(d[10:12])
|
||||
return ic
|
||||
|
||||
|
||||
def wv45(msg: str) -> Optional[int]:
|
||||
"""Wake vortex.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Wake vortex level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[12] == "0":
|
||||
return None
|
||||
|
||||
ws = common.bin2int(d[13:15])
|
||||
return ws
|
||||
|
||||
|
||||
def temp45(msg: str) -> Optional[float]:
|
||||
"""Static air temperature.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: tmeperature in Celsius degree
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
sign = int(d[16])
|
||||
value = common.bin2int(d[17:26])
|
||||
|
||||
if sign:
|
||||
value = value - 512
|
||||
|
||||
temp = value * 0.25 # celsius
|
||||
temp = round(temp, 1)
|
||||
|
||||
return temp
|
||||
|
||||
|
||||
def p45(msg: str) -> Optional[int]:
|
||||
"""Average static pressure.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: static pressure in hPa
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[26] == "0":
|
||||
return None
|
||||
p = common.bin2int(d[27:38]) # hPa
|
||||
return p
|
||||
|
||||
|
||||
def rh45(msg: str) -> Optional[int]:
|
||||
"""Radio height.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: radio height in ft
|
||||
|
||||
"""
|
||||
d = common.hex2bin(common.data(msg))
|
||||
if d[38] == "0":
|
||||
return None
|
||||
rh = common.bin2int(d[39:51]) * 16
|
||||
return rh
|
||||
@@ -1,61 +1,48 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 5,0
|
||||
# Track and turn report
|
||||
# ------------------------------------------
|
||||
|
||||
def is50(msg):
|
||||
from typing import Optional
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is50(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 5,0
|
||||
(Track and turn report)
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
# status bit 1, 12, 24, 35, 46
|
||||
|
||||
if wrongstatus(d, 1, 3, 11):
|
||||
if common.wrongstatus(d, 1, 3, 11):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 12, 13, 23):
|
||||
if common.wrongstatus(d, 12, 13, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 34):
|
||||
if common.wrongstatus(d, 24, 25, 34):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 35, 36, 45):
|
||||
if common.wrongstatus(d, 35, 36, 45):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 46, 47, 56):
|
||||
if common.wrongstatus(d, 46, 47, 56):
|
||||
return False
|
||||
|
||||
roll = roll50(msg)
|
||||
if (roll is not None) and abs(roll) > 60:
|
||||
if (roll is not None) and abs(roll) > 50:
|
||||
return False
|
||||
|
||||
gs = gs50(msg)
|
||||
@@ -72,52 +59,52 @@ def is50(msg):
|
||||
return True
|
||||
|
||||
|
||||
def roll50(msg):
|
||||
def roll50(msg: str) -> Optional[float]:
|
||||
"""Roll angle, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: angle in degrees,
|
||||
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
|
||||
|
||||
sign = int(d[1]) # 1 -> left wing down
|
||||
value = bin2int(d[2:11])
|
||||
sign = int(d[1]) # 1 -> left wing down
|
||||
value = common.bin2int(d[2:11])
|
||||
|
||||
if sign:
|
||||
value = value - 512
|
||||
|
||||
angle = value * 45.0 / 256.0 # degree
|
||||
angle = value * 45 / 256 # degree
|
||||
return round(angle, 1)
|
||||
|
||||
|
||||
def trk50(msg):
|
||||
def trk50(msg: str) -> Optional[float]:
|
||||
"""True track angle, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
sign = int(d[12]) # 1 -> west
|
||||
value = bin2int(d[13:23])
|
||||
sign = int(d[12]) # 1 -> west
|
||||
value = common.bin2int(d[13:23])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
trk = value * 90.0 / 512.0
|
||||
trk = value * 90 / 512.0
|
||||
|
||||
# convert from [-180, 180] to [0, 360]
|
||||
if trk < 0:
|
||||
@@ -126,63 +113,63 @@ def trk50(msg):
|
||||
return round(trk, 3)
|
||||
|
||||
|
||||
def gs50(msg):
|
||||
def gs50(msg: str) -> Optional[float]:
|
||||
"""Ground speed, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
spd = bin2int(d[24:34]) * 2 # kts
|
||||
spd = common.bin2int(d[24:34]) * 2 # kts
|
||||
return spd
|
||||
|
||||
|
||||
def rtrk50(msg):
|
||||
def rtrk50(msg: str) -> Optional[float]:
|
||||
"""Track angle rate, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
if d[36:45] == "111111111":
|
||||
return None
|
||||
|
||||
sign = int(d[35]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[36:45])
|
||||
sign = int(d[35]) # 1 -> negative value, two's complement
|
||||
value = common.bin2int(d[36:45])
|
||||
if sign:
|
||||
value = value - 512
|
||||
|
||||
angle = value * 8.0 / 256.0 # degree / sec
|
||||
angle = value * 8 / 256 # degree / sec
|
||||
return round(angle, 3)
|
||||
|
||||
|
||||
def tas50(msg):
|
||||
def tas50(msg: str) -> Optional[float]:
|
||||
"""Aircraft true airspeed, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
tas = bin2int(d[46:56]) * 2 # kts
|
||||
tas = common.bin2int(d[46:56]) * 2 # kts
|
||||
return tas
|
||||
|
||||
@@ -1,57 +1,44 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 5,3
|
||||
# Air-referenced state vector
|
||||
# ------------------------------------------
|
||||
|
||||
def is53(msg):
|
||||
from typing import Optional
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is53(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 5,3
|
||||
(Air-referenced state vector)
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
# status bit 1, 13, 24, 34, 47
|
||||
|
||||
if wrongstatus(d, 1, 3, 12):
|
||||
if common.wrongstatus(d, 1, 3, 12):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 13, 14, 23):
|
||||
if common.wrongstatus(d, 13, 14, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 33):
|
||||
if common.wrongstatus(d, 24, 25, 33):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 34, 35, 46):
|
||||
if common.wrongstatus(d, 34, 35, 46):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 47, 49, 56):
|
||||
if common.wrongstatus(d, 47, 49, 56):
|
||||
return False
|
||||
|
||||
ias = ias53(msg)
|
||||
@@ -73,27 +60,27 @@ def is53(msg):
|
||||
return True
|
||||
|
||||
|
||||
def hdg53(msg):
|
||||
def hdg53(msg: str) -> Optional[float]:
|
||||
"""Magnetic heading, BDS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS53) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = bin2int(d[2:12])
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = common.bin2int(d[2:12])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
hdg = value * 90.0 / 512.0 # degree
|
||||
hdg = value * 90 / 512 # degree
|
||||
|
||||
# convert from [-180, 180] to [0, 360]
|
||||
if hdg < 0:
|
||||
@@ -102,80 +89,81 @@ def hdg53(msg):
|
||||
return round(hdg, 3)
|
||||
|
||||
|
||||
def ias53(msg):
|
||||
def ias53(msg: str) -> Optional[float]:
|
||||
"""Indicated airspeed, DBS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message
|
||||
msg (str): 28 hexdigits
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
ias = bin2int(d[13:23]) # knots
|
||||
ias = common.bin2int(d[13:23]) # knots
|
||||
return ias
|
||||
|
||||
|
||||
def mach53(msg):
|
||||
def mach53(msg: str) -> Optional[float]:
|
||||
"""MACH number, DBS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message
|
||||
msg (str): 28 hexdigits
|
||||
|
||||
Returns:
|
||||
float: MACH number
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[23] == '0':
|
||||
if d[23] == "0":
|
||||
return None
|
||||
|
||||
mach = bin2int(d[24:33]) * 0.008
|
||||
mach = common.bin2int(d[24:33]) * 0.008
|
||||
return round(mach, 3)
|
||||
|
||||
|
||||
def tas53(msg):
|
||||
def tas53(msg: str) -> Optional[float]:
|
||||
"""Aircraft true airspeed, BDS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message
|
||||
msg (str): 28 hexdigits
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
tas = bin2int(d[34:46]) * 0.5 # kts
|
||||
tas = common.bin2int(d[34:46]) * 0.5 # kts
|
||||
return round(tas, 1)
|
||||
|
||||
def vr53(msg):
|
||||
|
||||
def vr53(msg: str) -> Optional[int]:
|
||||
"""Vertical rate
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
msg (str): 28 hexdigits (BDS60) string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
sign = int(d[47]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[48:56])
|
||||
sign = int(d[47]) # 1 -> negative value, two's complement
|
||||
value = common.bin2int(d[48:56])
|
||||
|
||||
if value == 0 or value == 255: # all zeros or all ones
|
||||
return 0
|
||||
|
||||
value = value - 256 if sign else value
|
||||
roc = value * 64 # feet/min
|
||||
roc = value * 64 # feet/min
|
||||
|
||||
return roc
|
||||
|
||||
@@ -1,56 +1,44 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 6,0
|
||||
# Heading and speed report
|
||||
# ------------------------------------------
|
||||
|
||||
def is60(msg):
|
||||
from typing import Optional
|
||||
|
||||
from ... import common
|
||||
from ...extra import aero
|
||||
|
||||
|
||||
def is60(msg: str) -> bool:
|
||||
"""Check if a message is likely to be BDS code 6,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
if common.allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
# status bit 1, 13, 24, 35, 46
|
||||
|
||||
if wrongstatus(d, 1, 2, 12):
|
||||
if common.wrongstatus(d, 1, 2, 12):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 13, 14, 23):
|
||||
if common.wrongstatus(d, 13, 14, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 34):
|
||||
if common.wrongstatus(d, 24, 25, 34):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 35, 36, 45):
|
||||
if common.wrongstatus(d, 35, 36, 45):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 46, 47, 56):
|
||||
if common.wrongstatus(d, 46, 47, 56):
|
||||
return False
|
||||
|
||||
ias = ias60(msg)
|
||||
@@ -69,121 +57,129 @@ def is60(msg):
|
||||
if vr_ins is not None and abs(vr_ins) > 6000:
|
||||
return False
|
||||
|
||||
# additional check knowing altitude
|
||||
if (mach is not None) and (ias is not None) and (common.df(msg) == 20):
|
||||
alt = common.altcode(msg)
|
||||
if alt is not None:
|
||||
ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts
|
||||
if abs(ias - ias_) > 20:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def hdg60(msg):
|
||||
def hdg60(msg: str) -> Optional[float]:
|
||||
"""Megnetic heading of aircraft
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = bin2int(d[2:12])
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = common.bin2int(d[2:12])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
hdg = value * 90 / 512.0 # degree
|
||||
hdg = value * 90 / 512 # degree
|
||||
|
||||
# convert from [-180, 180] to [0, 360]
|
||||
if hdg < 0:
|
||||
hdg = 360 + hdg
|
||||
|
||||
return round(hdg, 3)
|
||||
return hdg
|
||||
|
||||
|
||||
def ias60(msg):
|
||||
def ias60(msg: str) -> Optional[float]:
|
||||
"""Indicated airspeed
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
ias = bin2int(d[13:23]) # kts
|
||||
ias = common.bin2int(d[13:23]) # kts
|
||||
return ias
|
||||
|
||||
|
||||
def mach60(msg):
|
||||
def mach60(msg: str) -> Optional[float]:
|
||||
"""Aircraft MACH number
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: MACH number
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
d = common.hex2bin(common.data(msg))
|
||||
|
||||
if d[23] == '0':
|
||||
if d[23] == "0":
|
||||
return None
|
||||
|
||||
mach = bin2int(d[24:34]) * 2.048 / 512.0
|
||||
return round(mach, 3)
|
||||
mach = common.bin2int(d[24:34]) * 2.048 / 512.0
|
||||
return mach
|
||||
|
||||
|
||||
def vr60baro(msg):
|
||||
def vr60baro(msg: str) -> Optional[int]:
|
||||
"""Vertical rate from barometric measurement, this value may be very noisy.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
sign = int(d[35]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[36:45])
|
||||
sign = int(d[35]) # 1 -> negative value, two's complement
|
||||
value = common.bin2int(d[36:45])
|
||||
|
||||
if value == 0 or value == 511: # all zeros or all ones
|
||||
return 0
|
||||
|
||||
value = value - 512 if sign else value
|
||||
|
||||
roc = value * 32 # feet/min
|
||||
roc = value * 32 # feet/min
|
||||
return roc
|
||||
|
||||
|
||||
def vr60ins(msg):
|
||||
"""Vertical rate messured by onbard equiments (IRS, AHRS)
|
||||
def vr60ins(msg: str) -> Optional[int]:
|
||||
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
sign = int(d[46]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[47:56])
|
||||
sign = int(d[46]) # 1 -> negative value, two's complement
|
||||
value = common.bin2int(d[47:56])
|
||||
|
||||
if value == 0 or value == 511: # all zeros or all ones
|
||||
return 0
|
||||
|
||||
value = value - 512 if sign else value
|
||||
|
||||
roc = value * 32 # feet/min
|
||||
roc = value * 32 # feet/min
|
||||
return roc
|
||||
|
||||
88
pyModeS/decoder/bds/bds61_st1.py
Normal file
88
pyModeS/decoder/bds/bds61_st1.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# ------------------------------------------
|
||||
# BDS 6,1
|
||||
# ADS-B TC=28
|
||||
# Aircraft Airborne status
|
||||
# (Subtype 1)
|
||||
# ------------------------------------------
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def is_emergency(msg: str) -> bool:
|
||||
"""Check if the aircraft is reporting an emergency.
|
||||
|
||||
Non-emergencies are either a subtype of zero (no information) or
|
||||
subtype of one and a value of zero (no emergency).
|
||||
Subtype = 2 indicates an ACAS RA broadcast, look in BDS 3,0
|
||||
|
||||
:param msg: 28 bytes hexadecimal message string
|
||||
:return: if the aircraft has declared an emergency
|
||||
"""
|
||||
if common.typecode(msg) != 28:
|
||||
raise RuntimeError(
|
||||
"%s: Not an airborne status message, expecting TC=28" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
subtype = common.bin2int(mb[5:8])
|
||||
|
||||
if subtype == 2:
|
||||
raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented")
|
||||
|
||||
emergency_state = common.bin2int(mb[8:11])
|
||||
|
||||
if subtype == 1 and emergency_state == 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def emergency_state(msg: str) -> int:
|
||||
"""Decode aircraft emergency state.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
0 No emergency
|
||||
1 General emergency
|
||||
2 Lifeguard/Medical
|
||||
3 Minimum fuel
|
||||
4 No communications
|
||||
5 Unlawful communications
|
||||
6-7 Reserved
|
||||
|
||||
:param msg: 28 bytes hexadecimal message string
|
||||
:return: emergency state
|
||||
"""
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
subtype = common.bin2int(mb[5:8])
|
||||
|
||||
if subtype == 2:
|
||||
raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented")
|
||||
|
||||
emergency_state = common.bin2int(mb[8:11])
|
||||
return emergency_state
|
||||
|
||||
|
||||
def emergency_squawk(msg: str) -> str:
|
||||
"""Decode squawk code.
|
||||
|
||||
Emergency value 1: squawk 7700.
|
||||
Emergency value 4: squawk 7600.
|
||||
Emergency value 5: squawk 7500.
|
||||
|
||||
:param msg: 28 bytes hexadecimal message string
|
||||
:return: aircraft squawk code
|
||||
"""
|
||||
if common.typecode(msg) != 28:
|
||||
raise RuntimeError(
|
||||
"%s: Not an airborne status message, expecting TC=28" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
# construct the 13 bits Mode A ID code
|
||||
idcode = msgbin[43:56]
|
||||
|
||||
squawk = common.squawk(idcode)
|
||||
return squawk
|
||||
102
pyModeS/decoder/bds/bds61_st2.py
Normal file
102
pyModeS/decoder/bds/bds61_st2.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# ------------------------------------------
|
||||
# BDS 6,1
|
||||
# ADS-B TC=28
|
||||
# Aircraft Airborne status
|
||||
# (Subtype 1)
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import common
|
||||
from .. import acas
|
||||
|
||||
|
||||
def threat_type(msg: str) -> int:
|
||||
"""Determine the threat type indicator.
|
||||
|
||||
Value Meaning
|
||||
----- ---------------------------------------
|
||||
0 No identity data in TID
|
||||
1 TID has Mode S address (ICAO)
|
||||
2 TID has altitude, range, and bearing
|
||||
3 Not assigned
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: indicator of threat type
|
||||
"""
|
||||
mb = common.hex2bin(common.data(msg))
|
||||
tti = common.bin2int(mb[28:30])
|
||||
return tti
|
||||
|
||||
|
||||
def threat_identity(msg: str) -> str:
|
||||
mb = common.hex2bin(common.data(msg))
|
||||
tti = threat_type(msg)
|
||||
|
||||
# The ICAO of the threat is announced
|
||||
if tti == 1:
|
||||
return common.icao(mb[30:55])
|
||||
else:
|
||||
raise RuntimeError("%s: Missing threat identity (ICAO)")
|
||||
|
||||
|
||||
def threat_location(msg: str) -> None | tuple[int, int, int]:
|
||||
"""Get the altitude, range, and bearing of the threat.
|
||||
|
||||
Altitude is the Mode C altitude
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: tuple of the Mode C altitude, range, and bearing
|
||||
"""
|
||||
mb = common.hex2bin(common.data(msg))
|
||||
tti = threat_type(msg)
|
||||
|
||||
# Altitude, range, and bearing of threat
|
||||
if tti == 2:
|
||||
altitude = common.altitude(mb[31:44])
|
||||
distance = common.bin2int(mb[44:51])
|
||||
bearing = common.bin2int(mb[51:57])
|
||||
return altitude, distance, bearing
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def has_multiple_threats(msg: str) -> bool:
|
||||
"""Indicate if the ACAS is processing multiple threats simultaneously.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: if there are multiple threats
|
||||
"""
|
||||
return acas.mte(msg) == 1
|
||||
|
||||
|
||||
def active_resolution_advisories(msg: str) -> None | str:
|
||||
"""Decode active resolution advisory.
|
||||
|
||||
Uses ARA decoding function from ACAS module.
|
||||
|
||||
:param msg: 28 bytes hexadecimal message string
|
||||
:return: RA charactristics
|
||||
"""
|
||||
return acas.ara(msg)
|
||||
|
||||
|
||||
def is_ra_terminated(msg: str) -> bool:
|
||||
"""Indicate if the threat is still being generated.
|
||||
|
||||
Mode S transponder is still required to report RA 18 seconds after
|
||||
it is terminated by ACAS. Hence, the RAT filed is used.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: if the threat is terminated
|
||||
"""
|
||||
return acas.rat(msg) == 1
|
||||
|
||||
|
||||
def ra_complement(msg: str) -> None | str:
|
||||
"""Resolution Advisory Complement.
|
||||
|
||||
:param msg: 28 hexdigits string
|
||||
:return: RACs
|
||||
"""
|
||||
return acas.rac(msg)
|
||||
553
pyModeS/decoder/bds/bds62.py
Normal file
553
pyModeS/decoder/bds/bds62.py
Normal file
@@ -0,0 +1,553 @@
|
||||
# ------------------------------------------
|
||||
# BDS 6,2
|
||||
# ADS-B TC=29
|
||||
# Target State and Status
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import common
|
||||
|
||||
|
||||
def selected_altitude(msg: str) -> tuple[None | float, str]:
|
||||
"""Decode selected altitude.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Selected altitude (ft)
|
||||
string: Source ('MCP/FCU' or 'FMS')
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not"
|
||||
" contain selected altitude, use target altitude instead" % msg
|
||||
)
|
||||
|
||||
alt = common.bin2int(mb[9:20])
|
||||
if alt == 0:
|
||||
return None, "N/A"
|
||||
alt = (alt - 1) * 32
|
||||
alt_source = "MCP/FCU" if int(mb[8]) == 0 else "FMS"
|
||||
|
||||
return alt, alt_source
|
||||
|
||||
|
||||
def target_altitude(msg: str) -> tuple[None | int, str, str]:
|
||||
"""Decode target altitude.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Target altitude (ft)
|
||||
string: Source ('MCP/FCU', 'Holding mode' or 'FMS/RNAV')
|
||||
string: Altitude reference, either pressure altitude or barometric
|
||||
corrected altitude ('FL' or 'MSL')
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 2 target state and status message does not"
|
||||
" contain target altitude, use selected altitude instead" % msg
|
||||
)
|
||||
|
||||
alt_avail = common.bin2int(mb[7:9])
|
||||
if alt_avail == 0:
|
||||
return None, "N/A", ""
|
||||
elif alt_avail == 1:
|
||||
alt_source = "MCP/FCU"
|
||||
elif alt_avail == 2:
|
||||
alt_source = "Holding mode"
|
||||
else:
|
||||
alt_source = "FMS/RNAV"
|
||||
|
||||
alt_ref = "FL" if int(mb[9]) == 0 else "MSL"
|
||||
|
||||
alt = -1000 + common.bin2int(mb[15:25]) * 100
|
||||
|
||||
return alt, alt_source, alt_ref
|
||||
|
||||
|
||||
def vertical_mode(msg: str) -> None | int:
|
||||
"""Decode vertical mode.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
1 "Acquiring" mode
|
||||
2 "Capturing" or "Maintaining" mode
|
||||
3 Reserved
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Vertical mode
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 2 target state and status message does not"
|
||||
" contain vertical mode, use vnav mode instead" % msg
|
||||
)
|
||||
|
||||
vertical_mode = common.bin2int(mb[13:15])
|
||||
if vertical_mode == 0:
|
||||
return None
|
||||
|
||||
return vertical_mode
|
||||
|
||||
|
||||
def horizontal_mode(msg: str) -> None | int:
|
||||
"""Decode horizontal mode.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
1 "Acquiring" mode
|
||||
2 "Capturing" or "Maintaining" mode
|
||||
3 Reserved
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: Horizontal mode
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 2 target state and status message does not "
|
||||
"contain horizontal mode, use lnav mode instead" % msg
|
||||
)
|
||||
|
||||
horizontal_mode = common.bin2int(mb[25:27])
|
||||
if horizontal_mode == 0:
|
||||
return None
|
||||
|
||||
return horizontal_mode
|
||||
|
||||
|
||||
def selected_heading(msg: str) -> None | float:
|
||||
"""Decode selected heading.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
float: Selected heading (degree)
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain selected heading, use target angle instead" % msg
|
||||
)
|
||||
|
||||
if int(mb[29]) == 0:
|
||||
return None
|
||||
else:
|
||||
hdg_sign = int(mb[30])
|
||||
hdg = (hdg_sign + 1) * common.bin2int(mb[31:39]) * (180 / 256)
|
||||
hdg = round(hdg, 2)
|
||||
|
||||
return hdg
|
||||
|
||||
|
||||
def target_angle(msg: str) -> tuple[None | int, str, str]:
|
||||
"""Decode target heading/track angle.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Target angle (degree)
|
||||
string: Angle type ('Heading' or 'Track')
|
||||
string: Source ('MCP/FCU', 'Autopilot Mode' or 'FMS/RNAV')
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 2 target state and status message does not "
|
||||
"contain target angle, use selected heading instead" % msg
|
||||
)
|
||||
|
||||
angle_avail = common.bin2int(mb[25:27])
|
||||
if angle_avail == 0:
|
||||
return None, "", "N/A"
|
||||
else:
|
||||
angle = common.bin2int(mb[27:36])
|
||||
|
||||
if angle_avail == 1:
|
||||
angle_source = "MCP/FCU"
|
||||
elif angle_avail == 2:
|
||||
angle_source = "Autopilot mode"
|
||||
else:
|
||||
angle_source = "FMS/RNAV"
|
||||
|
||||
angle_type = "Heading" if int(mb[36]) else "Track"
|
||||
|
||||
return angle, angle_type, angle_source
|
||||
|
||||
|
||||
def baro_pressure_setting(msg: str) -> None | float:
|
||||
"""Decode barometric pressure setting.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
float: Barometric pressure setting (millibars)
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain barometric pressure setting" % msg
|
||||
)
|
||||
|
||||
baro = common.bin2int(mb[20:29])
|
||||
if baro == 0:
|
||||
return None
|
||||
|
||||
return 800 + (baro - 1) * 0.8
|
||||
|
||||
|
||||
def autopilot(msg) -> None | bool:
|
||||
"""Decode autopilot engagement.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: Autopilot engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain autopilot engagement" % msg
|
||||
)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
autopilot = True if int(mb[47]) == 1 else False
|
||||
|
||||
return autopilot
|
||||
|
||||
|
||||
def vnav_mode(msg) -> None | bool:
|
||||
"""Decode VNAV mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: VNAV mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain vnav mode, use vertical mode instead" % msg
|
||||
)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
vnav_mode = True if int(mb[48]) == 1 else False
|
||||
|
||||
return vnav_mode
|
||||
|
||||
|
||||
def altitude_hold_mode(msg) -> None | bool:
|
||||
"""Decode altitude hold mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: Altitude hold mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain altitude hold mode" % msg
|
||||
)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
alt_hold_mode = True if int(mb[49]) == 1 else False
|
||||
|
||||
return alt_hold_mode
|
||||
|
||||
|
||||
def approach_mode(msg) -> None | bool:
|
||||
"""Decode approach mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: Approach mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain approach mode" % msg
|
||||
)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
app_mode = True if int(mb[51]) == 1 else False
|
||||
|
||||
return app_mode
|
||||
|
||||
|
||||
def lnav_mode(msg) -> None | bool:
|
||||
"""Decode LNAV mode.
|
||||
|
||||
Args:
|
||||
msg (str): 28 hexdigits string
|
||||
|
||||
Returns:
|
||||
bool: LNAV mode engaged
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 1 target state and status message does not "
|
||||
"contain lnav mode, use horizontal mode instead" % msg
|
||||
)
|
||||
|
||||
if int(mb[46]) == 0:
|
||||
return None
|
||||
|
||||
lnav_mode = True if int(mb[53]) == 1 else False
|
||||
|
||||
return lnav_mode
|
||||
|
||||
|
||||
def tcas_operational(msg) -> None | bool:
|
||||
"""Decode TCAS/ACAS operational.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: TCAS/ACAS operational
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 0:
|
||||
tcas = True if int(mb[51]) == 0 else False
|
||||
else:
|
||||
tcas = True if int(mb[52]) == 1 else False
|
||||
|
||||
return tcas
|
||||
|
||||
|
||||
def tcas_ra(msg) -> bool:
|
||||
"""Decode TCAS/ACAS Resolution advisory.
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: TCAS/ACAS Resolution advisory active
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 2 target state and status message does not "
|
||||
"contain TCAS/ACAS RA" % msg
|
||||
)
|
||||
|
||||
tcas_ra = True if int(mb[52]) == 1 else False
|
||||
|
||||
return tcas_ra
|
||||
|
||||
|
||||
def emergency_status(msg) -> int:
|
||||
"""Decode aircraft emergency status.
|
||||
|
||||
Value Meaning
|
||||
----- -----------------------
|
||||
0 No emergency
|
||||
1 General emergency
|
||||
2 Lifeguard/medical emergency
|
||||
3 Minimum fuel
|
||||
4 No communications
|
||||
5 Unlawful interference
|
||||
6 Downed aircraft
|
||||
7 Reserved
|
||||
|
||||
Args:
|
||||
msg (str): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Emergency status
|
||||
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 29:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, expecting TC=29" % msg
|
||||
)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:7])
|
||||
|
||||
if subtype == 1:
|
||||
raise RuntimeError(
|
||||
"%s: ADS-B version 2 target state and status message does not "
|
||||
"contain emergency status" % msg
|
||||
)
|
||||
|
||||
return common.bin2int(mb[53:56])
|
||||
@@ -1,15 +1,88 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
"""Comm-B module.
|
||||
|
||||
The Comm-B module imports all functions from the following modules:
|
||||
|
||||
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.bds40
|
||||
- pyModeS.decoder.bds.bds50
|
||||
- pyModeS.decoder.bds.bds60
|
||||
|
||||
MRAR and MHR
|
||||
|
||||
- pyModeS.decoder.bds.bds44
|
||||
- pyModeS.decoder.bds.bds45
|
||||
|
||||
"""
|
||||
|
||||
# ELS - elementary surveillance
|
||||
from pyModeS.decoder.bds.bds10 import *
|
||||
from pyModeS.decoder.bds.bds17 import *
|
||||
from pyModeS.decoder.bds.bds20 import *
|
||||
from pyModeS.decoder.bds.bds30 import *
|
||||
from .bds.bds10 import is10, ovc10
|
||||
from .bds.bds17 import is17, cap17
|
||||
from .bds.bds20 import is20, cs20
|
||||
from .bds.bds30 import is30
|
||||
|
||||
# ELS - enhanced surveillance
|
||||
from pyModeS.decoder.bds.bds40 import *
|
||||
from pyModeS.decoder.bds.bds50 import *
|
||||
from pyModeS.decoder.bds.bds60 import *
|
||||
from .bds.bds40 import (
|
||||
is40,
|
||||
selalt40fms,
|
||||
selalt40mcp,
|
||||
p40baro,
|
||||
alt40fms,
|
||||
alt40mcp,
|
||||
)
|
||||
from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50
|
||||
from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins
|
||||
|
||||
# MRAR
|
||||
from pyModeS.decoder.bds.bds44 import *
|
||||
# MRAR and MHR
|
||||
from .bds.bds44 import is44, wind44, temp44, p44, hum44, turb44
|
||||
from .bds.bds45 import is45, turb45, ws45, mb45, ic45, wv45, temp45, p45, rh45
|
||||
|
||||
__all__ = [
|
||||
"is10",
|
||||
"ovc10",
|
||||
"is17",
|
||||
"cap17",
|
||||
"is20",
|
||||
"cs20",
|
||||
"is30",
|
||||
"is40",
|
||||
"selalt40fms",
|
||||
"selalt40mcp",
|
||||
"p40baro",
|
||||
"alt40fms",
|
||||
"alt40mcp",
|
||||
"is50",
|
||||
"roll50",
|
||||
"trk50",
|
||||
"gs50",
|
||||
"rtrk50",
|
||||
"tas50",
|
||||
"is60",
|
||||
"hdg60",
|
||||
"ias60",
|
||||
"mach60",
|
||||
"vr60baro",
|
||||
"vr60ins",
|
||||
"is44",
|
||||
"wind44",
|
||||
"temp44",
|
||||
"p44",
|
||||
"hum44",
|
||||
"turb44",
|
||||
"is45",
|
||||
"turb45",
|
||||
"ws45",
|
||||
"mb45",
|
||||
"ic45",
|
||||
"wv45",
|
||||
"temp45",
|
||||
"p45",
|
||||
"rh45",
|
||||
]
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import numpy as np
|
||||
|
||||
def hex2bin(hexstr):
|
||||
"""Convert a hexdecimal string to binary string, with zero fillings. """
|
||||
num_of_bits = len(hexstr) * 4
|
||||
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
|
||||
return binstr
|
||||
|
||||
|
||||
def bin2int(binstr):
|
||||
"""Convert a binary string to integer. """
|
||||
return int(binstr, 2)
|
||||
|
||||
|
||||
def hex2int(hexstr):
|
||||
"""Convert a hexdecimal string to integer. """
|
||||
return int(hexstr, 16)
|
||||
|
||||
|
||||
def bin2np(binstr):
|
||||
"""Convert a binary string to numpy array. """
|
||||
return np.array([int(i) for i in binstr])
|
||||
|
||||
|
||||
def np2bin(npbin):
|
||||
"""Convert a binary numpy array to string. """
|
||||
return np.array2string(npbin, separator='')[1:-1]
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Decode Downlink Format vaule, bits 1 to 5."""
|
||||
msgbin = hex2bin(msg)
|
||||
return min( bin2int(msgbin[0:5]) , 24 )
|
||||
|
||||
|
||||
def crc(msg, encode=False):
|
||||
"""Mode-S Cyclic Redundancy Check
|
||||
Detect if bit error occurs in the Mode-S message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
encode (bool): True to encode the date only and return the checksum
|
||||
Returns:
|
||||
string: message checksum, or partity bits (encoder)
|
||||
"""
|
||||
|
||||
# the polynominal generattor code for CRC [1111111111111010000001001]
|
||||
generator = np.array([1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,1,0,0,1])
|
||||
ng = len(generator)
|
||||
|
||||
msgnpbin = bin2np(hex2bin(msg))
|
||||
|
||||
if encode:
|
||||
msgnpbin[-24:] = [0] * 24
|
||||
|
||||
# loop all bits, except last 24 piraty bits
|
||||
for i in range(len(msgnpbin)-24):
|
||||
if msgnpbin[i] == 0:
|
||||
continue
|
||||
|
||||
# perform XOR, when 1
|
||||
msgnpbin[i:i+ng] = np.bitwise_xor(msgnpbin[i:i+ng], generator)
|
||||
|
||||
# last 24 bits
|
||||
reminder = np2bin(msgnpbin[-24:])
|
||||
return reminder
|
||||
|
||||
|
||||
def floor(x):
|
||||
""" Mode-S floor function
|
||||
|
||||
Defined as the greatest integer value k, such that k <= x
|
||||
|
||||
eg.: floor(3.6) = 3, while floor(-3.6) = -4
|
||||
"""
|
||||
return int(np.floor(x))
|
||||
|
||||
|
||||
def icao(msg):
|
||||
"""Calculate the ICAO address from an Mode-S message
|
||||
with DF4, DF5, DF20, DF21
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
String: ICAO address in 6 bytes hexadecimal string
|
||||
"""
|
||||
|
||||
DF = df(msg)
|
||||
|
||||
if DF in (11, 17, 18):
|
||||
addr = msg[2:8]
|
||||
elif DF in (0, 4, 5, 16, 20, 21):
|
||||
c0 = bin2int(crc(msg, encode=True))
|
||||
c1 = hex2int(msg[-6:])
|
||||
addr = '%06X' % (c0 ^ c1)
|
||||
else:
|
||||
addr = None
|
||||
|
||||
return addr
|
||||
|
||||
|
||||
def is_icao_assigned(icao):
|
||||
""" Check whether the ICAO address is assigned (Annex 10, Vol 3)"""
|
||||
|
||||
if (icao is None) or (not isinstance(icao, str)) or (len(icao)!=6):
|
||||
return False
|
||||
|
||||
icaoint = hex2int(icao)
|
||||
|
||||
if 0x200000 < icaoint < 0x27FFFF: return False # AFI
|
||||
if 0x280000 < icaoint < 0x28FFFF: return False # SAM
|
||||
if 0x500000 < icaoint < 0x5FFFFF: return False # EUR, NAT
|
||||
if 0x600000 < icaoint < 0x67FFFF: return False # MID
|
||||
if 0x680000 < icaoint < 0x6F0000: return False # ASIA
|
||||
if 0x900000 < icaoint < 0x9FFFFF: return False # NAM, PAC
|
||||
if 0xB00000 < icaoint < 0xBFFFFF: return False # CAR
|
||||
if 0xD00000 < icaoint < 0xDFFFFF: return False # future
|
||||
if 0xF00000 < icaoint < 0xFFFFFF: return False # future
|
||||
|
||||
return True
|
||||
|
||||
def typecode(msg):
|
||||
"""Type code of ADS-B message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: type code number
|
||||
"""
|
||||
if df(msg) not in (17, 18):
|
||||
return None
|
||||
|
||||
msgbin = hex2bin(msg)
|
||||
return bin2int(msgbin[32:37])
|
||||
|
||||
|
||||
def cprNL(lat):
|
||||
"""NL() function in CPR decoding"""
|
||||
|
||||
if lat == 0:
|
||||
return 59
|
||||
|
||||
if lat == 87 or lat == -87:
|
||||
return 2
|
||||
|
||||
if lat > 87 or lat < -87:
|
||||
return 1
|
||||
|
||||
nz = 15
|
||||
a = 1 - np.cos(np.pi / (2 * nz))
|
||||
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
|
||||
nl = 2 * np.pi / (np.arccos(1 - a/b))
|
||||
NL = floor(nl)
|
||||
return NL
|
||||
|
||||
def idcode(msg):
|
||||
"""Computes identity (squawk code) from DF5 or DF21 message, bit 20-32.
|
||||
credit: @fbyrkjeland
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: squawk code
|
||||
"""
|
||||
|
||||
if df(msg) not in [5, 21]:
|
||||
raise RuntimeError("Message must be Downlink Format 5 or 21.")
|
||||
|
||||
mbin = hex2bin(msg)
|
||||
|
||||
C1 = mbin[19]
|
||||
A1 = mbin[20]
|
||||
C2 = mbin[21]
|
||||
A2 = mbin[22]
|
||||
C4 = mbin[23]
|
||||
A4 = mbin[24]
|
||||
# _ = mbin[25]
|
||||
B1 = mbin[26]
|
||||
D1 = mbin[27]
|
||||
B2 = mbin[28]
|
||||
D2 = mbin[29]
|
||||
B4 = mbin[30]
|
||||
D4 = mbin[31]
|
||||
|
||||
byte1 = int(A4+A2+A1, 2)
|
||||
byte2 = int(B4+B2+B1, 2)
|
||||
byte3 = int(C4+C2+C1, 2)
|
||||
byte4 = int(D4+D2+D1, 2)
|
||||
|
||||
return str(byte1) + str(byte2) + str(byte3) + str(byte4)
|
||||
|
||||
|
||||
def altcode(msg):
|
||||
"""Computes the altitude from DF4 or DF20 message, bit 20-32.
|
||||
credit: @fbyrkjeland
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
"""
|
||||
|
||||
if df(msg) not in [0, 4, 16, 20]:
|
||||
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
|
||||
|
||||
# Altitude code, bit 20-32
|
||||
mbin = hex2bin(msg)
|
||||
|
||||
mbit = mbin[25] # M bit: 26
|
||||
qbit = mbin[27] # Q bit: 28
|
||||
|
||||
|
||||
if mbit == '0': # unit in ft
|
||||
if qbit == '1': # 25ft interval
|
||||
vbin = mbin[19:25] + mbin[26] + mbin[28:32]
|
||||
alt = bin2int(vbin) * 25 - 1000
|
||||
if qbit == '0': # 100ft interval, above 50175ft
|
||||
C1 = mbin[19]
|
||||
A1 = mbin[20]
|
||||
C2 = mbin[21]
|
||||
A2 = mbin[22]
|
||||
C4 = mbin[23]
|
||||
A4 = mbin[24]
|
||||
# _ = mbin[25]
|
||||
B1 = mbin[26]
|
||||
# D1 = mbin[27] # always zero
|
||||
B2 = mbin[28]
|
||||
D2 = mbin[29]
|
||||
B4 = mbin[30]
|
||||
D4 = mbin[31]
|
||||
|
||||
graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
|
||||
alt = gray2alt(graystr)
|
||||
|
||||
if mbit == '1': # unit in meter
|
||||
vbin = mbin[19:25] + mbin[26:31]
|
||||
alt = int(bin2int(vbin) * 3.28084) # convert to ft
|
||||
|
||||
return alt
|
||||
|
||||
|
||||
def gray2alt(codestr):
|
||||
gc500 = codestr[:8]
|
||||
n500 = gray2int(gc500)
|
||||
|
||||
# in 100-ft step must be converted first
|
||||
gc100 = codestr[8:]
|
||||
n100 = gray2int(gc100)
|
||||
|
||||
if n100 in [0, 5, 6]:
|
||||
return None
|
||||
|
||||
if n100 == 7:
|
||||
n100 = 5
|
||||
|
||||
if n500%2:
|
||||
n100 = 6 - n100
|
||||
|
||||
alt = (n500*500 + n100*100) - 1300
|
||||
return alt
|
||||
|
||||
|
||||
def gray2int(graystr):
|
||||
"""Convert greycode to binary"""
|
||||
num = bin2int(graystr)
|
||||
num ^= (num >> 8)
|
||||
num ^= (num >> 4)
|
||||
num ^= (num >> 2)
|
||||
num ^= (num >> 1)
|
||||
return num
|
||||
|
||||
|
||||
def data(msg):
|
||||
"""Return the data frame in the message, bytes 9 to 22"""
|
||||
return msg[8:-6]
|
||||
|
||||
|
||||
def allzeros(msg):
|
||||
"""check if the data bits are all zeros
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if bin2int(d) > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def wrongstatus(data, sb, msb, lsb):
|
||||
"""Check if the status bit and field bits are consistency. This Function
|
||||
is used for checking BDS code versions.
|
||||
"""
|
||||
|
||||
# status bit, most significant bit, least significant bit
|
||||
status = int(data[sb-1])
|
||||
value = bin2int(data[msb-1:lsb])
|
||||
|
||||
if not status:
|
||||
if value != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,18 +1,66 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
"""EHS Wrapper.
|
||||
|
||||
``pyModeS.ehs`` is deprecated, please use ``pyModeS.commb`` instead.
|
||||
|
||||
The EHS wrapper imports all functions from the following modules:
|
||||
- pyModeS.decoder.bds.bds40
|
||||
- pyModeS.decoder.bds.bds50
|
||||
- pyModeS.decoder.bds.bds60
|
||||
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from pyModeS.decoder.bds.bds40 import *
|
||||
from pyModeS.decoder.bds.bds50 import *
|
||||
from pyModeS.decoder.bds.bds60 import *
|
||||
from pyModeS.decoder.bds import infer
|
||||
from .bds.bds40 import (
|
||||
is40,
|
||||
selalt40fms,
|
||||
selalt40mcp,
|
||||
p40baro,
|
||||
alt40fms,
|
||||
alt40mcp,
|
||||
)
|
||||
from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50
|
||||
from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins
|
||||
from .bds import infer
|
||||
|
||||
__all__ = [
|
||||
"is40",
|
||||
"selalt40fms",
|
||||
"selalt40mcp",
|
||||
"p40baro",
|
||||
"alt40fms",
|
||||
"alt40mcp",
|
||||
"is50",
|
||||
"roll50",
|
||||
"trk50",
|
||||
"gs50",
|
||||
"rtrk50",
|
||||
"tas50",
|
||||
"is60",
|
||||
"hdg60",
|
||||
"ias60",
|
||||
"mach60",
|
||||
"vr60baro",
|
||||
"vr60ins",
|
||||
"infer",
|
||||
]
|
||||
|
||||
warnings.simplefilter("once", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"pms.ehs module is deprecated. Please use pms.commb instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
warnings.simplefilter('once', DeprecationWarning)
|
||||
warnings.warn("pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning)
|
||||
|
||||
def BDS(msg):
|
||||
warnings.warn("pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return infer(msg)
|
||||
|
||||
|
||||
def icao(msg):
|
||||
from pyModeS.decoder.common import icao
|
||||
return icao(msg)
|
||||
from . import common
|
||||
|
||||
return common.icao(msg)
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
"""ELS Wrapper.
|
||||
|
||||
from pyModeS.decoder.bds.bds10 import *
|
||||
from pyModeS.decoder.bds.bds17 import *
|
||||
from pyModeS.decoder.bds.bds20 import *
|
||||
from pyModeS.decoder.bds.bds30 import *
|
||||
``pyModeS.els`` is deprecated, please use ``pyModeS.commb`` instead.
|
||||
|
||||
The ELS wrapper imports all functions from the following modules:
|
||||
- pyModeS.decoder.bds.bds10
|
||||
- pyModeS.decoder.bds.bds17
|
||||
- pyModeS.decoder.bds.bds20
|
||||
- pyModeS.decoder.bds.bds30
|
||||
|
||||
"""
|
||||
|
||||
import warnings
|
||||
warnings.simplefilter('once', DeprecationWarning)
|
||||
warnings.warn("pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning)
|
||||
|
||||
from .bds.bds10 import is10, ovc10
|
||||
from .bds.bds17 import cap17, is17
|
||||
from .bds.bds20 import cs20, is20
|
||||
from .bds.bds30 import is30
|
||||
|
||||
warnings.simplefilter("once", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"pms.els module is deprecated. Please use pms.commb instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"is10",
|
||||
"ovc10",
|
||||
"is17",
|
||||
"cap17",
|
||||
"is20",
|
||||
"cs20",
|
||||
"is30",
|
||||
]
|
||||
|
||||
25
pyModeS/decoder/flarm/__init__.py
Normal file
25
pyModeS/decoder/flarm/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from .decode import flarm as flarm_decode
|
||||
|
||||
__all__ = ["DecodedMessage", "flarm"]
|
||||
|
||||
|
||||
class DecodedMessage(TypedDict):
|
||||
timestamp: int
|
||||
icao24: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
altitude: int
|
||||
vertical_speed: float
|
||||
groundspeed: int
|
||||
track: int
|
||||
type: str
|
||||
sensorLatitude: float
|
||||
sensorLongitude: float
|
||||
isIcao24: bool
|
||||
noTrack: bool
|
||||
stealth: bool
|
||||
|
||||
|
||||
flarm = flarm_decode
|
||||
124
pyModeS/decoder/flarm/core.c
Normal file
124
pyModeS/decoder/flarm/core.c
Normal file
@@ -0,0 +1,124 @@
|
||||
#include "core.h"
|
||||
|
||||
/*
|
||||
*
|
||||
* https://pastebin.com/YK2f8bfm
|
||||
*
|
||||
* NEW ENCRYPTION
|
||||
*
|
||||
* Swiss glider anti-colission system moved to a new encryption scheme: XXTEA
|
||||
* The algorithm encrypts all the packet after the header: total 20 bytes or 5 long int words of data
|
||||
*
|
||||
* XXTEA description and code are found here: http://en.wikipedia.org/wiki/XXTEA
|
||||
* The system uses 6 iterations of the main loop.
|
||||
*
|
||||
* The system version 6 sends two type of packets: position and ... some unknown data
|
||||
* The difference is made by bit 0 of byte 3 of the packet: for position data this bit is zero.
|
||||
*
|
||||
* For position data the key used depends on the time and transmitting device address.
|
||||
* The key is as well obscured by a weird algorithm.
|
||||
* The code to generate the key is:
|
||||
*
|
||||
* */
|
||||
|
||||
void make_key(int *key, long time, long address)
|
||||
{
|
||||
const long key1[4] = {0xe43276df, 0xdca83759, 0x9802b8ac, 0x4675a56b};
|
||||
const long key1b[4] = {0xfc78ea65, 0x804b90ea, 0xb76542cd, 0x329dfa32};
|
||||
const long *table = ((((time >> 23) & 255) & 0x01) != 0) ? key1b : key1;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
key[i] = obscure(table[i] ^ ((time >> 6) ^ address), 0x045D9F3B) ^ 0x87B562F4;
|
||||
}
|
||||
}
|
||||
|
||||
long obscure(long key, unsigned long seed)
|
||||
{
|
||||
unsigned int m1 = seed * (key ^ (key >> 16));
|
||||
unsigned int m2 = seed * (m1 ^ (m1 >> 16));
|
||||
return m2 ^ (m2 >> 16);
|
||||
}
|
||||
|
||||
/*
|
||||
* NEW PACKET FORMAT:
|
||||
*
|
||||
* Byte Bits
|
||||
* 0 AAAA AAAA device address
|
||||
* 1 AAAA AAAA
|
||||
* 2 AAAA AAAA
|
||||
* 3 00aa 0000 aa = 10 or 01
|
||||
*
|
||||
* 4 vvvv vvvv vertical speed
|
||||
* 5 xxxx xxvv
|
||||
* 6 gggg gggg GPS status
|
||||
* 7 tttt gggg plane type
|
||||
*
|
||||
* 8 LLLL LLLL Latitude
|
||||
* 9 LLLL LLLL
|
||||
* 10 aaaa aLLL
|
||||
* 11 aaaa aaaa Altitude
|
||||
*
|
||||
* 12 NNNN NNNN Longitude
|
||||
* 13 NNNN NNNN
|
||||
* 14 xxxx NNNN
|
||||
* 15 FFxx xxxx multiplying factor
|
||||
*
|
||||
* 16 SSSS SSSS as in version 4
|
||||
* 17 ssss ssss
|
||||
* 18 KKKK KKKK
|
||||
* 19 kkkk kkkk
|
||||
*
|
||||
* 20 EEEE EEEE
|
||||
* 21 eeee eeee
|
||||
* 22 PPPP PPPP
|
||||
* 24 pppp pppp
|
||||
* */
|
||||
|
||||
/*
|
||||
* https://en.wikipedia.org/wiki/XXTEA
|
||||
*/
|
||||
|
||||
void btea(uint32_t *v, int n, uint32_t const key[4])
|
||||
{
|
||||
uint32_t y, z, sum;
|
||||
unsigned p, rounds, e;
|
||||
if (n > 1)
|
||||
{ /* Coding Part */
|
||||
/* Unused, should remove? */
|
||||
rounds = 6 + 52 / n;
|
||||
sum = 0;
|
||||
z = v[n - 1];
|
||||
do
|
||||
{
|
||||
sum += DELTA;
|
||||
e = (sum >> 2) & 3;
|
||||
for (p = 0; p < (unsigned)n - 1; p++)
|
||||
{
|
||||
y = v[p + 1];
|
||||
z = v[p] += MX;
|
||||
}
|
||||
y = v[0];
|
||||
z = v[n - 1] += MX;
|
||||
} while (--rounds);
|
||||
}
|
||||
else if (n < -1)
|
||||
{ /* Decoding Part */
|
||||
n = -n;
|
||||
rounds = 6; // + 52 / n;
|
||||
sum = rounds * DELTA;
|
||||
y = v[0];
|
||||
do
|
||||
{
|
||||
e = (sum >> 2) & 3;
|
||||
for (p = n - 1; p > 0; p--)
|
||||
{
|
||||
z = v[p - 1];
|
||||
y = v[p] -= MX;
|
||||
}
|
||||
z = v[n - 1];
|
||||
y = v[0] -= MX;
|
||||
sum -= DELTA;
|
||||
} while (--rounds);
|
||||
}
|
||||
}
|
||||
13
pyModeS/decoder/flarm/core.h
Normal file
13
pyModeS/decoder/flarm/core.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef __CORE_H__
|
||||
#define __CORE_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define DELTA 0x9e3779b9
|
||||
#define MX (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))
|
||||
|
||||
void make_key(int *key, long time, long address);
|
||||
long obscure(long key, unsigned long seed);
|
||||
void btea(uint32_t *v, int n, uint32_t const key[4]);
|
||||
|
||||
#endif
|
||||
4
pyModeS/decoder/flarm/core.pxd
Normal file
4
pyModeS/decoder/flarm/core.pxd
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
cdef extern from "core.h":
|
||||
void make_key(int*, long time, long address)
|
||||
void btea(int*, int, int*)
|
||||
14
pyModeS/decoder/flarm/decode.pyi
Normal file
14
pyModeS/decoder/flarm/decode.pyi
Normal file
@@ -0,0 +1,14 @@
|
||||
from typing import Any
|
||||
|
||||
from . import DecodedMessage
|
||||
|
||||
AIRCRAFT_TYPES: list[str]
|
||||
|
||||
|
||||
def flarm(
|
||||
timestamp: int,
|
||||
msg: str,
|
||||
refLat: float,
|
||||
refLon: float,
|
||||
**kwargs: Any,
|
||||
) -> DecodedMessage: ...
|
||||
145
pyModeS/decoder/flarm/decode.pyx
Normal file
145
pyModeS/decoder/flarm/decode.pyx
Normal file
@@ -0,0 +1,145 @@
|
||||
from core cimport make_key as c_make_key, btea as c_btea
|
||||
from cpython cimport array
|
||||
|
||||
import array
|
||||
import math
|
||||
from ctypes import c_byte
|
||||
from textwrap import wrap
|
||||
|
||||
AIRCRAFT_TYPES = [
|
||||
"Unknown", # 0
|
||||
"Glider", # 1
|
||||
"Tow-Plane", # 2
|
||||
"Helicopter", # 3
|
||||
"Parachute", # 4
|
||||
"Parachute Drop-Plane", # 5
|
||||
"Hangglider", # 6
|
||||
"Paraglider", # 7
|
||||
"Aircraft", # 8
|
||||
"Jet", # 9
|
||||
"UFO", # 10
|
||||
"Balloon", # 11
|
||||
"Airship", # 12
|
||||
"UAV", # 13
|
||||
"Reserved", # 14
|
||||
"Static Obstacle", # 15
|
||||
]
|
||||
|
||||
cdef long bytearray2int(str icao24):
|
||||
return (
|
||||
(int(icao24[4:6], 16) & 0xFF)
|
||||
| ((int(icao24[2:4], 16) & 0xFF) << 8)
|
||||
| ((int(icao24[:2], 16) & 0xFF) << 16)
|
||||
)
|
||||
|
||||
cpdef array.array make_key(long timestamp, str icao24):
|
||||
cdef long addr = bytearray2int(icao24)
|
||||
cdef array.array a = array.array('i', [0, 0, 0, 0])
|
||||
c_make_key(a.data.as_ints, timestamp, (addr << 8) & 0xffffff)
|
||||
return a
|
||||
|
||||
cpdef array.array btea(long timestamp, str msg):
|
||||
cdef int p
|
||||
cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2]
|
||||
cdef array.array key = make_key(timestamp, icao24)
|
||||
|
||||
pieces = wrap(msg[8:], 8)
|
||||
cdef array.array toDecode = array.array('i', len(pieces) * [0])
|
||||
for i, piece in enumerate(pieces):
|
||||
p = 0
|
||||
for elt in wrap(piece, 2)[::-1]:
|
||||
p = (p << 8) + int(elt, 16)
|
||||
toDecode[i] = p
|
||||
|
||||
c_btea(toDecode.data.as_ints, -5, key.data.as_ints)
|
||||
return toDecode
|
||||
|
||||
cdef float velocity(int ns, int ew):
|
||||
return math.hypot(ew / 4, ns / 4)
|
||||
|
||||
def heading(ns, ew, velocity):
|
||||
if velocity < 1e-6:
|
||||
velocity = 1
|
||||
return (math.atan2(ew / velocity / 4, ns / velocity / 4) / 0.01745) % 360
|
||||
|
||||
def turningRate(a1, a2):
|
||||
return ((((a2 - a1)) + 540) % 360) - 180
|
||||
|
||||
def flarm(long timestamp, str msg, float refLat, float refLon, **kwargs):
|
||||
"""Decode a FLARM message.
|
||||
|
||||
Args:
|
||||
timestamp (int)
|
||||
msg (str)
|
||||
refLat (float): the receiver's location
|
||||
refLon (float): the receiver's location
|
||||
|
||||
Returns:
|
||||
a dictionary with all decoded fields. Any extra keyword argument passed
|
||||
is included in the output dictionary.
|
||||
"""
|
||||
cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2]
|
||||
cdef int magic = int(msg[6:8], 16)
|
||||
|
||||
if magic != 0x10 and magic != 0x20:
|
||||
return None
|
||||
|
||||
cdef array.array decoded = btea(timestamp, msg)
|
||||
|
||||
cdef int aircraft_type = (decoded[0] >> 28) & 0xF
|
||||
cdef int gps = (decoded[0] >> 16) & 0xFFF
|
||||
cdef int raw_vs = c_byte(decoded[0] & 0x3FF).value
|
||||
|
||||
noTrack = ((decoded[0] >> 14) & 0x1) == 1
|
||||
stealth = ((decoded[0] >> 13) & 0x1) == 1
|
||||
|
||||
cdef int altitude = (decoded[1] >> 19) & 0x1FFF
|
||||
|
||||
cdef int lat = decoded[1] & 0x7FFFF
|
||||
|
||||
cdef int mult_factor = 1 << ((decoded[2] >> 30) & 0x3)
|
||||
cdef int lon = decoded[2] & 0xFFFFF
|
||||
|
||||
ns = list(
|
||||
c_byte((decoded[3] >> (i * 8)) & 0xFF).value * mult_factor
|
||||
for i in range(4)
|
||||
)
|
||||
ew = list(
|
||||
c_byte((decoded[4] >> (i * 8)) & 0xFF).value * mult_factor
|
||||
for i in range(4)
|
||||
)
|
||||
|
||||
cdef int roundLat = int(refLat * 1e7) >> 7
|
||||
lat = (lat - roundLat) % 0x080000
|
||||
if lat >= 0x040000:
|
||||
lat -= 0x080000
|
||||
lat = (((lat + roundLat) << 7) + 0x40)
|
||||
|
||||
roundLon = int(refLon * 1e7) >> 7
|
||||
lon = (lon - roundLon) % 0x100000
|
||||
if lon >= 0x080000:
|
||||
lon -= 0x100000
|
||||
lon = (((lon + roundLon) << 7) + 0x40)
|
||||
|
||||
speed = sum(velocity(n, e) for n, e in zip(ns, ew)) / 4
|
||||
|
||||
heading4 = heading(ns[0], ew[0], speed)
|
||||
heading8 = heading(ns[1], ew[1], speed)
|
||||
|
||||
return dict(
|
||||
timestamp=timestamp,
|
||||
icao24=icao24,
|
||||
latitude=round(lat * 1e-7, 6),
|
||||
longitude=round(lon * 1e-7, 6),
|
||||
geoaltitude=altitude,
|
||||
vertical_speed=raw_vs * mult_factor / 10,
|
||||
groundspeed=round(speed),
|
||||
track=round(heading4 - 4 * turningRate(heading4, heading8) / 4),
|
||||
type=AIRCRAFT_TYPES[aircraft_type],
|
||||
sensorLatitude=refLat,
|
||||
sensorLongitude=refLon,
|
||||
isIcao24=magic==0x10,
|
||||
noTrack=noTrack,
|
||||
stealth=stealth,
|
||||
**kwargs
|
||||
)
|
||||
@@ -1,21 +1,138 @@
|
||||
# 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
|
||||
Decode short roll call surveillance replies, with downlink format 4 or 5
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
from __future__ import annotations
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
from .. import common
|
||||
|
||||
T = TypeVar("T")
|
||||
F = Callable[[str], T]
|
||||
|
||||
|
||||
def _checkdf(func: F[T]) -> F[T]:
|
||||
"""Ensure downlink format is 4 or 5."""
|
||||
|
||||
def wrapper(msg: str) -> T:
|
||||
df = common.df(msg)
|
||||
if df not in [4, 5]:
|
||||
raise RuntimeError(
|
||||
"Incorrect downlink format, expect 4 or 5, got {}".format(df)
|
||||
)
|
||||
return func(msg)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@_checkdf
|
||||
def fs(msg: str) -> tuple[int, str]:
|
||||
"""Decode flight status.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: flight status, description
|
||||
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
fs = common.bin2int(msgbin[5:8])
|
||||
text = ""
|
||||
|
||||
if fs == 0:
|
||||
text = "no alert, no SPI, aircraft is airborne"
|
||||
elif fs == 1:
|
||||
text = "no alert, no SPI, aircraft is on-ground"
|
||||
elif fs == 2:
|
||||
text = "alert, no SPI, aircraft is airborne"
|
||||
elif fs == 3:
|
||||
text = "alert, no SPI, aircraft is on-ground"
|
||||
elif fs == 4:
|
||||
text = "alert, SPI, aircraft is airborne or on-ground"
|
||||
elif fs == 5:
|
||||
text = "no alert, SPI, aircraft is airborne or on-ground"
|
||||
|
||||
return fs, text
|
||||
|
||||
|
||||
@_checkdf
|
||||
def dr(msg: str) -> tuple[int, str]:
|
||||
"""Decode downlink request.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: downlink request, description
|
||||
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
dr = common.bin2int(msgbin[8:13])
|
||||
|
||||
text = ""
|
||||
|
||||
if dr == 0:
|
||||
text = "no downlink request"
|
||||
elif dr == 1:
|
||||
text = "request to send Comm-B message"
|
||||
elif dr == 4:
|
||||
text = "Comm-B broadcast 1 available"
|
||||
elif dr == 5:
|
||||
text = "Comm-B broadcast 2 available"
|
||||
elif dr >= 16:
|
||||
text = "ELM downlink segments available: {}".format(dr - 15)
|
||||
|
||||
return dr, text
|
||||
|
||||
|
||||
@_checkdf
|
||||
def um(msg: str) -> tuple[int, int, None | str]:
|
||||
"""Decode utility message.
|
||||
|
||||
Utility message contains interrogator identifier and reservation type.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: interrogator identifier code that triggered the reply, and
|
||||
reservation type made by the interrogator
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
iis = common.bin2int(msgbin[13:17])
|
||||
ids = common.bin2int(msgbin[17:19])
|
||||
if ids == 0:
|
||||
ids_text = None
|
||||
if ids == 1:
|
||||
ids_text = "Comm-B interrogator identifier code"
|
||||
if ids == 2:
|
||||
ids_text = "Comm-C interrogator identifier code"
|
||||
if ids == 3:
|
||||
ids_text = "Comm-D interrogator identifier code"
|
||||
return iis, ids, ids_text
|
||||
|
||||
|
||||
@_checkdf
|
||||
def altitude(msg: str) -> None | int:
|
||||
"""Decode altitude.
|
||||
|
||||
Args:
|
||||
msg (String): 14 hexdigits string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
|
||||
"""
|
||||
return common.altcode(msg)
|
||||
|
||||
|
||||
@_checkdf
|
||||
def identity(msg: str) -> str:
|
||||
"""Decode squawk code.
|
||||
|
||||
Args:
|
||||
msg (String): 14 hexdigits string
|
||||
|
||||
Returns:
|
||||
string: squawk code
|
||||
|
||||
"""
|
||||
return common.idcode(msg)
|
||||
|
||||
@@ -1,120 +1,200 @@
|
||||
"""Uncertainty parameters.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import TypedDict
|
||||
else:
|
||||
from typing import TypedDict
|
||||
|
||||
NA = None
|
||||
|
||||
TC_NUCp_lookup = {
|
||||
0:0, 5:9, 6:8, 7:7, 8:6,
|
||||
9:9, 10:8, 11:7, 12:6, 13:5, 14:4, 15:3, 16:2, 17:1, 18:0,
|
||||
20:9, 21:8, 22:0
|
||||
0: 0,
|
||||
5: 9,
|
||||
6: 8,
|
||||
7: 7,
|
||||
8: 6,
|
||||
9: 9,
|
||||
10: 8,
|
||||
11: 7,
|
||||
12: 6,
|
||||
13: 5,
|
||||
14: 4,
|
||||
15: 3,
|
||||
16: 2,
|
||||
17: 1,
|
||||
18: 0,
|
||||
20: 9,
|
||||
21: 8,
|
||||
22: 0,
|
||||
}
|
||||
|
||||
TC_NICv1_lookup = {
|
||||
5:11, 6:10, 7:9, 8:0,
|
||||
9:11, 10:10, 11:{1:9, 0:8}, 12:7, 13:6, 14:5, 15:4, 16:{1:3, 0:2}, 17:1, 18:0,
|
||||
20:11, 21:10, 22:0
|
||||
TC_NICv1_lookup: dict[int, int | dict[int, int]] = {
|
||||
5: 11,
|
||||
6: 10,
|
||||
7: 9,
|
||||
8: 0,
|
||||
9: 11,
|
||||
10: 10,
|
||||
11: {1: 9, 0: 8},
|
||||
12: 7,
|
||||
13: 6,
|
||||
14: 5,
|
||||
15: 4,
|
||||
16: {1: 3, 0: 2},
|
||||
17: 1,
|
||||
18: 0,
|
||||
20: 11,
|
||||
21: 10,
|
||||
22: 0,
|
||||
}
|
||||
|
||||
TC_NICv2_lookup = {
|
||||
5:11, 6:10, 7:{2:9, 0:8}, 8:{3:7, 2:6, 1:6, 0:0},
|
||||
9:11, 10:10, 11:{3:9, 0:8}, 12:7, 13:6, 14:5, 15:4, 16:{3:3, 0:2}, 17:1, 18:0,
|
||||
20:11, 21:10, 22:0
|
||||
TC_NICv2_lookup: dict[int, int | dict[int, int]] = {
|
||||
5: 11,
|
||||
6: 10,
|
||||
7: {2: 9, 0: 8},
|
||||
8: {3: 7, 2: 6, 1: 6, 0: 0},
|
||||
9: 11,
|
||||
10: 10,
|
||||
11: {3: 9, 0: 8},
|
||||
12: 7,
|
||||
13: 6,
|
||||
14: 5,
|
||||
15: 4,
|
||||
16: {3: 3, 0: 2},
|
||||
17: 1,
|
||||
18: 0,
|
||||
20: 11,
|
||||
21: 10,
|
||||
22: 0,
|
||||
}
|
||||
|
||||
|
||||
NUCp = {
|
||||
9: {'HPL':7.5, 'RCu':3, 'RCv':4},
|
||||
8: {'HPL':25, 'RCu':10, 'RCv':15},
|
||||
7: {'HPL':185, 'RCu':93, 'RCv':NA},
|
||||
6: {'HPL':370, 'RCu':185, 'RCv':NA},
|
||||
5: {'HPL':926, 'RCu':463, 'RCv':NA},
|
||||
4: {'HPL':1852, 'RCu':926, 'RCv':NA},
|
||||
3: {'HPL':3704, 'RCu':1852, 'RCv':NA},
|
||||
2: {'HPL':18520, 'RCu':9260, 'RCv':NA},
|
||||
1: {'HPL':37040, 'RCu':18520, 'RCv':NA},
|
||||
0: {'HPL':NA, 'RCu':NA, 'RCv':NA},
|
||||
}
|
||||
class NUCpEntry(TypedDict):
|
||||
HPL: None | float
|
||||
RCu: None | int
|
||||
RCv: None | int
|
||||
|
||||
NUCv = {
|
||||
0: {'HVE':NA, 'VVE':NA},
|
||||
1: {'HVE':10, 'VVE':15.2},
|
||||
2: {'HVE':3, 'VVE':4.5},
|
||||
3: {'HVE':1, 'VVE':1.5},
|
||||
4: {'HVE':0.3, 'VVE':0.46},
|
||||
}
|
||||
|
||||
NACp = {
|
||||
11: {'EPU': 3, 'VEPU': 4},
|
||||
10: {'EPU': 10, 'VEPU': 15},
|
||||
9: {'EPU': 30, 'VEPU': 45},
|
||||
8: {'EPU': 93, 'VEPU': NA},
|
||||
7: {'EPU': 185, 'VEPU': NA},
|
||||
6: {'EPU': 556, 'VEPU': NA},
|
||||
5: {'EPU': 926, 'VEPU': NA},
|
||||
4: {'EPU': 1852, 'VEPU': NA},
|
||||
3: {'EPU': 3704, 'VEPU': NA},
|
||||
2: {'EPU': 7408, 'VEPU': NA},
|
||||
1: {'EPU': 18520, 'VEPU': NA},
|
||||
0: {'EPU': NA, 'VEPU': NA},
|
||||
}
|
||||
|
||||
NACv = {
|
||||
0: {'HFOMr':NA, 'VFOMr':NA},
|
||||
1: {'HFOMr':10, 'VFOMr':15.2},
|
||||
2: {'HFOMr':3, 'VFOMr':4.5},
|
||||
3: {'HFOMr':1, 'VFOMr':1.5},
|
||||
4: {'HFOMr':0.3, 'VFOMr':0.46},
|
||||
}
|
||||
|
||||
SIL = {
|
||||
3: {'PE_RCu': 1e-7, 'PE_VPL': 2e-7},
|
||||
2: {'PE_RCu': 1e-5, 'PE_VPL': 1e-5},
|
||||
1: {'PE_RCu': 1e-3, 'PE_VPL': 1e-3},
|
||||
0: {'PE_RCu': NA, 'PE_VPL': NA},
|
||||
NUCp: dict[int, NUCpEntry] = {
|
||||
9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
|
||||
8: {"HPL": 25, "RCu": 10, "RCv": 15},
|
||||
7: {"HPL": 185, "RCu": 93, "RCv": NA},
|
||||
6: {"HPL": 370, "RCu": 185, "RCv": NA},
|
||||
5: {"HPL": 926, "RCu": 463, "RCv": NA},
|
||||
4: {"HPL": 1852, "RCu": 926, "RCv": NA},
|
||||
3: {"HPL": 3704, "RCu": 1852, "RCv": NA},
|
||||
2: {"HPL": 18520, "RCu": 9260, "RCv": NA},
|
||||
1: {"HPL": 37040, "RCu": 18520, "RCv": NA},
|
||||
0: {"HPL": NA, "RCu": NA, "RCv": NA},
|
||||
}
|
||||
|
||||
|
||||
NICv1 = {
|
||||
class NUCvEntry(TypedDict):
|
||||
HVE: None | float
|
||||
VVE: None | float
|
||||
|
||||
|
||||
NUCv: dict[int, NUCvEntry] = {
|
||||
0: {"HVE": NA, "VVE": NA},
|
||||
1: {"HVE": 10, "VVE": 15.2},
|
||||
2: {"HVE": 3, "VVE": 4.5},
|
||||
3: {"HVE": 1, "VVE": 1.5},
|
||||
4: {"HVE": 0.3, "VVE": 0.46},
|
||||
}
|
||||
|
||||
|
||||
class NACpEntry(TypedDict):
|
||||
EPU: None | int
|
||||
VEPU: None | int
|
||||
|
||||
|
||||
NACp: dict[int, NACpEntry] = {
|
||||
11: {"EPU": 3, "VEPU": 4},
|
||||
10: {"EPU": 10, "VEPU": 15},
|
||||
9: {"EPU": 30, "VEPU": 45},
|
||||
8: {"EPU": 93, "VEPU": NA},
|
||||
7: {"EPU": 185, "VEPU": NA},
|
||||
6: {"EPU": 556, "VEPU": NA},
|
||||
5: {"EPU": 926, "VEPU": NA},
|
||||
4: {"EPU": 1852, "VEPU": NA},
|
||||
3: {"EPU": 3704, "VEPU": NA},
|
||||
2: {"EPU": 7408, "VEPU": NA},
|
||||
1: {"EPU": 18520, "VEPU": NA},
|
||||
0: {"EPU": NA, "VEPU": NA},
|
||||
}
|
||||
|
||||
|
||||
class NACvEntry(TypedDict):
|
||||
HFOMr: None | float
|
||||
VFOMr: None | float
|
||||
|
||||
|
||||
NACv: dict[int, NACvEntry] = {
|
||||
0: {"HFOMr": NA, "VFOMr": NA},
|
||||
1: {"HFOMr": 10, "VFOMr": 15.2},
|
||||
2: {"HFOMr": 3, "VFOMr": 4.5},
|
||||
3: {"HFOMr": 1, "VFOMr": 1.5},
|
||||
4: {"HFOMr": 0.3, "VFOMr": 0.46},
|
||||
}
|
||||
|
||||
|
||||
class SILEntry(TypedDict):
|
||||
PE_RCu: None | float
|
||||
PE_VPL: None | float
|
||||
|
||||
|
||||
SIL: dict[int, SILEntry] = {
|
||||
3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
|
||||
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
|
||||
1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
|
||||
0: {"PE_RCu": NA, "PE_VPL": NA},
|
||||
}
|
||||
|
||||
|
||||
class NICv1Entry(TypedDict):
|
||||
Rc: None | float
|
||||
VPL: None | float
|
||||
|
||||
|
||||
NICv1: dict[int, dict[int, NICv1Entry]] = {
|
||||
# NIC is used as the index at second Level
|
||||
|
||||
11: {0: {'Rc': 7.5, 'VPL': 11}},
|
||||
10: {0: {'Rc': 25, 'VPL': 37.5}},
|
||||
9: {1: {'Rc': 75, 'VPL': 112}},
|
||||
8: {0: {'Rc': 185, 'VPL': NA}},
|
||||
7: {0: {'Rc': 370, 'VPL': NA}},
|
||||
6: {
|
||||
0: {'Rc': 926, 'VPL': NA},
|
||||
1: {'Rc': 1111, 'VPL': NA},
|
||||
},
|
||||
5: {0: {'Rc': 1852, 'VPL': NA}},
|
||||
4: {0: {'Rc': 3702, 'VPL': NA}},
|
||||
3: {1: {'Rc': 7408, 'VPL': NA}},
|
||||
2: {0: {'Rc': 14008, 'VPL': NA}},
|
||||
1: {0: {'Rc': 37000, 'VPL': NA}},
|
||||
0: {0: {'Rc': NA, 'VPL': NA}},
|
||||
11: {0: {"Rc": 7.5, "VPL": 11}},
|
||||
10: {0: {"Rc": 25, "VPL": 37.5}},
|
||||
9: {1: {"Rc": 75, "VPL": 112}},
|
||||
8: {0: {"Rc": 185, "VPL": NA}},
|
||||
7: {0: {"Rc": 370, "VPL": NA}},
|
||||
6: {0: {"Rc": 926, "VPL": NA}, 1: {"Rc": 1111, "VPL": NA}},
|
||||
5: {0: {"Rc": 1852, "VPL": NA}},
|
||||
4: {0: {"Rc": 3702, "VPL": NA}},
|
||||
3: {1: {"Rc": 7408, "VPL": NA}},
|
||||
2: {0: {"Rc": 14008, "VPL": NA}},
|
||||
1: {0: {"Rc": 37000, "VPL": NA}},
|
||||
0: {0: {"Rc": NA, "VPL": NA}},
|
||||
}
|
||||
|
||||
NICv2 = {
|
||||
|
||||
class NICv2Entry(TypedDict):
|
||||
Rc: None | float
|
||||
|
||||
|
||||
NICv2: dict[int, dict[int, NICv2Entry]] = {
|
||||
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
|
||||
|
||||
11: {0: {'Rc': 7.5}},
|
||||
10: {0: {'Rc': 25}},
|
||||
9: {
|
||||
2: {'Rc': 75},
|
||||
3: {'Rc': 75},
|
||||
},
|
||||
8: {0: {'Rc': 185}},
|
||||
7: {
|
||||
0: {'Rc': 370},
|
||||
3: {'Rc': 370},
|
||||
},
|
||||
6: {
|
||||
0: {'Rc': 926},
|
||||
1: {'Rc': 556},
|
||||
2: {'Rc': 556},
|
||||
3: {'Rc': 1111},
|
||||
},
|
||||
5: {0: {'Rc': 1852}},
|
||||
4: {0: {'Rc': 3702}},
|
||||
3: {3: {'Rc': 7408}},
|
||||
2: {0: {'Rc': 14008}},
|
||||
1: {0: {'Rc': 37000}},
|
||||
0: {0: {'Rc': NA}},
|
||||
11: {0: {"Rc": 7.5}},
|
||||
10: {0: {"Rc": 25}},
|
||||
9: {2: {"Rc": 75}, 3: {"Rc": 75}},
|
||||
8: {0: {"Rc": 185}},
|
||||
7: {0: {"Rc": 370}, 3: {"Rc": 370}},
|
||||
6: {0: {"Rc": 926}, 1: {"Rc": 556}, 2: {"Rc": 556}, 3: {"Rc": 1111}},
|
||||
5: {0: {"Rc": 1852}},
|
||||
4: {0: {"Rc": 3702}},
|
||||
3: {3: {"Rc": 7408}},
|
||||
2: {0: {"Rc": 14008}},
|
||||
1: {0: {"Rc": 37000}},
|
||||
0: {0: {"Rc": NA}},
|
||||
}
|
||||
|
||||
223
pyModeS/decoder/uplink.py
Normal file
223
pyModeS/decoder/uplink.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from typing import Optional
|
||||
from .. import common
|
||||
from textwrap import wrap
|
||||
|
||||
|
||||
def uplink_icao(msg: str) -> str:
|
||||
"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: str) -> int:
|
||||
"""Decode Uplink Format value, bits 1 to 5."""
|
||||
ufbin = common.hex2bin(msg[:2])
|
||||
return min(common.bin2int(ufbin[0:5]), 24)
|
||||
|
||||
|
||||
def bds(msg: str) -> Optional[str]:
|
||||
"Decode requested BDS register from selective (Roll Call) interrogation."
|
||||
UF = uf(msg)
|
||||
msgbin = common.hex2bin(msg)
|
||||
msgbin_split = wrap(msgbin, 8)
|
||||
mbytes = list(map(common.bin2int, msgbin_split))
|
||||
|
||||
if UF in {4, 5, 20, 21}:
|
||||
|
||||
di = mbytes[1] & 0x7 # DI - Designator Identification
|
||||
RR = mbytes[1] >> 3 & 0x1F
|
||||
if RR > 15:
|
||||
BDS1 = RR - 16
|
||||
if di == 7:
|
||||
RRS = mbytes[2] & 0x0F
|
||||
BDS2 = RRS
|
||||
elif di == 3:
|
||||
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
|
||||
BDS2 = RRS
|
||||
else:
|
||||
# for other values of DI, the BDS2 is assumed 0
|
||||
# (as per ICAO Annex 10 Vol IV)
|
||||
BDS2 = 0
|
||||
|
||||
return str(format(BDS1,"X")) + str(format(BDS2,"X"))
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def pr(msg: str) -> Optional[int]:
|
||||
"""Decode PR (probability of reply) field from All Call interrogation.
|
||||
Interpretation:
|
||||
0 signifies reply with probability of 1
|
||||
1 signifies reply with probability of 1/2
|
||||
2 signifies reply with probability of 1/4
|
||||
3 signifies reply with probability of 1/8
|
||||
4 signifies reply with probability of 1/16
|
||||
5, 6, 7 not assigned
|
||||
8 signifies disregard lockout, reply with probability of 1
|
||||
9 signifies disregard lockout, reply with probability of 1/2
|
||||
10 signifies disregard lockout, reply with probability of 1/4
|
||||
11 signifies disregard lockout, reply with probability of 1/8
|
||||
12 signifies disregard lockout, reply with probability of 1/16
|
||||
13, 14, 15 not assigned.
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
msgbin_split = wrap(msgbin, 8)
|
||||
mbytes = list(map(common.bin2int, msgbin_split))
|
||||
if uf(msg) == 11:
|
||||
return ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def ic(msg: str) -> Optional[str]:
|
||||
"""Decode IC (interrogator code) from a ground-based interrogation."""
|
||||
|
||||
UF = uf(msg)
|
||||
msgbin = common.hex2bin(msg)
|
||||
msgbin_split = wrap(msgbin, 8)
|
||||
mbytes = list(map(common.bin2int, msgbin_split))
|
||||
IC = None
|
||||
if UF == 11:
|
||||
|
||||
codeLabel = mbytes[1] & 0x7
|
||||
icField = (mbytes[1] >> 3) & 0xF
|
||||
|
||||
# Store the Interogator Code
|
||||
ic_switcher = {
|
||||
0: "II" + str(icField),
|
||||
1: "SI" + str(icField),
|
||||
2: "SI" + str(icField + 16),
|
||||
3: "SI" + str(icField + 32),
|
||||
4: "SI" + str(icField + 48),
|
||||
}
|
||||
IC = ic_switcher.get(codeLabel, "")
|
||||
|
||||
if UF in {4, 5, 20, 21}:
|
||||
di = mbytes[1] & 0x7
|
||||
RR = mbytes[1] >> 3 & 0x1F
|
||||
if RR > 15:
|
||||
BDS1 = RR - 16 # noqa: F841
|
||||
if di == 0 or di == 1 or di == 7:
|
||||
# II
|
||||
II = (mbytes[2] >> 4) & 0xF
|
||||
IC = "II" + str(II)
|
||||
elif di == 3:
|
||||
# SI
|
||||
SI = (mbytes[2] >> 2) & 0x3F
|
||||
IC = "SI" + str(SI)
|
||||
return IC
|
||||
|
||||
|
||||
def lockout(msg):
|
||||
"""Decode the lockout command from selective (Roll Call) interrogation."""
|
||||
msgbin = common.hex2bin(msg)
|
||||
msgbin_split = wrap(msgbin, 8)
|
||||
mbytes = list(map(common.bin2int, msgbin_split))
|
||||
|
||||
if uf(msg) in {4, 5, 20, 21}:
|
||||
lockout = False
|
||||
di = mbytes[1] & 0x7
|
||||
if di == 7:
|
||||
# LOS
|
||||
if ((mbytes[3] & 0x40) >> 6) == 1:
|
||||
lockout = True
|
||||
elif di == 3:
|
||||
# LSS
|
||||
if ((mbytes[2] & 0x2) >> 1) == 1:
|
||||
lockout = True
|
||||
return lockout
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def uplink_fields(msg):
|
||||
"""Decode individual fields of a ground-based interrogation."""
|
||||
msgbin = common.hex2bin(msg)
|
||||
msgbin_split = wrap(msgbin, 8)
|
||||
mbytes = list(map(common.bin2int, msgbin_split))
|
||||
PR = ""
|
||||
IC = ""
|
||||
lockout = False
|
||||
di = ""
|
||||
RR = ""
|
||||
RRS = ""
|
||||
BDS = ""
|
||||
if uf(msg) == 11:
|
||||
|
||||
# Probability of Reply decoding
|
||||
|
||||
PR = ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7)
|
||||
|
||||
# Get cl and ic bit fields from the data
|
||||
# Decode the SI or II interrogator code
|
||||
codeLabel = mbytes[1] & 0x7
|
||||
icField = (mbytes[1] >> 3) & 0xF
|
||||
|
||||
# Store the Interogator Code
|
||||
ic_switcher = {
|
||||
0: "II" + str(icField),
|
||||
1: "SI" + str(icField),
|
||||
2: "SI" + str(icField + 16),
|
||||
3: "SI" + str(icField + 32),
|
||||
4: "SI" + str(icField + 48),
|
||||
}
|
||||
IC = ic_switcher.get(codeLabel, "")
|
||||
|
||||
if uf(msg) in {4, 5, 20, 21}:
|
||||
# Decode the DI and get the lockout information conveniently
|
||||
# (LSS or LOS)
|
||||
|
||||
# DI - Designator Identification
|
||||
di = mbytes[1] & 0x7
|
||||
RR = mbytes[1] >> 3 & 0x1F
|
||||
if RR > 15:
|
||||
BDS1 = RR - 16
|
||||
BDS2 = 0
|
||||
if di == 0 or di == 1:
|
||||
# II
|
||||
II = (mbytes[2] >> 4) & 0xF
|
||||
IC = "II" + str(II)
|
||||
elif di == 7:
|
||||
# LOS
|
||||
if ((mbytes[3] & 0x40) >> 6) == 1:
|
||||
lockout = True
|
||||
# II
|
||||
II = (mbytes[2] >> 4) & 0xF
|
||||
IC = "II" + str(II)
|
||||
RRS = mbytes[2] & 0x0F
|
||||
BDS2 = RRS
|
||||
elif di == 3:
|
||||
# LSS
|
||||
if ((mbytes[2] & 0x2) >> 1) == 1:
|
||||
lockout = True
|
||||
# SI
|
||||
SI = (mbytes[2] >> 2) & 0x3F
|
||||
IC = "SI" + str(SI)
|
||||
RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5)
|
||||
BDS2 = RRS
|
||||
if RR > 15:
|
||||
BDS = str(format(BDS1,"X")) + str(format(BDS2,"X"))
|
||||
|
||||
return {
|
||||
"DI": di,
|
||||
"IC": IC,
|
||||
"LOS": lockout,
|
||||
"PR": PR,
|
||||
"RR": RR,
|
||||
"RRS": RRS,
|
||||
"BDS": BDS,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
"""
|
||||
Functions for aeronautics in this module
|
||||
- physical quantities always in SI units
|
||||
- lat,lon,course and heading in degrees
|
||||
|
||||
- physical quantities always in SI units
|
||||
- lat,lon,course and heading in degrees
|
||||
|
||||
International Standard Atmosphere
|
||||
::
|
||||
|
||||
p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m]
|
||||
a = vsound(H) # speed of sound [m/s] as function of H[m]
|
||||
p = pressure(H) # calls atmos but retruns only pressure [Pa]
|
||||
p = pressure(H) # calls atmos but returns only pressure [Pa]
|
||||
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:
|
||||
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
|
||||
Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion
|
||||
Vtas = mach2tas(Mach,H) # mach number to true airspeed (Vtas) conversion
|
||||
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
|
||||
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
|
||||
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
|
||||
Vcas = tas2cas(Vtas,H) # Vtas to Vcas conversion both m/s, H in [m]
|
||||
Vcas = mach2cas(Mach,H) # Mach to Vcas conversion Vcas in m/s, H in [m]
|
||||
Mach = cas2mach(Vcas,H) # Vcas to mach copnversion Vcas in m/s, H in [m]
|
||||
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
"""Aero and geo Constants """
|
||||
kts = 0.514444 # knot -> m/s
|
||||
ft = 0.3048 # ft -> m
|
||||
fpm = 0.00508 # ft/min -> m/s
|
||||
inch = 0.0254 # inch -> m
|
||||
sqft = 0.09290304 # 1 square foot
|
||||
nm = 1852. # nautical mile -> m
|
||||
lbs = 0.453592 # pound -> kg
|
||||
g0 = 9.80665 # m/s2, Sea level gravity constant
|
||||
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
|
||||
p0 = 101325. # Pa, air pressure, sea level ISA
|
||||
rho0 = 1.225 # kg/m3, air density, sea level ISA
|
||||
T0 = 288.15 # K, temperature, sea level ISA
|
||||
gamma = 1.40 # cp/cv for air
|
||||
gamma1 = 0.2 # (gamma-1)/2 for air
|
||||
gamma2 = 3.5 # gamma/(gamma-1) for air
|
||||
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
|
||||
r_earth = 6371000. # m, average earth radius
|
||||
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
|
||||
kts = 0.514444 # knot -> m/s
|
||||
ft = 0.3048 # ft -> m
|
||||
fpm = 0.00508 # ft/min -> m/s
|
||||
inch = 0.0254 # inch -> m
|
||||
sqft = 0.09290304 # 1 square foot
|
||||
nm = 1852 # nautical mile -> m
|
||||
lbs = 0.453592 # pound -> kg
|
||||
g0 = 9.80665 # m/s2, Sea level gravity constant
|
||||
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
|
||||
p0 = 101325 # Pa, air pressure, sea level ISA
|
||||
rho0 = 1.225 # kg/m3, air density, sea level ISA
|
||||
T0 = 288.15 # K, temperature, sea level ISA
|
||||
gamma = 1.40 # cp/cv for air
|
||||
gamma1 = 0.2 # (gamma-1)/2 for air
|
||||
gamma2 = 3.5 # gamma/(gamma-1) for air
|
||||
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
|
||||
r_earth = 6371000 # m, average earth radius
|
||||
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
|
||||
|
||||
|
||||
def atmos(H):
|
||||
# H in metres
|
||||
T = np.maximum(288.15 - 0.0065 * H, 216.65)
|
||||
rhotrop = 1.225 * (T / 288.15)**4.256848030018761
|
||||
dhstrat = np.maximum(0., H - 11000.0)
|
||||
rhotrop = 1.225 * (T / 288.15) ** 4.256848030018761
|
||||
dhstrat = np.maximum(0.0, H - 11000.0)
|
||||
rho = rhotrop * np.exp(-dhstrat / 6341.552161)
|
||||
p = rho * R * T
|
||||
return p, rho, T
|
||||
@@ -88,18 +94,20 @@ def distance(lat1, lon1, lat2, lon2, H=0):
|
||||
"""
|
||||
|
||||
# phi = 90 - latitude
|
||||
phi1 = np.radians(90.0 - lat1)
|
||||
phi2 = np.radians(90.0 - lat2)
|
||||
phi1 = np.radians(90 - lat1)
|
||||
phi2 = np.radians(90 - lat2)
|
||||
|
||||
# theta = longitude
|
||||
theta1 = np.radians(lon1)
|
||||
theta2 = np.radians(lon2)
|
||||
|
||||
cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(phi2)
|
||||
cos = np.where(cos>1, 1, cos)
|
||||
cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(
|
||||
phi2
|
||||
)
|
||||
cos = np.where(cos > 1, 1, cos)
|
||||
|
||||
arc = np.arccos(cos)
|
||||
dist = arc * (r_earth + H) # meters, radius of earth
|
||||
dist = arc * (r_earth + H) # meters, radius of earth
|
||||
return dist
|
||||
|
||||
|
||||
@@ -108,9 +116,8 @@ def bearing(lat1, lon1, lat2, lon2):
|
||||
lon1 = np.radians(lon1)
|
||||
lat2 = np.radians(lat2)
|
||||
lon2 = np.radians(lon2)
|
||||
x = np.sin(lon2-lon1) * np.cos(lat2)
|
||||
y = np.cos(lat1) * np.sin(lat2) \
|
||||
- np.sin(lat1) * np.cos(lat2) * np.cos(lon2-lon1)
|
||||
x = np.sin(lon2 - lon1) * np.cos(lat2)
|
||||
y = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(lon2 - lon1)
|
||||
initial_bearing = np.arctan2(x, y)
|
||||
initial_bearing = np.degrees(initial_bearing)
|
||||
bearing = (initial_bearing + 360) % 360
|
||||
@@ -123,44 +130,44 @@ def bearing(lat1, lon1, lat2, lon2):
|
||||
def tas2mach(Vtas, H):
|
||||
"""True Airspeed to Mach number"""
|
||||
a = vsound(H)
|
||||
Mach = Vtas/a
|
||||
Mach = Vtas / a
|
||||
return Mach
|
||||
|
||||
|
||||
def mach2tas(Mach, H):
|
||||
"""Mach number to True Airspeed"""
|
||||
a = vsound(H)
|
||||
Vtas = Mach*a
|
||||
Vtas = Mach * a
|
||||
return Vtas
|
||||
|
||||
|
||||
def eas2tas(Veas, H):
|
||||
"""Equivalent Airspeed to True Airspeed"""
|
||||
rho = density(H)
|
||||
Vtas = Veas * np.sqrt(rho0/rho)
|
||||
Vtas = Veas * np.sqrt(rho0 / rho)
|
||||
return Vtas
|
||||
|
||||
|
||||
def tas2eas(Vtas, H):
|
||||
"""True Airspeed to Equivalent Airspeed"""
|
||||
rho = density(H)
|
||||
Veas = Vtas * np.sqrt(rho/rho0)
|
||||
Veas = Vtas * np.sqrt(rho / rho0)
|
||||
return Veas
|
||||
|
||||
|
||||
def cas2tas(Vcas, H):
|
||||
"""Calibrated Airspeed to True Airspeed"""
|
||||
p, rho, T = atmos(H)
|
||||
qdyn = p0*((1.+rho0*Vcas*Vcas/(7.*p0))**3.5-1.)
|
||||
Vtas = np.sqrt(7.*p/rho*((1.+qdyn/p)**(2./7.)-1.))
|
||||
qdyn = p0 * ((1 + rho0 * Vcas * Vcas / (7 * p0)) ** 3.5 - 1.0)
|
||||
Vtas = np.sqrt(7 * p / rho * ((1 + qdyn / p) ** (2 / 7.0) - 1.0))
|
||||
return Vtas
|
||||
|
||||
|
||||
def tas2cas(Vtas, H):
|
||||
"""True Airspeed to Calibrated Airspeed"""
|
||||
p, rho, T = atmos(H)
|
||||
qdyn = p*((1.+rho*Vtas*Vtas/(7.*p))**3.5-1.)
|
||||
Vcas = np.sqrt(7.*p0/rho0*((qdyn/p0+1.)**(2./7.)-1.))
|
||||
qdyn = p * ((1 + rho * Vtas * Vtas / (7 * p)) ** 3.5 - 1.0)
|
||||
Vcas = np.sqrt(7 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2 / 7.0) - 1.0))
|
||||
return Vcas
|
||||
|
||||
|
||||
|
||||
197
pyModeS/extra/rtlreader.py
Normal file
197
pyModeS/extra/rtlreader.py
Normal file
@@ -0,0 +1,197 @@
|
||||
import time
|
||||
import traceback
|
||||
import numpy as np
|
||||
import pyModeS as pms
|
||||
|
||||
try:
|
||||
import rtlsdr # type: ignore
|
||||
except:
|
||||
print("------------------------------------------------------------------------")
|
||||
print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !")
|
||||
print("------------------------------------------------------------------------")
|
||||
|
||||
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.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
|
||||
|
||||
self.exception_queue = None
|
||||
|
||||
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.exception_queue = exception_queue
|
||||
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 self.exception_queue is not None:
|
||||
self.exception_queue.put(tb)
|
||||
raise e
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import signal
|
||||
|
||||
rtl = RtlReader()
|
||||
signal.signal(signal.SIGINT, rtl.stop)
|
||||
|
||||
rtl.debug = True
|
||||
rtl.run()
|
||||
@@ -1,53 +1,51 @@
|
||||
'''
|
||||
Stream beast raw data from a TCP server, convert to mode-s messages
|
||||
'''
|
||||
from __future__ import print_function, division
|
||||
"""Stream beast raw data from a TCP server, convert to mode-s messages."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import time
|
||||
from threading import Thread
|
||||
import pyModeS as pms
|
||||
import traceback
|
||||
import zmq
|
||||
|
||||
if (sys.version_info > (3, 0)):
|
||||
PY_VERSION = 3
|
||||
else:
|
||||
PY_VERSION = 2
|
||||
|
||||
class BaseClient(Thread):
|
||||
def __init__(self, host, port, rawtype):
|
||||
Thread.__init__(self)
|
||||
class TcpClient(object):
|
||||
def __init__(self, host, port, datatype):
|
||||
super(TcpClient, self).__init__()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.buffer = []
|
||||
self.rawtype = rawtype
|
||||
if self.rawtype not in ['avr', 'beast', 'skysense']:
|
||||
print("rawtype must be either avr, beast or skysense")
|
||||
self.socket = None
|
||||
self.datatype = datatype
|
||||
if self.datatype not in ["raw", "beast", "skysense"]:
|
||||
print("datatype must be either raw, beast or skysense")
|
||||
os._exit(1)
|
||||
|
||||
self.raw_pipe_in = None
|
||||
self.stop_flag = False
|
||||
|
||||
self.exception_queue = None
|
||||
|
||||
def connect(self):
|
||||
while True:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(10) # 10 second timeout
|
||||
s.connect((self.host, self.port))
|
||||
print("Server connected - %s:%s" % (self.host, self.port))
|
||||
print("collecting ADS-B messages...")
|
||||
return s
|
||||
except socket.error as err:
|
||||
print("Socket connection error: %s. reconnecting..." % err)
|
||||
time.sleep(3)
|
||||
self.socket = zmq.Context().socket(zmq.STREAM)
|
||||
self.socket.setsockopt(zmq.LINGER, 0)
|
||||
self.socket.setsockopt(zmq.RCVTIMEO, 10000)
|
||||
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
|
||||
|
||||
def stop(self):
|
||||
self.socket.close()
|
||||
|
||||
def read_avr_buffer(self):
|
||||
# -- testing --
|
||||
# for b in self.buffer:
|
||||
# print(chr(b), b)
|
||||
|
||||
# Append message with 0-9,A-F,a-f, until stop sign
|
||||
def read_raw_buffer(self):
|
||||
""" Read raw ADS-B data type.
|
||||
|
||||
String strats with "*" and ends with ";". For example:
|
||||
*5d484ba898f8c6;
|
||||
*8d400cd5990d7e9a10043e5e6da0;
|
||||
*a0001498be800030aa0000c7a75f;
|
||||
"""
|
||||
messages = []
|
||||
|
||||
msg_stop = False
|
||||
self.current_msg = ""
|
||||
for b in self.buffer:
|
||||
if b == 59:
|
||||
msg_stop = True
|
||||
@@ -55,9 +53,9 @@ class BaseClient(Thread):
|
||||
messages.append([self.current_msg, ts])
|
||||
if b == 42:
|
||||
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.buffer = []
|
||||
@@ -65,7 +63,8 @@ class BaseClient(Thread):
|
||||
return messages
|
||||
|
||||
def read_beast_buffer(self):
|
||||
'''
|
||||
"""Handle mode-s beast data type.
|
||||
|
||||
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
|
||||
2 byte Mode-AC
|
||||
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
|
||||
@@ -79,8 +78,7 @@ class BaseClient(Thread):
|
||||
|
||||
timestamp:
|
||||
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
|
||||
'''
|
||||
|
||||
"""
|
||||
messages_mlat = []
|
||||
msg = []
|
||||
i = 0
|
||||
@@ -89,16 +87,16 @@ class BaseClient(Thread):
|
||||
# then, reset the self.buffer with the remainder
|
||||
|
||||
while i < len(self.buffer):
|
||||
if (self.buffer[i:i+2] == [0x1a, 0x1a]):
|
||||
msg.append(0x1a)
|
||||
if self.buffer[i : i + 2] == [0x1A, 0x1A]:
|
||||
msg.append(0x1A)
|
||||
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
|
||||
msg.append(0x1a)
|
||||
elif self.buffer[i] == 0x1a:
|
||||
msg.append(0x1A)
|
||||
elif self.buffer[i] == 0x1A:
|
||||
if i == len(self.buffer) - 1:
|
||||
# special case where the last bit is 0x1a
|
||||
msg.append(0x1a)
|
||||
msg.append(0x1A)
|
||||
elif len(msg) > 0:
|
||||
messages_mlat.append(msg)
|
||||
msg = []
|
||||
@@ -110,12 +108,12 @@ class BaseClient(Thread):
|
||||
if len(msg) > 0:
|
||||
reminder = []
|
||||
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
|
||||
reminder.extend([m, m])
|
||||
else:
|
||||
reminder.append(m)
|
||||
self.buffer = [0x1a] + msg
|
||||
self.buffer = [0x1A] + msg
|
||||
else:
|
||||
self.buffer = []
|
||||
|
||||
@@ -129,67 +127,83 @@ class BaseClient(Thread):
|
||||
|
||||
if msgtype == 0x32:
|
||||
# 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:
|
||||
# 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:
|
||||
# Other message tupe
|
||||
continue
|
||||
|
||||
if len(msg) not in [14, 28]:
|
||||
# incomplete message
|
||||
continue
|
||||
|
||||
df = pms.df(msg)
|
||||
|
||||
# skip incomplete message
|
||||
if df in [0, 4, 5, 11] and len(msg) != 14:
|
||||
continue
|
||||
if df in [16, 17, 18, 19, 20, 21, 24] and len(msg) != 28:
|
||||
continue
|
||||
|
||||
messages.append([msg, ts])
|
||||
return messages
|
||||
|
||||
def read_skysense_buffer(self):
|
||||
"""
|
||||
----------------------------------------------------------------------------------
|
||||
Field SS MS MS MS MS MS MS MS MS MS MS MS MS MS MS TS TS TS TS TS TS RS RS RS
|
||||
----------------------------------------------------------------------------------
|
||||
Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
||||
----------------------------------------------------------------------------------
|
||||
|
||||
SS field - Start character
|
||||
Position 0:
|
||||
1 byte = 8 bits
|
||||
Start character '$'
|
||||
|
||||
MS field - Payload
|
||||
Postion 1 through 14:
|
||||
14 bytes = 112 bits
|
||||
Mode-S payload
|
||||
In case of DF types that only carry 7 bytes of information position 8 through 14 are set to 0x00.
|
||||
|
||||
TS field - Time stamp
|
||||
Position 15 through 20:
|
||||
6 bytes = 48 bits
|
||||
Time stamp with fields as:
|
||||
|
||||
Lock Status - Status of internal time keeping mechanism
|
||||
Equal to 1 if operating normally
|
||||
Bit 47 - 1 bit
|
||||
|
||||
Time of day in UTC seconds, between 0 and 86399
|
||||
Bits 46 through 30 - 17 bits
|
||||
|
||||
Nanoseconds into current second, between 0 and 999999999
|
||||
Bits 29 through 0 - 30 bits
|
||||
|
||||
RS field - Signal Level
|
||||
Position 21 through 23:
|
||||
3 bytes = 24 bits
|
||||
RSSI (received signal strength indication) and relative noise level with fields
|
||||
|
||||
RNL, Q12.4 unsigned fixed point binary with 4 fractional bits and 8 integer bits.
|
||||
This is and indication of the noise level of the message. Roughly 40 counts per 10dBm.
|
||||
Bits 23 through 12 - 12 bits
|
||||
|
||||
RSSI, Q12.4 unsigned fixed point binary with 4 fractional bits and 8 integer bits.
|
||||
This is an indication of the signal level of the received message in ADC counts. Roughly 40 counts per 10dBm.
|
||||
Bits 11 through 0 - 12 bits
|
||||
"""Skysense stream format.
|
||||
|
||||
::
|
||||
|
||||
----------------------------------------------------------------------------------
|
||||
Field SS MS MS MS MS MS MS MS MS MS MS MS MS MS MS TS TS TS TS TS TS RS RS RS
|
||||
----------------------------------------------------------------------------------
|
||||
Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
||||
----------------------------------------------------------------------------------
|
||||
|
||||
SS field - Start character
|
||||
Position 0:
|
||||
1 byte = 8 bits
|
||||
Start character '$'
|
||||
|
||||
MS field - Payload
|
||||
Position 1 through 14:
|
||||
14 bytes = 112 bits
|
||||
Mode-S payload
|
||||
In case of DF types that only carry 7 bytes of information
|
||||
position 8 through 14 are set to 0x00.
|
||||
|
||||
TS field - Time stamp
|
||||
Position 15 through 20:
|
||||
6 bytes = 48 bits
|
||||
Time stamp with fields as:
|
||||
|
||||
Lock Status - Status of internal time keeping mechanism
|
||||
Equal to 1 if operating normally
|
||||
Bit 47 - 1 bit
|
||||
|
||||
Time of day in UTC seconds, between 0 and 86399
|
||||
Bits 46 through 30 - 17 bits
|
||||
|
||||
Nanoseconds into current second, between 0 and 999999999
|
||||
Bits 29 through 0 - 30 bits
|
||||
|
||||
RS field - Signal Level
|
||||
Position 21 through 23:
|
||||
3 bytes = 24 bits
|
||||
RSSI (received signal strength indication) and relative
|
||||
noise level with fields
|
||||
|
||||
RNL, Q12.4 unsigned fixed point binary with 4 fractional
|
||||
bits and 8 integer bits.
|
||||
This is and indication of the noise level of the message.
|
||||
Roughly 40 counts per 10dBm.
|
||||
Bits 23 through 12 - 12 bits
|
||||
|
||||
RSSI, Q12.4 unsigned fixed point binary with 4 fractional
|
||||
bits and 8 integer bits.
|
||||
This is an indication of the signal level of the received
|
||||
message in ADC counts. Roughly 40 counts per 10dBm.
|
||||
Bits 11 through 0 - 12 bits
|
||||
"""
|
||||
SS_MSGLENGTH = 24
|
||||
SS_STARTCHAR = 0x24
|
||||
@@ -200,27 +214,35 @@ class BaseClient(Thread):
|
||||
messages = []
|
||||
while len(self.buffer) > SS_MSGLENGTH:
|
||||
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
|
||||
if (self.buffer[i]>>7):
|
||||
#Long message
|
||||
payload = self.buffer[i:i+14]
|
||||
if self.buffer[i] >> 7:
|
||||
# Long message
|
||||
payload = self.buffer[i : i + 14]
|
||||
else:
|
||||
#Short message
|
||||
payload = self.buffer[i:i+7]
|
||||
msg = ''.join('%02X' % j for j in payload)
|
||||
i += 14 #Both message types use 14 bytes
|
||||
tsbin = self.buffer[i:i+6]
|
||||
sec = ( (tsbin[0] & 0x7f) << 10) | (tsbin[1] << 2 ) | (tsbin[2] >> 6)
|
||||
nano = ( (tsbin[2] & 0x3f) << 24) | (tsbin[3] << 16) | (tsbin[4] << 8) | tsbin[5]
|
||||
ts = sec + nano*1.0e-9
|
||||
# Short message
|
||||
payload = self.buffer[i : i + 7]
|
||||
msg = "".join("%02X" % j for j in payload)
|
||||
i += 14 # Both message types use 14 bytes
|
||||
tsbin = self.buffer[i : i + 6]
|
||||
sec = ((tsbin[0] & 0x7F) << 10) | (tsbin[1] << 2) | (tsbin[2] >> 6)
|
||||
nano = (
|
||||
((tsbin[2] & 0x3F) << 24)
|
||||
| (tsbin[3] << 16)
|
||||
| (tsbin[4] << 8)
|
||||
| tsbin[5]
|
||||
)
|
||||
ts = sec + nano * 1.0e-9
|
||||
i += 6
|
||||
#Signal and noise level - Don't care for now
|
||||
# Signal and noise level - Don't care for now
|
||||
i += 3
|
||||
self.buffer = self.buffer[SS_MSGLENGTH:]
|
||||
messages.append( [msg,ts] )
|
||||
messages.append([msg, ts])
|
||||
else:
|
||||
self.buffer = self.buffer[1:]
|
||||
self.buffer = self.buffer[1:]
|
||||
return messages
|
||||
|
||||
def handle_messages(self, messages):
|
||||
@@ -228,29 +250,24 @@ class BaseClient(Thread):
|
||||
for msg, t in messages:
|
||||
print("%15.9f %s" % (t, msg))
|
||||
|
||||
def run(self):
|
||||
sock = self.connect()
|
||||
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
|
||||
self.raw_pipe_in = raw_pipe_in
|
||||
self.exception_queue = exception_queue
|
||||
self.stop_flag = stop_flag
|
||||
self.connect()
|
||||
|
||||
while True:
|
||||
try:
|
||||
received = sock.recv(1024)
|
||||
|
||||
if PY_VERSION == 2:
|
||||
received = [ord(i) for i in received]
|
||||
received = [i for i in self.socket.recv(4096)]
|
||||
|
||||
self.buffer.extend(received)
|
||||
# print(''.join(x.encode('hex') for x in self.buffer))
|
||||
|
||||
# process self.buffer when it is longer enough
|
||||
# if len(self.buffer) < 2048:
|
||||
# continue
|
||||
# -- Removed!! Cause delay in low data rate scenario --
|
||||
|
||||
if self.rawtype == 'beast':
|
||||
if self.datatype == "beast":
|
||||
messages = self.read_beast_buffer()
|
||||
elif self.rawtype == 'avr':
|
||||
messages = self.read_avr_buffer()
|
||||
elif self.rawtype == 'skysense':
|
||||
elif self.datatype == "raw":
|
||||
messages = self.read_raw_buffer()
|
||||
elif self.datatype == "skysense":
|
||||
messages = self.read_skysense_buffer()
|
||||
|
||||
if not messages:
|
||||
@@ -258,21 +275,24 @@ class BaseClient(Thread):
|
||||
else:
|
||||
self.handle_messages(messages)
|
||||
|
||||
time.sleep(0.001)
|
||||
# raise RuntimeError("test exception")
|
||||
|
||||
except zmq.error.Again:
|
||||
continue
|
||||
except Exception as e:
|
||||
print("Unexpected Error:", e)
|
||||
|
||||
try:
|
||||
sock = self.connect()
|
||||
except Exception as e:
|
||||
print("Unexpected Error:", e)
|
||||
tb = traceback.format_exc()
|
||||
if self.exception_queue is not None:
|
||||
self.exception_queue.put(tb)
|
||||
raise e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
# for testing purpose only
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
rawtype = sys.argv[3]
|
||||
client = BaseClient(host=host, port=port, rawtype=rawtype)
|
||||
client.daemon = True
|
||||
client.run()
|
||||
datatype = sys.argv[3]
|
||||
client = TcpClient(host=host, port=port, datatype=datatype)
|
||||
try:
|
||||
client.run()
|
||||
finally:
|
||||
client.stop()
|
||||
|
||||
0
pyModeS/py.typed
Normal file
0
pyModeS/py.typed
Normal file
488
pyModeS/py_common.py
Normal file
488
pyModeS/py_common.py
Normal file
@@ -0,0 +1,488 @@
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from textwrap import wrap
|
||||
|
||||
|
||||
def hex2bin(hexstr: str) -> str:
|
||||
"""Convert a hexadecimal string to binary string, with zero fillings."""
|
||||
num_of_bits = len(hexstr) * 4
|
||||
binstr = bin(int(hexstr, 16))[2:].zfill(int(num_of_bits))
|
||||
return binstr
|
||||
|
||||
|
||||
def hex2int(hexstr: str) -> int:
|
||||
"""Convert a hexadecimal string to integer."""
|
||||
return int(hexstr, 16)
|
||||
|
||||
|
||||
def bin2int(binstr: str) -> int:
|
||||
"""Convert a binary string to integer."""
|
||||
return int(binstr, 2)
|
||||
|
||||
|
||||
def bin2hex(binstr: str) -> str:
|
||||
"""Convert a binary string to hexadecimal string."""
|
||||
return "{0:X}".format(int(binstr, 2))
|
||||
|
||||
|
||||
def df(msg: str) -> int:
|
||||
"""Decode Downlink Format value, bits 1 to 5."""
|
||||
dfbin = hex2bin(msg[:2])
|
||||
return min(bin2int(dfbin[0:5]), 24)
|
||||
|
||||
|
||||
def crc(msg: str, encode: bool = False) -> int:
|
||||
"""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: 28 bytes hexadecimal message string
|
||||
encode: 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)]
|
||||
|
||||
if encode:
|
||||
msg = msg[:-6] + "000000"
|
||||
|
||||
msgbin = hex2bin(msg)
|
||||
msgbin_split = wrap(msgbin, 8)
|
||||
mbytes = list(map(bin2int, msgbin_split))
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
result = (mbytes[-3] << 16) | (mbytes[-2] << 8) | mbytes[-1]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def crc_legacy(msg: str, encode: bool = False) -> int:
|
||||
"""Mode-S Cyclic Redundancy Check. (Legacy code, 2x slow)."""
|
||||
# the polynominal generattor code for CRC [1111111111111010000001001]
|
||||
generator = np.array(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1]
|
||||
)
|
||||
ng = len(generator)
|
||||
|
||||
msgnpbin = np.array([int(i) for i in hex2bin(msg)])
|
||||
|
||||
if encode:
|
||||
msgnpbin[-24:] = [0] * 24
|
||||
|
||||
# loop all bits, except last 24 piraty bits
|
||||
for i in range(len(msgnpbin) - 24):
|
||||
if msgnpbin[i] == 0:
|
||||
continue
|
||||
|
||||
# perform XOR, when 1
|
||||
msgnpbin[i : i + ng] = np.bitwise_xor(msgnpbin[i : i + ng], generator)
|
||||
|
||||
# last 24 bits
|
||||
msgbin = np.array2string(msgnpbin[-24:], separator="")[1:-1]
|
||||
reminder = bin2int(msgbin)
|
||||
|
||||
return reminder
|
||||
|
||||
|
||||
def floor(x: float) -> int:
|
||||
"""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 int(np.floor(x))
|
||||
|
||||
|
||||
def icao(msg: str) -> Optional[str]:
|
||||
"""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
|
||||
|
||||
"""
|
||||
addr: Optional[str]
|
||||
DF = df(msg)
|
||||
|
||||
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 = int(msg[-6:], 16)
|
||||
addr = "%06X" % (c0 ^ c1)
|
||||
else:
|
||||
addr = None
|
||||
|
||||
return addr
|
||||
|
||||
|
||||
def is_icao_assigned(icao: str) -> bool:
|
||||
"""Check whether the ICAO address is assigned (Annex 10, Vol 3)."""
|
||||
if (icao is None) or (not isinstance(icao, str)) or (len(icao) != 6):
|
||||
return False
|
||||
|
||||
icaoint = int(icao, 16)
|
||||
|
||||
if 0x200000 < icaoint < 0x27FFFF:
|
||||
return False # AFI
|
||||
if 0x280000 < icaoint < 0x28FFFF:
|
||||
return False # SAM
|
||||
if 0x500000 < icaoint < 0x5FFFFF:
|
||||
return False # EUR, NAT
|
||||
if 0x600000 < icaoint < 0x67FFFF:
|
||||
return False # MID
|
||||
if 0x680000 < icaoint < 0x6F0000:
|
||||
return False # ASIA
|
||||
if 0x900000 < icaoint < 0x9FFFFF:
|
||||
return False # NAM, PAC
|
||||
if 0xB00000 < icaoint < 0xBFFFFF:
|
||||
return False # CAR
|
||||
if 0xD00000 < icaoint < 0xDFFFFF:
|
||||
return False # future
|
||||
if 0xF00000 < icaoint < 0xFFFFFF:
|
||||
return False # future
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def typecode(msg: str) -> Optional[int]:
|
||||
"""Type code of ADS-B message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: type code number
|
||||
"""
|
||||
if df(msg) not in (17, 18):
|
||||
return None
|
||||
|
||||
tcbin = hex2bin(msg[8:10])
|
||||
return bin2int(tcbin[0:5])
|
||||
|
||||
|
||||
def cprNL(lat: float) -> int:
|
||||
"""NL() function in CPR decoding."""
|
||||
|
||||
if np.isclose(lat, 0):
|
||||
return 59
|
||||
elif np.isclose(abs(lat), 87):
|
||||
return 2
|
||||
elif lat > 87 or lat < -87:
|
||||
return 1
|
||||
|
||||
nz = 15
|
||||
a = 1 - np.cos(np.pi / (2 * nz))
|
||||
b = np.cos(np.pi / 180 * abs(lat)) ** 2
|
||||
nl = 2 * np.pi / (np.arccos(1 - a / b))
|
||||
NL = floor(nl)
|
||||
return NL
|
||||
|
||||
|
||||
def idcode(msg: str) -> str:
|
||||
"""Compute identity code (squawk) encoded in DF5 or DF21 message.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: squawk code
|
||||
|
||||
"""
|
||||
if df(msg) not in [5, 21]:
|
||||
raise RuntimeError("Message must be Downlink Format 5 or 21.")
|
||||
|
||||
mbin = hex2bin(msg)
|
||||
idcodebin = mbin[19:32]
|
||||
|
||||
return squawk(idcodebin)
|
||||
|
||||
|
||||
def squawk(binstr: str) -> str:
|
||||
"""Decode 13 bits identity (squawk) code.
|
||||
|
||||
Args:
|
||||
binstr (String): 13 bits binary string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
|
||||
"""
|
||||
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
|
||||
raise RuntimeError("Input must be 13 bits binary string")
|
||||
|
||||
C1 = binstr[0]
|
||||
A1 = binstr[1]
|
||||
C2 = binstr[2]
|
||||
A2 = binstr[3]
|
||||
C4 = binstr[4]
|
||||
A4 = binstr[5]
|
||||
# X = binstr[6]
|
||||
B1 = binstr[7]
|
||||
D1 = binstr[8]
|
||||
B2 = binstr[9]
|
||||
D2 = binstr[10]
|
||||
B4 = binstr[11]
|
||||
D4 = binstr[12]
|
||||
|
||||
byte1 = int(A4 + A2 + A1, 2)
|
||||
byte2 = int(B4 + B2 + B1, 2)
|
||||
byte3 = int(C4 + C2 + C1, 2)
|
||||
byte4 = int(D4 + D2 + D1, 2)
|
||||
|
||||
return str(byte1) + str(byte2) + str(byte3) + str(byte4)
|
||||
|
||||
|
||||
def altcode(msg: str) -> Optional[int]:
|
||||
"""Compute altitude encoded in DF4 or DF20 message.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
|
||||
"""
|
||||
alt: Optional[int]
|
||||
|
||||
if df(msg) not in [0, 4, 16, 20]:
|
||||
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
|
||||
|
||||
# Altitude code, bit 20-32
|
||||
mbin = hex2bin(msg)
|
||||
|
||||
altitude_code = mbin[19:32]
|
||||
|
||||
alt = altitude(altitude_code)
|
||||
|
||||
return alt
|
||||
|
||||
|
||||
def altitude(binstr: str) -> Optional[int]:
|
||||
"""Decode 13 bits altitude code.
|
||||
|
||||
Args:
|
||||
binstr (String): 13 bits binary string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
|
||||
"""
|
||||
alt: Optional[int]
|
||||
|
||||
if len(binstr) != 13 or not set(binstr).issubset(set("01")):
|
||||
raise RuntimeError("Input must be 13 bits binary string")
|
||||
|
||||
Mbit = binstr[6]
|
||||
Qbit = binstr[8]
|
||||
|
||||
if bin2int(binstr) == 0:
|
||||
# altitude unknown or invalid
|
||||
alt = None
|
||||
|
||||
elif Mbit == "0": # unit in ft
|
||||
if Qbit == "1": # 25ft interval
|
||||
vbin = binstr[:6] + binstr[7] + binstr[9:]
|
||||
alt = bin2int(vbin) * 25 - 1000
|
||||
if Qbit == "0": # 100ft interval, above 50187.5ft
|
||||
C1 = binstr[0]
|
||||
A1 = binstr[1]
|
||||
C2 = binstr[2]
|
||||
A2 = binstr[3]
|
||||
C4 = binstr[4]
|
||||
A4 = binstr[5]
|
||||
# M = binstr[6]
|
||||
B1 = binstr[7]
|
||||
# Q = binstr[8]
|
||||
B2 = binstr[9]
|
||||
D2 = binstr[10]
|
||||
B4 = binstr[11]
|
||||
D4 = binstr[12]
|
||||
|
||||
graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
|
||||
alt = gray2alt(graystr)
|
||||
|
||||
if Mbit == "1": # unit in meter
|
||||
vbin = binstr[:6] + binstr[7:]
|
||||
alt = int(bin2int(vbin) * 3.28084) # convert to ft
|
||||
|
||||
return alt
|
||||
|
||||
|
||||
def gray2alt(binstr: str) -> Optional[int]:
|
||||
gc500 = binstr[:8]
|
||||
n500 = gray2int(gc500)
|
||||
|
||||
# in 100-ft step must be converted first
|
||||
gc100 = binstr[8:]
|
||||
n100 = gray2int(gc100)
|
||||
|
||||
if n100 in [0, 5, 6]:
|
||||
return None
|
||||
|
||||
if n100 == 7:
|
||||
n100 = 5
|
||||
|
||||
if n500 % 2:
|
||||
n100 = 6 - n100
|
||||
|
||||
alt = (n500 * 500 + n100 * 100) - 1300
|
||||
return alt
|
||||
|
||||
|
||||
def gray2int(binstr: str) -> int:
|
||||
"""Convert greycode to binary."""
|
||||
num = bin2int(binstr)
|
||||
num ^= num >> 8
|
||||
num ^= num >> 4
|
||||
num ^= num >> 2
|
||||
num ^= num >> 1
|
||||
return num
|
||||
|
||||
|
||||
def data(msg: str) -> str:
|
||||
"""Return the data frame in the message, bytes 9 to 22."""
|
||||
return msg[8:-6]
|
||||
|
||||
|
||||
def allzeros(msg: str) -> bool:
|
||||
"""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: str, sb: int, msb: int, lsb: int) -> bool:
|
||||
"""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
|
||||
|
||||
|
||||
def fs(msg):
|
||||
"""Decode flight status for DF 4, 5, 20, and 21.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: flight status, description
|
||||
|
||||
"""
|
||||
msgbin = hex2bin(msg)
|
||||
fs = bin2int(msgbin[5:8])
|
||||
text = None
|
||||
|
||||
if fs == 0:
|
||||
text = "no alert, no SPI, aircraft is airborne"
|
||||
elif fs == 1:
|
||||
text = "no alert, no SPI, aircraft is on-ground"
|
||||
elif fs == 2:
|
||||
text = "alert, no SPI, aircraft is airborne"
|
||||
elif fs == 3:
|
||||
text = "alert, no SPI, aircraft is on-ground"
|
||||
elif fs == 4:
|
||||
text = "alert, SPI, aircraft is airborne or on-ground"
|
||||
elif fs == 5:
|
||||
text = "no alert, SPI, aircraft is airborne or on-ground"
|
||||
|
||||
return fs, text
|
||||
|
||||
|
||||
def dr(msg):
|
||||
"""Decode downlink request for DF 4, 5, 20, and 21.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: downlink request, description
|
||||
|
||||
"""
|
||||
msgbin = hex2bin(msg)
|
||||
dr = bin2int(msgbin[8:13])
|
||||
|
||||
text = None
|
||||
|
||||
if dr == 0:
|
||||
text = "no downlink request"
|
||||
elif dr == 1:
|
||||
text = "request to send Comm-B message"
|
||||
elif dr == 4:
|
||||
text = "Comm-B broadcast 1 available"
|
||||
elif dr == 5:
|
||||
text = "Comm-B broadcast 2 available"
|
||||
elif dr >= 16:
|
||||
text = "ELM downlink segments available: {}".format(dr - 15)
|
||||
|
||||
return dr, text
|
||||
|
||||
|
||||
def um(msg):
|
||||
"""Decode utility message for DF 4, 5, 20, and 21.
|
||||
|
||||
Utility message contains interrogator identifier and reservation type.
|
||||
|
||||
Args:
|
||||
msg (str): 14 hexdigits string
|
||||
Returns:
|
||||
int, str: interrogator identifier code that triggered the reply, and
|
||||
reservation type made by the interrogator
|
||||
"""
|
||||
msgbin = hex2bin(msg)
|
||||
iis = bin2int(msgbin[13:17])
|
||||
ids = bin2int(msgbin[17:19])
|
||||
if ids == 0:
|
||||
ids_text = None
|
||||
if ids == 1:
|
||||
ids_text = "Comm-B interrogator identifier code"
|
||||
if ids == 2:
|
||||
ids_text = "Comm-C interrogator identifier code"
|
||||
if ids == 3:
|
||||
ids_text = "Comm-D interrogator identifier code"
|
||||
return iis, ids, ids_text
|
||||
292
pyModeS/streamer/decode.py
Normal file
292
pyModeS/streamer/decode.py
Normal file
@@ -0,0 +1,292 @@
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
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
|
||||
output_buffer.append([t, icao, "ias60", ias60])
|
||||
if hdg60:
|
||||
self.acs[icao]["hdg"] = hdg60
|
||||
output_buffer.append([t, icao, "hdg60", hdg60])
|
||||
if mach60:
|
||||
self.acs[icao]["mach"] = mach60
|
||||
output_buffer.append([t, icao, "mach60", 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))
|
||||
@@ -1,120 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import curses
|
||||
from threading import Lock
|
||||
import pyModeS as pms
|
||||
from pyModeS.extra.tcpclient import BaseClient
|
||||
from pyModeS.streamer.stream import Stream
|
||||
import signal
|
||||
import multiprocessing
|
||||
from pyModeS.streamer.decode import Decode
|
||||
from pyModeS.streamer.screen import Screen
|
||||
from pyModeS.streamer.source import NetSource, RtlSdrSource
|
||||
|
||||
LOCK = Lock()
|
||||
ADSB_MSG = []
|
||||
ADSB_TS = []
|
||||
COMMB_MSG = []
|
||||
COMMB_TS = []
|
||||
|
||||
support_rawtypes = ["raw", "beast", "skysense"]
|
||||
|
||||
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)
|
||||
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: {}".format(
|
||||
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()
|
||||
|
||||
SERVER = args.server
|
||||
PORT = int(args.port)
|
||||
RAWTYPE = args.rawtype
|
||||
LAT0 = float(args.latlon[0])
|
||||
LON0 = float(args.latlon[1])
|
||||
SOURCE = args.source
|
||||
LATLON = args.latlon
|
||||
UNCERTAINTY = args.uncertainty
|
||||
DUMPTO = args.dumpto
|
||||
|
||||
if SOURCE == "rtlsdr":
|
||||
pass
|
||||
elif SOURCE == "net":
|
||||
if args.connect is None:
|
||||
print("Error: --connect argument must not be empty.")
|
||||
else:
|
||||
SERVER, PORT, DATATYPE = args.connect
|
||||
if DATATYPE not in support_rawtypes:
|
||||
print("Data type not supported, available ones are %s" % support_rawtypes)
|
||||
|
||||
else:
|
||||
print('Source must be "rtlsdr" or "net".')
|
||||
sys.exit(1)
|
||||
|
||||
if DUMPTO is not None:
|
||||
# append to current folder except root is given
|
||||
if DUMPTO[0] != '/':
|
||||
DUMPTO = os.getcwd() + '/' + DUMPTO
|
||||
if DUMPTO[0] != "/":
|
||||
DUMPTO = os.getcwd() + "/" + DUMPTO
|
||||
|
||||
if not os.path.isdir(DUMPTO):
|
||||
print('Error: dump folder (%s) does not exist' % DUMPTO)
|
||||
print("Error: dump folder (%s) does not exist" % DUMPTO)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ModesClient(BaseClient):
|
||||
def __init__(self, host, port, rawtype):
|
||||
super(ModesClient, self).__init__(host, port, rawtype)
|
||||
|
||||
def handle_messages(self, messages):
|
||||
local_buffer_adsb_msg = []
|
||||
local_buffer_adsb_ts = []
|
||||
local_buffer_ehs_msg = []
|
||||
local_buffer_ehs_ts = []
|
||||
|
||||
for msg, t in messages:
|
||||
if len(msg) < 28: # only process long messages
|
||||
continue
|
||||
|
||||
df = pms.df(msg)
|
||||
|
||||
if df == 17 or df == 18:
|
||||
local_buffer_adsb_msg.append(msg)
|
||||
local_buffer_adsb_ts.append(t)
|
||||
elif df == 20 or df == 21:
|
||||
local_buffer_ehs_msg.append(msg)
|
||||
local_buffer_ehs_ts.append(t)
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
LOCK.acquire()
|
||||
ADSB_MSG.extend(local_buffer_adsb_msg)
|
||||
ADSB_TS.extend(local_buffer_adsb_ts)
|
||||
COMMB_MSG.extend(local_buffer_ehs_msg)
|
||||
COMMB_TS.extend(local_buffer_ehs_ts)
|
||||
LOCK.release()
|
||||
|
||||
|
||||
# redirect all stdout to null, avoiding messing up with the screen
|
||||
sys.stdout = open(os.devnull, 'w')
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
client = ModesClient(host=SERVER, port=PORT, rawtype=RAWTYPE)
|
||||
client.daemon = True
|
||||
client.start()
|
||||
|
||||
stream = Stream(lat0=LAT0, lon0=LON0, dumpto=DUMPTO)
|
||||
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)
|
||||
|
||||
try:
|
||||
screen = Screen(uncertainty=UNCERTAINTY)
|
||||
screen.daemon = True
|
||||
screen.start()
|
||||
if SOURCE == "net":
|
||||
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
|
||||
elif SOURCE == "rtlsdr":
|
||||
source = RtlSdrSource()
|
||||
|
||||
while True:
|
||||
if len(ADSB_MSG) > 200:
|
||||
LOCK.acquire()
|
||||
stream.process_raw(ADSB_TS, ADSB_MSG, COMMB_TS, COMMB_MSG)
|
||||
ADSB_MSG = []
|
||||
ADSB_TS = []
|
||||
COMMB_MSG = []
|
||||
COMMB_TS = []
|
||||
LOCK.release()
|
||||
|
||||
acs = stream.get_aircraft()
|
||||
try:
|
||||
screen.update_data(acs)
|
||||
screen.update()
|
||||
time.sleep(0.02)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
continue
|
||||
recv_process = multiprocessing.Process(
|
||||
target=source.run, args=(raw_pipe_in, stop_flag, exception_queue)
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import curses
|
||||
import numpy as np
|
||||
import time
|
||||
from threading import Thread
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
COLUMNS = [
|
||||
('call', 10),
|
||||
('lat', 10),
|
||||
('lon', 10),
|
||||
('alt', 7),
|
||||
('gs', 5),
|
||||
('tas', 5),
|
||||
('ias', 5),
|
||||
('mach', 7),
|
||||
('roc', 7),
|
||||
('trk', 10),
|
||||
('hdg', 10),
|
||||
('live', 6),
|
||||
("call", 10),
|
||||
("lat", 10),
|
||||
("lon", 10),
|
||||
("alt", 7),
|
||||
("gs", 5),
|
||||
("tas", 5),
|
||||
("ias", 5),
|
||||
("mach", 7),
|
||||
("roc", 7),
|
||||
("trk", 10),
|
||||
("hdg", 10),
|
||||
("live", 6),
|
||||
]
|
||||
|
||||
UNCERTAINTY_COLUMNS = [
|
||||
('|', 5),
|
||||
('ver', 4),
|
||||
('HPL', 5),
|
||||
('RCu', 5),
|
||||
('RCv', 5),
|
||||
('HVE', 5),
|
||||
('VVE', 5),
|
||||
('Rc', 4),
|
||||
('VPL', 5),
|
||||
('EPU', 5),
|
||||
('VEPU', 6),
|
||||
('HFOMr', 7),
|
||||
('VFOMr', 7),
|
||||
('PE_RCu', 8),
|
||||
('PE_VPL', 8),
|
||||
("|", 5),
|
||||
("ver", 4),
|
||||
("HPL", 5),
|
||||
("RCu", 5),
|
||||
("RCv", 5),
|
||||
("HVE", 5),
|
||||
("VVE", 5),
|
||||
("Rc", 4),
|
||||
("VPL", 5),
|
||||
("EPU", 5),
|
||||
("VEPU", 6),
|
||||
("HFOMr", 7),
|
||||
("VFOMr", 7),
|
||||
("PE_RCu", 8),
|
||||
("PE_VPL", 8),
|
||||
]
|
||||
|
||||
class Screen(Thread):
|
||||
|
||||
class Screen(object):
|
||||
def __init__(self, uncertainty=False):
|
||||
Thread.__init__(self)
|
||||
super(Screen, self).__init__()
|
||||
self.screen = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.mousemask(1)
|
||||
@@ -55,16 +55,20 @@ class Screen(Thread):
|
||||
if uncertainty:
|
||||
self.columns.extend(UNCERTAINTY_COLUMNS)
|
||||
|
||||
|
||||
def reset_cursor_pos(self):
|
||||
self.screen.move(self.y, self.x)
|
||||
|
||||
def update_data(self, acs):
|
||||
def update_ac(self, acs):
|
||||
self.acs = acs
|
||||
|
||||
def draw_frame(self):
|
||||
self.screen.border(0)
|
||||
self.screen.addstr(0, 2, "Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)" % len(self.acs))
|
||||
self.screen.addstr(
|
||||
0,
|
||||
2,
|
||||
"Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)"
|
||||
% len(self.acs),
|
||||
)
|
||||
|
||||
def update(self):
|
||||
if len(self.acs) == 0:
|
||||
@@ -81,21 +85,20 @@ class Screen(Thread):
|
||||
|
||||
row = 1
|
||||
|
||||
header = ' icao'
|
||||
header = " icao"
|
||||
for c, cw in self.columns:
|
||||
header += (cw-len(c))*' ' + c
|
||||
header += (cw - len(c)) * " " + c
|
||||
|
||||
# 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:
|
||||
header = header[:self.scr_w-3] + '>'
|
||||
|
||||
header = header[: self.scr_w - 3] + ">"
|
||||
|
||||
self.screen.addstr(row, 1, header)
|
||||
|
||||
row +=1
|
||||
self.screen.addstr(row, 1, '-'*(self.scr_w-2))
|
||||
row += 1
|
||||
self.screen.addstr(row, 1, "-" * (self.scr_w - 2))
|
||||
|
||||
icaos = np.array(list(self.acs.keys()))
|
||||
icaos = np.sort(icaos)
|
||||
@@ -105,10 +108,10 @@ class Screen(Thread):
|
||||
idx = row + self.offset - 3
|
||||
|
||||
if idx > len(icaos) - 1:
|
||||
line = ' '*(self.scr_w-2)
|
||||
line = " " * (self.scr_w - 2)
|
||||
|
||||
else:
|
||||
line = ''
|
||||
line = ""
|
||||
|
||||
icao = icaos[idx]
|
||||
ac = self.acs[icao]
|
||||
@@ -116,22 +119,22 @@ class Screen(Thread):
|
||||
line += icao
|
||||
|
||||
for c, cw in self.columns:
|
||||
if c=='|':
|
||||
val = '|'
|
||||
elif c=='live':
|
||||
val = str(int(time.time() - ac[c]))+'s'
|
||||
if c == "|":
|
||||
val = "|"
|
||||
elif c == "live":
|
||||
val = str(ac[c] - int(time.time())) + "s"
|
||||
elif ac[c] is None:
|
||||
val = ''
|
||||
val = ""
|
||||
else:
|
||||
val = ac[c]
|
||||
val_str = str(val)
|
||||
line += (cw-len(val_str))*' ' + val_str
|
||||
line += (cw - len(val_str)) * " " + val_str
|
||||
|
||||
# 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:
|
||||
line = line[:self.scr_w-3] + '>'
|
||||
line = line[: self.scr_w - 3] + ">"
|
||||
|
||||
if (icao is not None) and (self.lock_icao == icao):
|
||||
self.screen.addstr(row, 1, line, curses.A_STANDOUT)
|
||||
@@ -140,15 +143,15 @@ class Screen(Thread):
|
||||
else:
|
||||
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
|
||||
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()
|
||||
|
||||
def run(self):
|
||||
def kye_handling(self):
|
||||
self.draw_frame()
|
||||
self.scr_h, self.scr_w = self.screen.getmaxyx()
|
||||
|
||||
@@ -168,7 +171,7 @@ class Screen(Thread):
|
||||
self.offset = offset_intent
|
||||
else:
|
||||
self.offset = 0
|
||||
elif c == curses.KEY_DOWN :
|
||||
elif c == curses.KEY_DOWN:
|
||||
y_intent = self.y + 1
|
||||
if y_intent < self.scr_h - 3:
|
||||
self.y = y_intent
|
||||
@@ -178,6 +181,38 @@ class Screen(Thread):
|
||||
self.y = y_intent
|
||||
elif c == curses.KEY_ENTER or c == 10 or c == 13:
|
||||
self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode()
|
||||
elif c == 27: # escape key
|
||||
self.lock_icao = None
|
||||
elif c == curses.KEY_F5:
|
||||
self.screen.refresh()
|
||||
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)
|
||||
|
||||
91
pyModeS/streamer/source.py
Normal file
91
pyModeS/streamer/source.py
Normal 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()
|
||||
@@ -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
|
||||
3
setup.cfg
Normal file
3
setup.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://github.com/embray/setup.cfg
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
170
setup.py
170
setup.py
@@ -4,16 +4,18 @@ See:
|
||||
https://packaging.python.org/en/latest/distributing.html
|
||||
https://github.com/pypa/sampleproject
|
||||
|
||||
Steps for deploying a new verison:
|
||||
Steps for deploying a new version:
|
||||
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
|
||||
run: python setup.py bdist_wheel --universal
|
||||
4. twine upload dist/*
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from os import path
|
||||
@@ -21,103 +23,79 @@ from os import path
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||
with open(path.join(here, "README.rst"), encoding="utf-8") as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='pyModeS',
|
||||
|
||||
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||
# the version across setup.py and the project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version='2.0',
|
||||
|
||||
description='Python ADS-B/Mode-S Decoder',
|
||||
details = dict(
|
||||
name="pyModeS",
|
||||
version="2.11",
|
||||
description="Python Mode-S and ADS-B Decoder",
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
url='https://github.com/junzis/pyModes',
|
||||
|
||||
# Author details
|
||||
author='Junzi Sun',
|
||||
author_email='j.sun-1@tudelft.nl',
|
||||
|
||||
# Choose your license
|
||||
license='GNU GPL v3',
|
||||
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
url="https://github.com/junzis/pyModeS",
|
||||
author="Junzi Sun",
|
||||
author_email="j.sun-1@tudelft.nl",
|
||||
license="GNU GPL v3",
|
||||
classifiers=[
|
||||
# How mature is this project? Common values are
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 3 - Alpha',
|
||||
|
||||
# Indicate who your project is intended for
|
||||
'Intended Audience :: Developers',
|
||||
'Topic :: Software Development :: Build Tools',
|
||||
|
||||
# Pick your license as you wish (should match "license" above)
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
],
|
||||
|
||||
# What does your project relate to?
|
||||
keywords='Mode-S ADS-B EHS decoding',
|
||||
|
||||
# You can just specify the packages manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||
|
||||
# Alternatively, if you want to distribute just a my_module.py, uncomment
|
||||
# this:
|
||||
# py_modules=["my_module"],
|
||||
|
||||
# List run-time dependencies here. These will be installed by pip when
|
||||
# your project is installed. For an analysis of "install_requires" vs pip's
|
||||
# requirements files see:
|
||||
# https://packaging.python.org/en/latest/requirements.html
|
||||
install_requires=['numpy', 'argparse'],
|
||||
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
# dependencies). You can install these using the following syntax,
|
||||
# for example:
|
||||
# $ pip install -e .[dev,test]
|
||||
# extras_require={
|
||||
# 'dev': ['check-manifest'],
|
||||
# 'test': ['coverage'],
|
||||
# },
|
||||
|
||||
# If there are data files included in your packages that need to be
|
||||
# installed, specify them here. If using Python 2.6 or less, then these
|
||||
# have to be included in MANIFEST.in as well.
|
||||
# package_data={
|
||||
# 'sample': ['package_data.dat'],
|
||||
# },
|
||||
|
||||
# Although 'package_data' is the preferred approach, in some case you may
|
||||
# need to place data files outside of your packages. See:
|
||||
# http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
|
||||
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
|
||||
# data_files=[('my_data', ['data/data_file'])],
|
||||
|
||||
# To provide executable scripts, use entry points in preference to the
|
||||
# "scripts" keyword. Entry points provide cross-platform support and allow
|
||||
# pip to create the appropriate form of executable for the target platform.
|
||||
# entry_points={
|
||||
# 'console_scripts': [
|
||||
# 'sample=sample:main',
|
||||
# ],
|
||||
# },
|
||||
|
||||
scripts=['pyModeS/streamer/modeslive'],
|
||||
keywords="Mode-S ADS-B EHS ELS Comm-B",
|
||||
packages=find_packages(exclude=["contrib", "docs", "tests"]),
|
||||
# typing_extensions are no longer necessary after Python 3.8 (TypedDict)
|
||||
install_requires=["numpy", "pyzmq", "typing_extensions"],
|
||||
extras_require={"fast": ["Cython"]},
|
||||
package_data={
|
||||
"pyModeS": ["*.pyx", "*.pxd", "py.typed"],
|
||||
"pyModeS.decoder.flarm": ["*.pyx", "*.pxd", "*.pyi"],
|
||||
},
|
||||
scripts=["pyModeS/streamer/modeslive"],
|
||||
)
|
||||
|
||||
try:
|
||||
from distutils.core import Extension
|
||||
from Cython.Build import cythonize
|
||||
|
||||
compile_args = []
|
||||
include_dirs = ["pyModeS/decoder/flarm"]
|
||||
|
||||
if sys.platform == "linux":
|
||||
compile_args += [
|
||||
"-march=native",
|
||||
"-O3",
|
||||
"-msse",
|
||||
"-msse2",
|
||||
"-mfma",
|
||||
"-mfpmath=sse",
|
||||
"-Wno-pointer-sign",
|
||||
]
|
||||
|
||||
extensions = [
|
||||
Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"]),
|
||||
Extension(
|
||||
"pyModeS.decoder.flarm.decode",
|
||||
[
|
||||
"pyModeS/decoder/flarm/decode.pyx",
|
||||
"pyModeS/decoder/flarm/core.c",
|
||||
],
|
||||
extra_compile_args=compile_args,
|
||||
include_dirs=include_dirs,
|
||||
),
|
||||
]
|
||||
|
||||
setup(
|
||||
**dict(
|
||||
details,
|
||||
ext_modules=cythonize(
|
||||
extensions,
|
||||
include_path=include_dirs,
|
||||
compiler_directives={"binding": True, "language_level": 3},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
except ImportError:
|
||||
setup(**details)
|
||||
|
||||
130
tests/benchmark.py
Normal file
130
tests/benchmark.py
Normal 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
|
||||
@@ -1,41 +1,42 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import adsb, ehs
|
||||
import csv
|
||||
import time
|
||||
|
||||
from pyModeS.decoder import adsb
|
||||
|
||||
print("===== Decode ADS-B sample data=====")
|
||||
|
||||
f = open("tests/data/sample_data_adsb.csv", "rt")
|
||||
|
||||
msg0 = None
|
||||
msg1 = None
|
||||
|
||||
tstart = time.time()
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
|
||||
ts = int(r[0])
|
||||
m = r[1].encode()
|
||||
|
||||
icao = adsb.icao(m)
|
||||
tc = adsb.typecode(m)
|
||||
|
||||
if 1 <= tc <= 4:
|
||||
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
|
||||
if tc == 19:
|
||||
print(ts, m, icao, tc, adsb.velocity(m))
|
||||
if 5 <= tc <= 18:
|
||||
if adsb.oe_flag(m):
|
||||
msg1 = m
|
||||
t1 = ts
|
||||
else:
|
||||
msg0 = m
|
||||
t0 = ts
|
||||
|
||||
if msg0 and msg1:
|
||||
pos = adsb.position(msg0, msg1, t0, t1)
|
||||
alt = adsb.altitude(m)
|
||||
print(ts, m, icao, tc, pos, alt)
|
||||
|
||||
|
||||
# === Decode sample data file ===
|
||||
dt = time.time() - tstart
|
||||
|
||||
def adsb_decode_all(n=None):
|
||||
print("===== Decode ADS-B sample data=====")
|
||||
import csv
|
||||
f = open('tests/data/sample_data_adsb.csv', 'rt')
|
||||
|
||||
msg0 = None
|
||||
msg1 = None
|
||||
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
if n and i > n:
|
||||
break
|
||||
|
||||
ts = r[0]
|
||||
m = r[1]
|
||||
icao = adsb.icao(m)
|
||||
tc = adsb.typecode(m)
|
||||
if 1 <= tc <= 4:
|
||||
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
|
||||
if tc == 19:
|
||||
print(ts, m, icao, tc, adsb.velocity(m))
|
||||
if 5 <= tc <= 18:
|
||||
if adsb.oe_flag(m):
|
||||
msg1 = m
|
||||
t1 = ts
|
||||
else:
|
||||
msg0 = m
|
||||
t0 = ts
|
||||
|
||||
if msg0 and msg1:
|
||||
pos = adsb.position(msg0, msg1, t0, t1)
|
||||
alt = adsb.altitude(m)
|
||||
print(ts, m, icao, tc, pos, alt)
|
||||
|
||||
if __name__ == '__main__':
|
||||
adsb_decode_all(n=100)
|
||||
print("Execution time: {} seconds".format(dt))
|
||||
|
||||
@@ -1,35 +1,52 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import commb, common, bds
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
|
||||
def bds_info(BDS, m):
|
||||
if BDS == "BDS10":
|
||||
info = [commb.ovc10(m)]
|
||||
|
||||
elif BDS == "BDS17":
|
||||
info = ([i[-2:] for i in commb.cap17(m)])
|
||||
info = [i[-2:] for i in commb.cap17(m)]
|
||||
|
||||
elif BDS == "BDS20":
|
||||
info = [commb.cs20(m)]
|
||||
|
||||
elif BDS == "BDS40":
|
||||
info = (commb.alt40mcp(m), commb.alt40fms(m), commb.p40baro(m))
|
||||
info = (commb.selalt40mcp(m), commb.selalt40fms(m), commb.p40baro(m))
|
||||
|
||||
elif BDS == "BDS44":
|
||||
info = (commb.wind44(m), commb.temp44(m), commb.p44(m), commb.hum44(m))
|
||||
|
||||
elif BDS == "BDS44REV":
|
||||
info = (commb.wind44(m, rev=True), commb.temp44(m, rev=True), commb.p44(m, rev=True), commb.hum44(m, rev=True))
|
||||
info = (
|
||||
commb.wind44(m, rev=True),
|
||||
commb.temp44(m, rev=True),
|
||||
commb.p44(m, rev=True),
|
||||
commb.hum44(m, rev=True),
|
||||
)
|
||||
|
||||
elif BDS == "BDS50":
|
||||
info = (commb.roll50(m), commb.trk50(m), commb.gs50(m), commb.rtrk50(m), commb.tas50(m))
|
||||
info = (
|
||||
commb.roll50(m),
|
||||
commb.trk50(m),
|
||||
commb.gs50(m),
|
||||
commb.rtrk50(m),
|
||||
commb.tas50(m),
|
||||
)
|
||||
|
||||
elif BDS == "BDS60":
|
||||
info = (commb.hdg60(m), commb.ias60(m), commb.mach60(m), commb.vr60baro(m), commb.vr60ins(m))
|
||||
info = (
|
||||
commb.hdg60(m),
|
||||
commb.ias60(m),
|
||||
commb.mach60(m),
|
||||
commb.vr60baro(m),
|
||||
commb.vr60ins(m),
|
||||
)
|
||||
|
||||
else:
|
||||
info = None
|
||||
info = []
|
||||
|
||||
return info
|
||||
|
||||
@@ -39,8 +56,7 @@ def commb_decode_all(df, n=None):
|
||||
|
||||
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)):
|
||||
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)
|
||||
|
||||
if not BDS:
|
||||
print(ts, m, icao, df, '%5s'%code, 'UNKNOWN')
|
||||
print(ts, m, icao, df, "%5s" % code, "UNKNOWN")
|
||||
continue
|
||||
|
||||
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(",")):
|
||||
if i == 0:
|
||||
print(_bds, *bds_info(_bds, m))
|
||||
else:
|
||||
print(' ' * 55, _bds, *bds_info(_bds, m))
|
||||
print(" " * 55, _bds, *bds_info(_bds, m))
|
||||
|
||||
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__':
|
||||
commb_decode_all(df=20, n=100)
|
||||
commb_decode_all(df=21, n=100)
|
||||
if __name__ == "__main__":
|
||||
commb_decode_all(df=20, n=500)
|
||||
commb_decode_all(df=21, n=500)
|
||||
|
||||
40
tests/test_acas.py
Normal file
40
tests/test_acas.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import pyModeS as pms
|
||||
|
||||
msgs = [
|
||||
"80E1983958C392E8710C642D0BAD",
|
||||
"8609F8E19E90653083FDE096BE26",
|
||||
"80011B41F40017D0000012E06B45",
|
||||
"808182365813667446EB715E253D",
|
||||
"80E1953058AB029BE8F4E60D989E",
|
||||
"8667ECFA8FE06B30E59A124D5AEF",
|
||||
"80030AA000042C0AD05D6205DB2C",
|
||||
"8631A80000373C463B6E7A00008B",
|
||||
"80E19131588B146108DE703F3C47",
|
||||
"825CC4A0001595C4600030A40000",
|
||||
"808182365813667438EB710EB6D8",
|
||||
"80A18393581D3655E90354A664B2",
|
||||
"8081823658136309B4F22BCB5F8D",
|
||||
"80E1953058AB06516E8602756DD8",
|
||||
"80E1991058C9063AB6A3E4744DC3",
|
||||
"8073EC91840AFCED8F300BE765C5",
|
||||
"8571F54AA814BF8130066A19A31D",
|
||||
"864070E1990D3B1E78048BAE6987",
|
||||
"80E1991058C9063AB6A3E4744DC3",
|
||||
"80818298581581F7CAF8C3C1EEA4",
|
||||
"80004E98BC90000FF01010951298",
|
||||
"8526D57E4D963C92CDEE1B6C7C49",
|
||||
"802A613C93F65A7FF803A51B5ADB",
|
||||
"85B6C54279B67BE2A0001998FFDA",
|
||||
"851944F15881648338D1AF4B7A27",
|
||||
"8321014858208000787905B0E800",
|
||||
"866DD078EDEBD330404FFFAE9BA5",
|
||||
"80E196905AB503260E835D849E35",
|
||||
]
|
||||
|
||||
for msg in msgs:
|
||||
print("-" * 80)
|
||||
print(msg)
|
||||
print("ARA", pms.acas.ara(msg))
|
||||
print("RAC", pms.acas.rac(msg))
|
||||
print("MTE", pms.acas.mte(msg))
|
||||
print("RAT", pms.acas.rat(msg))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user