133 Commits

Author SHA1 Message Date
Xavier Olive
20801883d5 suggestions for installation 2021-01-06 16:46:20 +01:00
Junzi Sun
56d70b03d2 Update README.rst 2021-01-06 12:40:04 +01:00
Junzi Sun
47de4d1163 Merge pull request #87 from Flyer350/patch-5
Fix comments in uplink.py
2020-12-02 11:38:14 +01:00
Xavier Olive
2978329d03 add the pip install pyModeS[fast] option for the Cython dependency 2020-12-01 14:09:39 +01:00
Flyer350
4471c836d9 Update uplink.py 2020-11-27 10:28:20 +01:00
Flyer350
d16484bf72 Update interrogator() in allcall.py (#86) 2020-11-26 10:35:16 +01:00
Junzi Sun
436e9ce2c8 Merge pull request #82 from Flyer350/patch-3
Adding more uplink decoding funtions.
2020-11-26 10:34:03 +01:00
Junzi Sun
4a6341bcc0 import allcall and surv module 2020-11-14 23:04:27 +01:00
Junzi Sun
90e60a74b5 Merge pull request #84 from boringow/master
Implement all-call and short roll-call replies
2020-11-14 22:10:37 +01:00
Junzi Sun
f65ffd13f7 add tests 2020-11-14 22:05:48 +01:00
Junzi Sun
a3e5e7e141 Update surv.py 2020-11-14 21:57:34 +01:00
Junzi Sun
668d6580d0 Update allcall.py 2020-11-14 21:57:08 +01:00
boringow
eacc828b75 Update surv.py
I have completed the allcall.py and the surv.py, but I can not commit the two files at the same time. Hope you like how it looks, If necessary, tell me what could I change. Have a nice weekend!
2020-11-14 09:37:08 +01:00
boringow
4ba900f500 Update allcall.py 2020-11-12 14:49:52 +01:00
boringow
35c0bfa513 Update allcall.py 2020-11-12 14:46:33 +01:00
Flyer350
beb50fc788 Update uplink.py 2020-11-11 00:12:47 +01:00
Flyer350
fc135eacef Update uplink.py
additional decoded fields added
2020-11-11 00:08:30 +01:00
boringow
84c6255567 Update allcall.py
Tested, it is all functioning well, I created a new decoder since I couldn't understand yours. It may not be the most optimum, I am quite new into coding so it may no be optimal, but for sure it is working :) Hope you like it!
2020-11-10 20:44:32 +01:00
Junzi Sun
224a0b8a67 remove print statement 2020-11-04 17:21:07 +01:00
Junzi Sun
c3976f1ca3 update cap17() 2020-10-16 22:32:03 +02:00
Junzi Sun
dd3a869c08 fix typos 2020-09-24 22:16:08 +02:00
Junzi Sun
7e966090ab update meteorological reports decoding example 2020-09-24 22:13:13 +02:00
Junzi Sun
7a4c465f7d remove mandatory pyrtlsdr dependency 2020-08-17 01:19:16 +02:00
Junzi Sun
0ced97e3eb fix exception buffer bug 2020-08-17 00:53:49 +02:00
Junzi Sun
2bdd638cef bug fix in is60() 2020-08-14 23:50:54 +02:00
Junzi Sun
93fc536926 improve BDS 50 and 60 inference 2020-08-05 18:38:12 +02:00
Junzi Sun
1f15a953e3 minor fix in surface speed calculation 2020-07-29 00:53:05 +02:00
Junzi Sun
0d7f628f3c fix modeslive --help bug 2020-07-13 18:52:42 +02:00
Junzi Sun
7348a10f1b minor bug fix 2020-06-10 00:07:26 +02:00
Junzi Sun
c9d2c4c6bc Merge branch 'tuftedocelot-bds-61' 2020-05-23 21:09:58 +02:00
Junzi Sun
0c5539be3a update BDS61 decoder 2020-05-23 21:07:23 +02:00
Junzi Sun
1cf4d37e56 Merge branch 'master' into tuftedocelot-bds-61 2020-05-23 20:30:30 +02:00
Junzi Sun
2fc376abae update squawk decoding 2020-05-23 20:29:18 +02:00
Junzi Sun
9c660578ad Merge branch 'bds-61' of git://github.com/tuftedocelot/pyModeS into tuftedocelot-bds-61 2020-05-23 19:55:30 +02:00
Junzi Sun
ba84cd7c39 update test procedue in Makefile 2020-05-23 19:49:07 +02:00
Junzi Sun
2085a2c432 merge annotations 2020-05-23 19:34:26 +02:00
Junzi Sun
4126dedd19 fix altitude decoding 2020-05-23 19:17:58 +02:00
Chris Lajoie
7892aac4de Continue reading from socket if no data received (#70)
Without this change, `modeslive` crashes if no data is available on the socket, which can happen during normal operation of a receiver if no aircraft are in range.

Tested on Python 3.7
2020-05-20 11:56:27 +02:00
Xavier Olive
7dcff01910 first annotations 2020-05-07 11:13:57 +02:00
Junzi Sun
228306dd5f version 2.8 2020-05-04 21:35:29 +02:00
Junzi Sun
1c6b39322f rename common file 2020-05-04 20:34:25 +02:00
Junzi Sun
9dde1b63d6 fix missing bin2hex() 2020-05-04 20:30:33 +02:00
Junzi Sun
560b737739 update api link 2020-05-04 01:51:33 +02:00
Junzi Sun
d50f05d00d Merge branch 'master' of github.com:junzis/pyModeS 2020-05-04 01:09:40 +02:00
Junzi Sun
11a560dbb5 update API doc 2020-05-04 01:07:02 +02:00
Junzi Sun
b51facbdc6 update docstring 2020-05-04 00:56:36 +02:00
Junzi Sun
d77d1f7f6b update docstring 2020-05-04 00:11:29 +02:00
Junzi Sun
5286355bf6 move common package to root level 2020-05-03 23:27:17 +02:00
Junzi Sun
fe9e033a64 update comments 2020-05-03 20:13:22 +02:00
Junzi Sun
57307109e6 remove python 2 imports 2020-05-02 19:32:54 +02:00
tuftedocelot
dc8f194e6e Add support and tests for BDS 6,1 emergency messages
ACAS RA messages (subtype 2) are not yet implemented. These messages
are the same format as BDS3,0 starting at byte 9
2020-04-30 20:35:19 -05:00
Junzi Sun
6571fe6fc0 Update README.rst 2020-04-04 20:48:23 +02:00
Junzi Sun
aa64d4e7a9 update README 2020-04-04 20:44:46 +02:00
wrobell
db57d7419f Improve doc for surface position decoding 2020-03-11 22:08:04 +01:00
Junzi Sun
ed18352c0c update NL() function in CPR decoding 2020-03-03 12:39:39 +01:00
Junzi Sun
a75d6fd050 install c module 2020-02-26 00:55:25 +01:00
Junzi Sun
0ff628bb8e update imports 2020-02-26 00:54:49 +01:00
Junzi Sun
ea7653ef79 update test 2020-02-26 00:25:11 +01:00
Junzi Sun
2046b1de07 speedup (#59)
* remove unused functions

* cythonize common

* add bds05

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

* bds08

* bds09

* optimisations in bds09

* "make" things easier

* clean up useless stuff

* add make options

* fix hidden altitude() call

* minor updates to C code

* update tests

* update benchmark

* consolidation

* update clean script

* reduce complexity and change default type to str

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

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

so I propose this fix.
I used pyModeS      2.2
2019-09-05 16:56:42 +02:00
Junzi Sun
03f81d120b Merge pull request #48 from richardjanssen/master
Fix surface positions with negative longitude
2019-09-03 22:14:41 +02:00
Junzi Sun
b9b95320d8 keep only common virtualenv folder names 2019-09-03 22:12:30 +02:00
Richard Janssen
0ea2cf7ade Fix surface positions for negative longitude 2019-09-03 12:15:34 +02:00
Junzi Sun
458b02028d update readme 2019-08-26 16:50:05 +02:00
Junzi Sun
b062bdf998 fix local buffer bug 2019-08-26 16:38:36 +02:00
Junzi Sun
cfcd21b692 improve multiprocessing efficiency 2019-08-26 12:00:14 +02:00
Junzi Sun
86f302f05e change modeslive to multiprocessing 2019-08-26 00:21:29 +02:00
Junzi Sun
28a6e53d49 update dependencies 2019-08-23 14:59:50 +02:00
Junzi Sun
81d7cef6e8 Merge pull request #47 from junzis/rtlsdr
Add RTL-SDR support
2019-08-23 14:47:50 +02:00
Junzi Sun
4906a49e9c update modeslive command 2019-08-23 14:46:13 +02:00
Junzi Sun
7cb75ea8ca Update README.rst 2019-08-22 19:47:20 +02:00
Junzi Sun
785584aff5 improve singal process efficiency 2019-08-22 16:59:12 +02:00
Junzi Sun
fdc34497c0 update instruction for rtl-sdr 2019-08-22 10:51:11 +02:00
Junzi Sun
4a3c9438f7 adding support for rtl-sdr tunner 2019-08-22 10:45:12 +02:00
Junzi Sun
b723374337 Merge pull request #45 from hegrin-cz/master
Corrected ADS-B emitter category parsing.
2019-08-22 10:37:58 +02:00
Hegr,Jiri
8f4dff5b30 Corrected category parsing. 2019-08-19 14:50:09 +02:00
Junzi Sun
7267859548 pyModeS v2.2 2019-08-05 16:37:30 +02:00
Junzi Sun
7a3bd089c4 update wind null return 2019-08-05 16:25:46 +02:00
Junzi Sun
17fdaca1c7 fix bug for alternative temperature 2019-08-05 14:21:21 +02:00
Junzi Sun
6f6b50776d rename functions for seleted altitudes in BDS40 2019-08-05 10:37:17 +02:00
Junzi Sun
0b7b9ad3dd rewrite CRC function 2019-07-09 15:02:06 +02:00
Junzi Sun
24806f7e88 add downlink format check 2019-06-06 11:46:44 +02:00
Junzi Sun
57ad40ec57 Merge pull request #44 from amhirsch/fix-velocity
Decoder recognizes supersonic velocity subtype
2019-06-06 11:33:26 +02:00
Junzi Sun
d230bdc4a9 Merge pull request #43 from amhirsch/debug-stream
Add debugging option
2019-06-06 11:32:33 +02:00
Alexander Hirsch
4a6d3334a7 Decoder recognizes supersonic velocity subtype
If the aircraft velocity message indicates a supersonic subtype
(2 or 4), multiply the derived velocity by four. This is in accordance
with Table A-2-9[a, b] in the "Technical Provisions for Mode-S Services
and Extended Squitter"
2019-06-05 16:11:50 -07:00
Alexander Hirsch
9f371fe86b Stream errors are printed with full traceback
The stream will now properly print the traceback of an error if the
debuging variable is set.
2019-06-03 17:42:26 -07:00
Alexander Hirsch
0ea4969393 Print the traceback of TCP client Exception
The function now prints the system execution information upon an
exception.
The last commit requested traceback information, but did not print the
information to the console.
2019-06-03 17:23:26 -07:00
Alexander Hirsch
312c77629b Provides debuging option for TCP stream
The TCP BaseClient class has been modified to recognize an environment
variable 'PYMODES_DEBUG'. When the variable is set to 'true', the
stream will halt execution rather than restarting. This is for
debugging purposes and does not alter the main functionality of
the program.
2019-06-03 16:08:28 -07:00
Junzi Sun
6159691c3d update CRC code 2019-05-27 21:26:18 +02:00
Junzi Sun
7edbf3fd30 Merge pull request #29 from alcibiade/fastcrc
FastCRC - A more efficient CRC calculation
2019-05-27 20:38:27 +02:00
Junzi Sun
e19d0cd945 Merge pull request #39 from amhirsch/DF-17Additions
Additional Information Extracted from TC-19/BDS0,9 Messages
2019-05-27 20:26:23 +02:00
Alexander Hirsch
6fc68841ce Updted docstrings for velocity messages 2019-05-27 08:25:11 -07:00
Alexander Hirsch
f27fe6c8aa Changed velocity source names 2019-05-27 08:13:02 -07:00
Junzi Sun
81a46e5070 add the BaseClient example 2019-05-27 10:59:32 +02:00
Junzi Sun
6c2adbe990 update citation info 2019-05-27 10:06:43 +02:00
Alexander Hirsch
3c14579040 velocity default argument for backwards compatibility 2019-05-17 09:45:16 -07:00
Alexander Hirsch
fc9b05b6f1 Added direction source to ground_velocity() 2019-05-17 06:06:06 -07:00
Alexander Hirsch
b813e41343 Fixed ground velocity return and updated velocity docstring 2019-05-17 05:59:14 -07:00
Alexander Hirsch
c9159feb7d BDS09 returns direction and v/s source 2019-05-16 19:05:19 -07:00
Junzi Sun
8cd5655a04 update api doc 2019-04-16 17:53:14 +02:00
Junzi Sun
8ded3500d4 version 2.1 2019-04-16 16:56:49 +02:00
Junzi Sun
c348a2295d add MHR to comm-b wapper 2019-04-16 16:45:52 +02:00
Junzi Sun
040a1879c9 update temp44() to produce two possible results 2019-04-16 16:44:50 +02:00
Junzi Sun
61b55531e8 update BDS 44 and 45, and MRAR switch in infer(). 2019-04-16 11:57:57 +02:00
Junzi Sun
abdafd7dea rework BDS 44 and 45 2019-04-12 17:18:03 +02:00
Junzi Sun
b97299ce1b update readme 2019-04-03 09:41:22 +02:00
Junzi Sun
652ef65bbb update api doc 2019-04-03 09:40:27 +02:00
alcibiade
bfdb8221a0 Remove remaining Python3 explicit typing.
The original version of this code is built against Python3.5+ and uses explicit typing. As I think the pyModeS supports Python2, I thought it was preferable top remove these types.
2018-11-05 21:55:45 +01:00
Yannick Kirschhoffer
32e6ee3904 Implement an alternative faster CRC. 2018-11-04 00:36:33 +01:00
Yannick Kirschhoffer
d82dfd5537 Extended test to stress the CRC function. 2018-11-04 00:17:09 +01:00
94 changed files with 4891 additions and 2772 deletions

9
.gitignore vendored
View File

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

26
Makefile Normal file
View 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/*

View File

@@ -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) message. 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
View File

@@ -1,3 +0,0 @@
_build
_static
_templates

View File

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

@@ -0,0 +1,7 @@
How to generate the apidoc
====================================
::
cd doc
make html

View File

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

View File

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

View File

@@ -1 +0,0 @@
pyModeS==1.1.0

View File

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

190
doc/source/conf.py Normal file
View 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
View 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
View File

@@ -0,0 +1,7 @@
pyModeS
=======
.. toctree::
:maxdepth: 4
pyModeS

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View File

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

View File

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

View 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
View 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
View File

View File

@@ -1,15 +1,23 @@
from __future__ import absolute_import, print_function, division
import os
import warnings
from .decoder.common import *
try:
from . import c_common as common
from .c_common import *
except:
from . import py_common as common
from .py_common import *
from .decoder import tell
from .decoder import adsb
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
import os
warnings.simplefilter("once", DeprecationWarning)
dirpath = os.path.dirname(os.path.realpath(__file__))

28
pyModeS/c_common.pxd Normal file
View 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)

405
pyModeS/c_common.pyx Normal file
View 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 hexdecimal string to binary string, with zero fillings."""
# num_of_bits = len(hexstr) * 4
cdef hexbytes = bytes(hexstr.encode())
cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes)
# binstr = bin(int(hexbytes, 16))[2:].zfill(int(num_of_bits))
cdef bytearray _binstr = bytearray(4 * len_hexstr)
cdef unsigned char[:] binstr = _binstr
cdef unsigned char int_
cdef Py_ssize_t i
for i in range(len_hexstr):
int_ = char_to_int(hexbytes[i])
binstr[4*i] = int_to_char((int_ >> 3) & 1)
binstr[4*i+1] = int_to_char((int_ >> 2) & 1)
binstr[4*i+2] = int_to_char((int_ >> 1) & 1)
binstr[4*i+3] = int_to_char((int_) & 1)
return _binstr.decode()
@cython.boundscheck(False)
cpdef long bin2int(str binstr):
"""Convert a binary string to integer."""
# return int(binstr, 2)
cdef bytearray binbytes = bytearray(binstr.encode())
cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes)
cdef long cumul = 0
cdef unsigned char[:] v_binstr = binbytes
for i in range(len_):
cumul = 2*cumul + char_to_int(v_binstr[i])
return cumul
@cython.boundscheck(False)
cpdef long hex2int(str hexstr):
"""Convert a binary string to integer."""
# return int(hexstr, 2)
cdef bytearray binbytes = bytearray(hexstr.encode())
cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes)
cdef long cumul = 0
cdef unsigned char[:] v_hexstr = binbytes
for i in range(len_):
cumul = 16*cumul + char_to_int(v_hexstr[i])
return cumul
@cython.boundscheck(False)
cpdef str bin2hex(str binstr):
return "{0:X}".format(int(binstr, 2))
@cython.boundscheck(False)
cpdef unsigned char df(str msg):
"""Decode Downlink Format vaule, bits 1 to 5."""
cdef str dfbin = hex2bin(msg[:2])
# return min(bin2int(dfbin[0:5]), 24)
cdef long df = bin2int(dfbin[0:5])
if df > 24:
return 24
return df
# the CRC generator
# G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000])
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef long crc(str msg, bint encode=False):
"""Mode-S Cyclic Redundancy Check.
Detect if bit error occurs in the Mode-S message. When encode option is on,
the checksum is generated.
Args:
msg (string): 28 bytes hexadecimal message string
encode (bool): True to encode the date only and return the checksum
Returns:
int: message checksum, or partity bits (encoder)
"""
# the CRC generator
# G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)]
# cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000])
cdef long[4] G = _G
# msgbin_split = wrap(msgbin, 8)
# mbytes = list(map(bin2int, msgbin_split))
cdef bytearray _msgbin = bytearray(hex2bin(msg).encode())
cdef unsigned char[:] msgbin = _msgbin
cdef Py_ssize_t len_msgbin = PyByteArray_GET_SIZE(_msgbin)
cdef Py_ssize_t len_mbytes = len_msgbin // 8
cdef Py_ssize_t i
if encode:
for i in range(len_msgbin - 24, len_msgbin):
msgbin[i] = 0
cdef array.array _mbytes = array.array(
'l', [bin2int(_msgbin[8*i:8*i+8].decode()) for i in range(len_mbytes)]
)
cdef long[:] mbytes = _mbytes
cdef long bits, mask
cdef Py_ssize_t ibyte, ibit
for ibyte in range(len_mbytes - 3):
for ibit in range(8):
mask = 0x80 >> ibit
bits = mbytes[ibyte] & mask
if bits > 0:
mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit)
mbytes[ibyte + 1] = mbytes[ibyte + 1] ^ (
0xFF & ((G[0] << 8 - ibit) | (G[1] >> ibit))
)
mbytes[ibyte + 2] = mbytes[ibyte + 2] ^ (
0xFF & ((G[1] << 8 - ibit) | (G[2] >> ibit))
)
mbytes[ibyte + 3] = mbytes[ibyte + 3] ^ (
0xFF & ((G[2] << 8 - ibit) | (G[3] >> ibit))
)
cdef long result = (mbytes[len_mbytes-3] << 16) | (mbytes[len_mbytes-2] << 8) | mbytes[len_mbytes-1]
return result
cpdef long floor(double x):
"""Mode-S floor function.
Defined as the greatest integer value k, such that k <= x
For example: floor(3.6) = 3 and floor(-3.6) = -4
"""
return <long> c_floor(x)
cpdef str icao(str msg):
"""Calculate the ICAO address from an Mode-S message."""
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.0 * fabs(lat)) ** 2
cdef double nl = 2 * pi / (acos(1 - a / b))
NL = floor(nl)
return NL
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef str idcode(str msg):
"""Compute identity (squawk code)."""
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

View File

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

View File

@@ -1,21 +1,5 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Decoding Air-Air Surveillance (ACAS) DF=0/16
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
[To be implemented]
"""

View File

@@ -1,89 +1,102 @@
# 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/>.
- pyModeS.decoder.bds.bds05: ``airborne_position()``, ``airborne_position_with_ref()``, ``altitude()``
- pyModeS.decoder.bds.bds06: ``surface_position()``, ``surface_position_with_ref()``, ``surface_velocity()``
- pyModeS.decoder.bds.bds08: ``category()``, ``callsign()``
- pyModeS.decoder.bds.bds09: ``airborne_velocity()``, ``altitude_diff()``
"""
The wrapper for decoding ADS-B messages
"""
from __future__ import absolute_import, print_function, division
import pyModeS as pms
from pyModeS.decoder import common
from pyModeS import common
from pyModeS.decoder import uncertainty
# from pyModeS.decoder.bds import bds05, bds06, bds09
from pyModeS.decoder.bds.bds05 import airborne_position, airborne_position_with_ref, altitude
from pyModeS.decoder.bds.bds06 import surface_position, surface_position_with_ref, surface_velocity
from pyModeS.decoder.bds.bds05 import (
airborne_position,
airborne_position_with_ref,
altitude as altitude05,
)
from pyModeS.decoder.bds.bds06 import (
surface_position,
surface_position_with_ref,
surface_velocity,
)
from pyModeS.decoder.bds.bds08 import category, callsign
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
from pyModeS.decoder.bds.bds61 import is_emergency, emergency_state, emergency_squawk
def df(msg):
return common.df(msg)
def icao(msg):
return common.icao(msg)
def typecode(msg):
return common.typecode(msg)
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
"""Decode position from a pair of even and odd position message
(works with both airborne and surface position messages)
"""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 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.
"""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 with in 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,66 +106,70 @@ def position_with_ref(msg, lat_ref, lon_ref):
tc = typecode(msg)
if 5<=tc<=8:
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
"""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 < 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):
"""Calculate the speed, heading, and vertical rate
(handles both airborne or surface message)
def velocity(msg, source=False):
"""Calculate the speed, heading, and vertical rate (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]: Four or six parameters, including:
- 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 'MAGENTIC_NORTH')
- [Optional] Vertical rate source ('BARO' or 'GNSS')
For surface messages, vertical rate and its respective sources are set to None.
"""
if 5 <= typecode(msg) <= 8:
return surface_velocity(msg)
return surface_velocity(msg, source)
elif typecode(msg) == 19:
return airborne_velocity(msg)
return airborne_velocity(msg, source)
else:
raise RuntimeError("incorrect or inconsistant message types, expecting 4<TC<9 or TC=19")
raise RuntimeError(
"incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
)
def speed_heading(msg):
@@ -160,7 +177,7 @@ def speed_heading(msg):
(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)
@@ -172,7 +189,7 @@ def speed_heading(msg):
def oe_flag(msg):
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: 0 or 1, for even or odd frame
"""
@@ -184,7 +201,7 @@ def version(msg):
"""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 +209,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])
@@ -204,7 +223,7 @@ def nuc_p(msg):
"""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: Horizontal Protection Limit
@@ -218,18 +237,18 @@ def nuc_p(msg):
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']
HPL = uncertainty.NUCp[NUCp]["HPL"]
RCu = uncertainty.NUCp[NUCp]["RCu"]
RCv = uncertainty.NUCp[NUCp]["RCv"]
except KeyError:
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
if tc in [20, 21]:
RCv = uncertainty.NA
@@ -240,7 +259,7 @@ def nuc_v(msg):
"""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 or string: 95% Horizontal Velocity Error
@@ -249,15 +268,16 @@ def nuc_v(msg):
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])
try:
HVE = uncertainty.NUCv[NUCv]['HVE']
VVE = uncertainty.NUCv[NUCv]['VVE']
HVE = uncertainty.NUCv[NUCv]["HVE"]
VVE = uncertainty.NUCv[NUCv]["VVE"]
except KeyError:
HVE, VVE = uncertainty.NA, uncertainty.NA
@@ -268,7 +288,7 @@ def nic_v1(msg, NICs):
"""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:
@@ -279,7 +299,8 @@ def nic_v1(msg, NICs):
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)
@@ -289,8 +310,8 @@ def nic_v1(msg, NICs):
NIC = NIC[NICs]
try:
Rc = uncertainty.NICv1[NIC][NICs]['Rc']
VPL = uncertainty.NICv1[NIC][NICs]['VPL']
Rc = uncertainty.NICv1[NIC][NICs]["Rc"]
VPL = uncertainty.NICv1[NIC][NICs]["VPL"]
except KeyError:
Rc, VPL = uncertainty.NA, uncertainty.NA
@@ -301,7 +322,7 @@ def nic_v2(msg, NICa, NICbc):
"""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
@@ -312,22 +333,23 @@ def nic_v2(msg, NICa, NICbc):
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
@@ -338,7 +360,7 @@ def nic_s(msg):
"""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 +368,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])
@@ -358,7 +382,7 @@ def nic_a_c(msg):
"""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 +390,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])
@@ -379,7 +405,7 @@ def nic_b(msg):
"""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)
@@ -387,7 +413,9 @@ def nic_b(msg):
tc = typecode(msg)
if tc < 9 or tc > 18:
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
raise RuntimeError(
"%s: Not a airborne position message, expecting 8<TC<19" % msg
)
msgbin = common.hex2bin(msg)
nic_b = int(msgbin[39])
@@ -399,7 +427,7 @@ def nac_p(msg):
"""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
@@ -408,8 +436,11 @@ def nac_p(msg):
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,8 +450,8 @@ 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
@@ -431,7 +462,7 @@ def nac_v(msg):
"""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
@@ -440,14 +471,16 @@ def nac_v(msg):
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
@@ -458,7 +491,7 @@ def sil(msg, version):
"""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
@@ -468,8 +501,11 @@ def sil(msg, version):
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 +515,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:

View File

@@ -1,21 +1,82 @@
# 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 dowlink format 11
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
from pyModeS import common
def _checkdf(func):
"""Ensure downlink format is 11."""
def wrapper(msg):
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):
"""Decode transponder code (ICAO address).
Args:
msg (str): 14 hexdigits string
Returns:
string: ICAO address
"""
return common.icao(msg)
@_checkdf
def interrogator(msg):
"""Decode interrogator identifier code.
Args:
msg (str): 14 hexdigits string
Returns:
int: interrogator identifier code
"""
# the CRC remainder contains the CL and IC field. 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):
"""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

View File

@@ -18,59 +18,84 @@
Common functions for Mode-S decoding
"""
from __future__ import absolute_import, print_function, division
import numpy as np
from pyModeS.extra import aero
from pyModeS.decoder import common
from pyModeS.decoder.bds import bds05, bds06, bds08, bds09, \
bds10, bds17, bds20, bds30, bds40, bds44, bds50, bds53, bds60
from pyModeS import common
from pyModeS.decoder.bds import (
bds05,
bds06,
bds08,
bds09,
bds10,
bds17,
bds20,
bds30,
bds40,
bds44,
bds45,
bds50,
bds53,
bds60,
)
def is50or60(msg, spd_ref, trk_ref, alt_ref):
"""Use reference ground speed and trk to determine BDS50 and DBS60
"""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.
"""
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 +105,52 @@ 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, mrar=False):
"""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.
"""
"""
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 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 +158,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

View File

@@ -1,36 +1,18 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# ------------------------------------------
# BDS 0,5
# ADS-B TC=9-18
# Airborn 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 pyModeS import common
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
------------------------------------------
BDS 0,5
ADS-B TC=9-18
Airborn position
------------------------------------------
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
def airborne_position(msg0, msg1, t0, t1):
"""Decode airborn position from a pair of even and odd position message
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
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,6 +23,16 @@ def airborne_position(msg0, msg1, t0, t1):
mb0 = common.hex2bin(msg0)[32:]
mb1 = common.hex2bin(msg1)[32:]
oe0 = int(mb0[21])
oe1 = int(mb1[21])
if oe0 == 0 and oe1 == 1:
pass
elif oe0 == 1 and oe1 == 0:
mb0, mb1 = mb1, mb0
t0, t1 = t1, t0
else:
raise RuntimeError("Both even and odd CPR frames are required.")
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
@@ -67,17 +59,17 @@ def airborne_position(msg0, msg1, t0, t1):
return None
# compute ni, longitude index m, and longitude
if (t0 > t1):
if t0 > t1:
lat = lat_even
nl = common.cprNL(lat)
ni = max(common.cprNL(lat)- 0, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
ni = max(common.cprNL(lat) - 0, 1)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 1, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_odd)
if lon > 180:
@@ -93,7 +85,7 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
be with in 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,17 +93,17 @@ 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
i = int(mb[21])
d_lat = 360.0/59 if i else 360.0/60
d_lat = 360.0 / 59 if i else 360.0 / 60
j = common.floor(lat_ref / d_lat) \
+ 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)
@@ -122,8 +114,9 @@ def airborne_position_with_ref(msg, lat_ref, lon_ref):
else:
d_lon = 360.0
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)
@@ -134,7 +127,7 @@ def altitude(msg):
"""Decode aircraft altitude
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: altitude in feet
@@ -142,21 +135,17 @@ def altitude(msg):
tc = common.typecode(msg)
if tc<9 or tc==19 or tc>22:
if tc < 9 or tc == 19 or tc > 22:
raise RuntimeError("%s: Not a airborn position message" % msg)
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:]
else:
# GNSS altitude, meters -> feet
alt = common.bin2int(mb[8:20]) * 3.28084
altcode = altbin[0:6] + "0" + altbin[6:]
alt = common.altitude(altcode)
return alt

View File

@@ -1,30 +1,10 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# ------------------------------------------
# BDS 0,6
# ADS-B TC=5-8
# Surface movment
# ------------------------------------------
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
------------------------------------------
BDS 0,6
ADS-B TC=5-8
Surface position
------------------------------------------
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
import math
from pyModeS import common
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
@@ -32,8 +12,8 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
the lat/lon of receiver must be provided to yield the correct solution.
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
@@ -75,22 +55,25 @@ def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
return None
# compute ni, longitude index m, and longitude
if (t0 > t1):
if t0 > t1:
lat = lat_even
nl = common.cprNL(lat_even)
ni = max(common.cprNL(lat_even) - 0, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat_odd)
ni = max(common.cprNL(lat_odd) - 1, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_odd)
# four possible longitude solutions
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
# make sure lons are between -180 and 180
lons = [(l + 180) % 360 - 180 for l in lons]
# the closest solution to receiver is the correct one
dls = [abs(lon_ref - l) for l in lons]
imin = min(range(4), key=dls.__getitem__)
@@ -106,7 +89,7 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
be with in 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,17 +97,17 @@ 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
i = int(mb[21])
d_lat = 90.0/59 if i else 90.0/60
d_lat = 90.0 / 59 if i else 90.0 / 60
j = common.floor(lat_ref / d_lat) \
+ 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)
@@ -135,25 +118,33 @@ def surface_position_with_ref(msg, lat_ref, lon_ref):
else:
d_lon = 90.0
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, source=False):
"""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]: Four or six parameters, including:
- 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:
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
@@ -167,7 +158,7 @@ def surface_velocity(msg):
else:
trk = None
# ground movment / speed
# ground movement / speed
mov = common.bin2int(mb[5:12])
if mov == 0 or mov > 124:
@@ -177,11 +168,12 @@ def surface_velocity(msg):
elif mov == 124:
spd = 175
else:
movs = [2, 9, 13, 39, 94, 109, 124]
kts = [0.125, 1, 2, 15, 70, 100, 175]
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
step = (kts[i] - kts[i-1]) * 1.0 / (movs[i]-movs[i-1])
spd = kts[i-1] + (mov-movs[i-1]) * step
spd = round(spd, 2)
return spd, trk, 0, 'GS'
mov_lb = [2, 9, 13, 39, 94, 109, 124]
kts_lb = [0.125, 1, 2, 15, 70, 100, 175]
step = [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]
if source:
return spd, trk, 0, "GS", "TRUE_NORTH", None
else:
return spd, trk, 0, "GS"

View File

@@ -1,35 +1,17 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# ------------------------------------------
# BDS 0,8
# ADS-B TC=1-4
# Aircraft identitification 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.
from pyModeS import common
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
------------------------------------------
BDS 0,8
ADS-B TC=1-4
Aircraft identitification and category
------------------------------------------
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
def category(msg):
"""Aircraft category number
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
int: category number
@@ -39,14 +21,15 @@ def category(msg):
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):
"""Aircraft callsign
Args:
msg (string): 28 bytes hexadecimal message string
msg (str): 28 hexdigits string
Returns:
string: callsign
@@ -55,11 +38,11 @@ def callsign(msg):
if common.typecode(msg) < 1 or common.typecode(msg) > 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 +54,5 @@ def callsign(msg):
# clean string, remove spaces and marks, if any.
# cs = cs.replace('_', '')
cs = cs.replace('#', '')
cs = cs.replace("#", "")
return cs

View File

@@ -1,44 +1,33 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# ------------------------------------------
# BDS 0,9
# ADS-B TC=19
# Aircraft Airborn 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.
# 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 pyModeS import common
"""
------------------------------------------
BDS 0,9
ADS-B TC=19
Aircraft Airborn velocity
------------------------------------------
"""
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
import math
def airborne_velocity(msg):
"""Calculate the speed, track (or heading), and vertical rate
def airborne_velocity(msg, source=False):
"""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]: Four or six parameters, including:
- 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 'MAGENTIC_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)
@@ -50,27 +39,32 @@ def airborne_velocity(msg):
return None
if subtype in (1, 2):
v_ew_sign = -1 if mb[13]=='1' else 1
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
v_ew_sign = -1 if mb[13] == "1" else 1
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
if subtype == 2: # Supersonic
v_ew *= 4
v_ns_sign = -1 if mb[24]=='1' else 1
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
v_ns_sign = -1 if mb[24] == "1" else 1
v_ns = common.bin2int(mb[25:35]) - 1 # north-south velocity
if subtype == 2: # Supersonic
v_ns *= 4
v_we = v_ew_sign * v_ew
v_sn = v_ns_sign * v_ns
spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts
spd = 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 = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val
tag = 'GS'
spd_type = "GS"
trk_or_hdg = round(trk, 2)
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
@@ -79,28 +73,38 @@ def airborne_velocity(msg):
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: # 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 = "MAGENTIC_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)
if source:
return spd, trk_or_hdg, vs, spd_type, dir_type, vr_source
else:
return spd, trk_or_hdg, vs, spd_type
return spd, trk_or_hdg, rocd, tag
def altitude_diff(msg):
"""Decode the differece between GNSS and barometric altitude
"""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)
@@ -114,4 +118,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.

View File

@@ -1,66 +1,52 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
# ------------------------------------------
# BDS 1,0
# Data link capability report
# ------------------------------------------
from pyModeS import common
def is10(msg):
"""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):
"""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])

View File

@@ -1,45 +1,27 @@
# 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 pyModeS import common
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
"""
------------------------------------------
BDS 1,7
Common usage GICB capability report
------------------------------------------
"""
def is17(msg):
"""Check if a message is likely to be BDS code 1,7
Args:
msg (String): 28 bytes hexadecimal message string
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 +32,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):
"""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 support 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

View File

@@ -1,47 +1,32 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
# ------------------------------------------
# BDS 2,0
# Aircraft identification
# ------------------------------------------
from pyModeS import common
def is20(msg):
"""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)
if '#' in cs:
if "#" in cs:
return False
return True
@@ -51,23 +36,23 @@ def cs20(msg):
"""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

View File

@@ -1,50 +1,35 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, division
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
# ------------------------------------------
# BDS 3,0
# ACAS active resolution advisory
# ------------------------------------------
from pyModeS import common
def is30(msg):
"""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] != '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

View File

@@ -1,103 +1,88 @@
# 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
# ------------------------------------------
import warnings
from pyModeS import common
def is40(msg):
"""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):
"""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):
"""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
@@ -105,15 +90,31 @@ def p40baro(msg):
"""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):
warnings.warn(
"alt40mcp() has been renamed to selalt40mcp(). It will be removed in the future.",
DeprecationWarning,
)
return selalt40mcp(msg)
def alt40fms(msg):
warnings.warn(
"alt40fms() has been renamed to selalt40fms(). It will be removed in the future.",
DeprecationWarning,
)
return selalt40fms(msg)

View File

@@ -1,218 +1,161 @@
# 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 pyModeS import common
def is44(msg):
"""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):
"""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.0 / 256.0 # degree
return round(speed, 0), round(direction, 1)
def temp44(msg, rev=False):
"""reported air temperature
def temp44(msg):
"""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):
"""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):
"""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.0 / 64 # %
return round(hm, 1)
def turb44(msg):
"""Turblence.
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

View File

@@ -0,0 +1,208 @@
# ------------------------------------------
# BDS 4,5
# Meteorological hazard report
# ------------------------------------------
from pyModeS import common
def is45(msg):
"""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):
"""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):
"""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):
"""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):
"""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):
"""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):
"""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):
"""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):
"""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

View File

@@ -1,61 +1,46 @@
# 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
# ------------------------------------------
from pyModeS import common
def is50(msg):
"""Check if a message is likely to be BDS code 5,0
(Track and turn report)
Args:
msg (String): 28 bytes hexadecimal message string
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)
@@ -76,24 +61,24 @@ def roll50(msg):
"""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.0 / 256.0 # degree
return round(angle, 1)
@@ -101,18 +86,18 @@ def trk50(msg):
"""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
@@ -130,17 +115,17 @@ def gs50(msg):
"""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
@@ -148,25 +133,25 @@ def rtrk50(msg):
"""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.0 / 256.0 # degree / sec
return round(angle, 3)
@@ -174,15 +159,15 @@ def tas50(msg):
"""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

View File

@@ -1,57 +1,42 @@
# 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
# ------------------------------------------
from pyModeS import common
def is53(msg):
"""Check if a message is likely to be BDS code 5,3
(Air-referenced state vector)
Args:
msg (String): 28 bytes hexadecimal message string
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)
@@ -77,23 +62,23 @@ def hdg53(msg):
"""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.0 / 512.0 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
@@ -106,17 +91,17 @@ def ias53(msg):
"""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
@@ -124,17 +109,17 @@ def mach53(msg):
"""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)
@@ -142,40 +127,41 @@ def tas53(msg):
"""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):
"""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

View File

@@ -1,56 +1,42 @@
# 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
# ------------------------------------------
from pyModeS import common
from pyModeS.extra import aero
def is60(msg):
"""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,6 +55,14 @@ 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
@@ -76,18 +70,18 @@ def hdg60(msg):
"""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
@@ -105,17 +99,17 @@ def ias60(msg):
"""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
@@ -123,17 +117,17 @@ def mach60(msg):
"""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
mach = common.bin2int(d[24:34]) * 2.048 / 512.0
return round(mach, 3)
@@ -141,49 +135,49 @@ def vr60baro(msg):
"""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)
"""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

View File

@@ -0,0 +1,83 @@
# ------------------------------------------
# BDS 6,1
# ADS-B TC=28
# Aircraft Airborne status
# ------------------------------------------
from pyModeS 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:49] + "0" + msgbin[49:55]
squawk = common.squawk(idcode)
return squawk

View File

@@ -1,4 +1,26 @@
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 *
@@ -11,5 +33,6 @@ from pyModeS.decoder.bds.bds40 import *
from pyModeS.decoder.bds.bds50 import *
from pyModeS.decoder.bds.bds60 import *
# MRAR
# MRAR and MHR
from pyModeS.decoder.bds.bds44 import *
from pyModeS.decoder.bds.bds45 import *

View File

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

View File

@@ -1,4 +1,14 @@
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 *
@@ -6,13 +16,20 @@ from pyModeS.decoder.bds.bds50 import *
from pyModeS.decoder.bds.bds60 import *
from pyModeS.decoder.bds import infer
warnings.simplefilter('once', DeprecationWarning)
warnings.warn("pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning)
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)

View File

@@ -1,4 +1,14 @@
from __future__ import absolute_import, print_function, division
"""ELS Wrapper.
``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
"""
from pyModeS.decoder.bds.bds10 import *
from pyModeS.decoder.bds.bds17 import *
@@ -6,5 +16,8 @@ from pyModeS.decoder.bds.bds20 import *
from pyModeS.decoder.bds.bds30 import *
import warnings
warnings.simplefilter('once', DeprecationWarning)
warnings.warn("pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning)
warnings.simplefilter("once", DeprecationWarning)
warnings.warn(
"pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning
)

View File

@@ -1,21 +1,132 @@
# 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 pyModeS import common
def _checkdf(func):
"""Ensure downlink format is 4 or 5."""
def wrapper(msg):
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):
"""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 = 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
@_checkdf
def dr(msg):
"""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 = 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
@_checkdf
def um(msg):
"""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):
"""Decode altitude.
Args:
msg (String): 14 hexdigits string
Returns:
int: altitude in ft
"""
return common.altcode(msg)
@_checkdf
def identity(msg):
"""Decode squawk code.
Args:
msg (String): 14 hexdigits string
Returns:
string: squawk code
"""
return common.idcode(msg)

View File

@@ -1,120 +1,152 @@
"""Uncertainty parameters.
See source code at: https://github.com/junzis/pyModeS/blob/master/pyModeS/decoder/uncertainty.py
"""
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
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
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},
9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
8: {"HPL": 25, "RCu": 10, "RCv": 15},
7: {"HPL": 185, "RCu": 93, "RCv": NA},
6: {"HPL": 370, "RCu": 185, "RCv": NA},
5: {"HPL": 926, "RCu": 463, "RCv": NA},
4: {"HPL": 1852, "RCu": 926, "RCv": NA},
3: {"HPL": 3704, "RCu": 1852, "RCv": NA},
2: {"HPL": 18520, "RCu": 9260, "RCv": NA},
1: {"HPL": 37040, "RCu": 18520, "RCv": NA},
0: {"HPL": NA, "RCu": NA, "RCv": NA},
}
NUCv = {
0: {'HVE':NA, 'VVE':NA},
1: {'HVE':10, 'VVE':15.2},
2: {'HVE':3, 'VVE':4.5},
3: {'HVE':1, 'VVE':1.5},
4: {'HVE':0.3, 'VVE':0.46},
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},
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},
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},
3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
0: {"PE_RCu": NA, "PE_VPL": NA},
}
NICv1 = {
# NIC is used as the index at second Level
11: {0: {'Rc': 7.5, 'VPL': 11}},
10: {0: {'Rc': 25, 'VPL': 37.5}},
9: {1: {'Rc': 75, 'VPL': 112}},
8: {0: {'Rc': 185, 'VPL': NA}},
7: {0: {'Rc': 370, 'VPL': NA}},
6: {
0: {'Rc': 926, 'VPL': NA},
1: {'Rc': 1111, 'VPL': NA},
},
5: {0: {'Rc': 1852, 'VPL': NA}},
4: {0: {'Rc': 3702, 'VPL': NA}},
3: {1: {'Rc': 7408, 'VPL': NA}},
2: {0: {'Rc': 14008, 'VPL': NA}},
1: {0: {'Rc': 37000, 'VPL': NA}},
0: {0: {'Rc': NA, 'VPL': NA}},
11: {0: {"Rc": 7.5, "VPL": 11}},
10: {0: {"Rc": 25, "VPL": 37.5}},
9: {1: {"Rc": 75, "VPL": 112}},
8: {0: {"Rc": 185, "VPL": NA}},
7: {0: {"Rc": 370, "VPL": NA}},
6: {0: {"Rc": 926, "VPL": NA}, 1: {"Rc": 1111, "VPL": NA}},
5: {0: {"Rc": 1852, "VPL": NA}},
4: {0: {"Rc": 3702, "VPL": NA}},
3: {1: {"Rc": 7408, "VPL": NA}},
2: {0: {"Rc": 14008, "VPL": NA}},
1: {0: {"Rc": 37000, "VPL": NA}},
0: {0: {"Rc": NA, "VPL": NA}},
}
NICv2 = {
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
11: {0: {'Rc': 7.5}},
10: {0: {'Rc': 25}},
9: {
2: {'Rc': 75},
3: {'Rc': 75},
},
8: {0: {'Rc': 185}},
7: {
0: {'Rc': 370},
3: {'Rc': 370},
},
6: {
0: {'Rc': 926},
1: {'Rc': 556},
2: {'Rc': 556},
3: {'Rc': 1111},
},
5: {0: {'Rc': 1852}},
4: {0: {'Rc': 3702}},
3: {3: {'Rc': 7408}},
2: {0: {'Rc': 14008}},
1: {0: {'Rc': 37000}},
0: {0: {'Rc': NA}},
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}},
}

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

@@ -0,0 +1,221 @@
from pyModeS import common
from textwrap import wrap
def uplink_icao(msg):
"""Calculate the ICAO address from a Mode-S interrogation (uplink message)"""
p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4)
data = int(msg[:-6], 16)
PA = int(msg[-6:], 16)
ad = 0
topbit = 0b1 << (len(msg) * 4 - 25)
for j in range(0, len(msg) * 4, 1):
if data & topbit:
data ^= p_gen
data = (data << 1) + ((PA >> 23) & 1)
PA = PA << 1
if j > (len(msg) * 4 - 26):
ad = ad + ((data >> (len(msg) * 4 - 25)) & 1)
ad = ad << 1
return "%06X" % (ad >> 2)
def uf(msg):
"""Decode Uplink Format value, bits 1 to 5."""
ufbin = common.hex2bin(msg[:2])
return min(common.bin2int(ufbin[0:5]), 24)
def bds(msg):
"""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(msg) 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) << 4) | ((mbytes[3] & 0xE0) >> 5)
BDS2 = RRS
else:
BDS2 = 0 # for other values of DI, the BDS2 is assumed 0 (as per ICAO Annex 10 Vol IV)
return str(BDS1) + str(BDS2)
else:
return None
else:
return None
def pr(msg):
"""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):
"""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
BDS2 = ""
if uf(msg) == 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(msg) in {4, 5, 20, 21}:
di = mbytes[1] & 0x7
RR = mbytes[1] >> 3 & 0x1F
if RR > 15:
BDS1 = RR - 16
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 = ""
LOS = ""
IC = ""
lockout = False
di = ""
RR = ""
RRS = ""
BDS1 = ""
BDS2 = ""
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) << 4) | ((mbytes[3] & 0xE0) >> 5)
BDS2 = RRS
return {
"DI": di,
"IC": IC,
"LOS": lockout,
"PR": PR,
"RR": RR,
"RRS": RRS,
"BDS": str(BDS1) + str(BDS2),
}

View File

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

View File

@@ -1,16 +1,21 @@
"""
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 = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
@@ -19,36 +24,37 @@ Speed conversion at altitude H[m] in ISA:
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.0 # 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.0 # 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.0 # 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
@@ -95,11 +101,13 @@ def distance(lat1, lon1, lat2, lon2, H=0):
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.0 + rho0 * Vcas * Vcas / (7.0 * p0)) ** 3.5 - 1.0)
Vtas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 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.0 + rho * Vtas * Vtas / (7.0 * p)) ** 3.5 - 1.0)
Vcas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
return Vcas

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

@@ -0,0 +1,197 @@
import time
import traceback
import numpy as np
import pyModeS as pms
try:
import rtlsdr
except:
print("------------------------------------------------------------------------")
print("! Warining: 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()

View File

@@ -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.disconnect()
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,21 @@ 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
datatype = sys.argv[3]
client = TcpClient(host=host, port=port, datatype=datatype)
client.run()

406
pyModeS/py_common.py Normal file
View File

@@ -0,0 +1,406 @@
from typing import Optional
import numpy as np
from textwrap import wrap
def hex2bin(hexstr: str) -> str:
"""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 hex2int(hexstr: str) -> int:
"""Convert a hexdecimal 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 hexdecimal 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.0 * 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

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

@@ -0,0 +1,288 @@
import os
import time
import datetime
import csv
import pyModeS as pms
class Decode:
def __init__(self, latlon=None, dumpto=None):
self.acs = dict()
if latlon is not None:
self.lat0 = float(latlon[0])
self.lon0 = float(latlon[1])
else:
self.lat0 = None
self.lon0 = None
self.t = 0
self.cache_timeout = 60 # seconds
if dumpto is not None and os.path.isdir(dumpto):
self.dumpto = dumpto
else:
self.dumpto = None
def process_raw(self, adsb_ts, adsb_msg, commb_ts, commb_msg, tnow=None):
"""process a chunk of adsb and commb messages received in the same
time period.
"""
if tnow is None:
tnow = time.time()
self.t = tnow
local_updated_acs_buffer = []
output_buffer = []
# process adsb message
for t, msg in zip(adsb_ts, adsb_msg):
icao = pms.icao(msg)
tc = pms.adsb.typecode(msg)
if icao not in self.acs:
self.acs[icao] = {
"live": None,
"call": None,
"lat": None,
"lon": None,
"alt": None,
"gs": None,
"trk": None,
"roc": None,
"tas": None,
"roll": None,
"rtrk": None,
"ias": None,
"mach": None,
"hdg": None,
"ver": None,
"HPL": None,
"RCu": None,
"RCv": None,
"HVE": None,
"VVE": None,
"Rc": None,
"VPL": None,
"EPU": None,
"VEPU": None,
"HFOMr": None,
"VFOMr": None,
"PE_RCu": None,
"PE_VPL": None,
}
self.acs[icao]["t"] = t
self.acs[icao]["live"] = int(t)
if 1 <= tc <= 4:
cs = pms.adsb.callsign(msg)
self.acs[icao]["call"] = cs
output_buffer.append([t, icao, "cs", cs])
if (5 <= tc <= 8) or (tc == 19):
vdata = pms.adsb.velocity(msg)
if vdata is None:
continue
spd, trk, roc, tag = vdata
if tag != "GS":
continue
if (spd is None) or (trk is None):
continue
self.acs[icao]["gs"] = spd
self.acs[icao]["trk"] = trk
self.acs[icao]["roc"] = roc
self.acs[icao]["tv"] = t
output_buffer.append([t, icao, "gs", spd])
output_buffer.append([t, icao, "trk", trk])
output_buffer.append([t, icao, "roc", roc])
if 5 <= tc <= 18:
oe = pms.adsb.oe_flag(msg)
self.acs[icao][oe] = msg
self.acs[icao]["t" + str(oe)] = t
if ("tpos" in self.acs[icao]) and (t - self.acs[icao]["tpos"] < 180):
# use single message decoding
rlat = self.acs[icao]["lat"]
rlon = self.acs[icao]["lon"]
latlon = pms.adsb.position_with_ref(msg, rlat, rlon)
elif (
("t0" in self.acs[icao])
and ("t1" in self.acs[icao])
and (abs(self.acs[icao]["t0"] - self.acs[icao]["t1"]) < 10)
):
# use multi message decoding
try:
latlon = pms.adsb.position(
self.acs[icao][0],
self.acs[icao][1],
self.acs[icao]["t0"],
self.acs[icao]["t1"],
self.lat0,
self.lon0,
)
except:
# mix of surface and airborne position message
continue
else:
latlon = None
if latlon is not None:
self.acs[icao]["tpos"] = t
self.acs[icao]["lat"] = latlon[0]
self.acs[icao]["lon"] = latlon[1]
alt = pms.adsb.altitude(msg)
self.acs[icao]["alt"] = alt
output_buffer.append([t, icao, "lat", latlon[0]])
output_buffer.append([t, icao, "lon", latlon[1]])
output_buffer.append([t, icao, "alt", alt])
local_updated_acs_buffer.append(icao)
# Uncertainty & accuracy
ac = self.acs[icao]
if 9 <= tc <= 18:
ac["nic_bc"] = pms.adsb.nic_b(msg)
if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22):
ac["HPL"], ac["RCu"], ac["RCv"] = pms.adsb.nuc_p(msg)
if (ac["ver"] == 1) and ("nic_s" in ac.keys()):
ac["Rc"], ac["VPL"] = pms.adsb.nic_v1(msg, ac["nic_s"])
elif (
(ac["ver"] == 2)
and ("nic_a" in ac.keys())
and ("nic_bc" in ac.keys())
):
ac["Rc"] = pms.adsb.nic_v2(msg, ac["nic_a"], ac["nic_bc"])
if tc == 19:
ac["HVE"], ac["VVE"] = pms.adsb.nuc_v(msg)
if ac["ver"] in [1, 2]:
ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg)
if tc == 29:
ac["PE_RCu"], ac["PE_VPL"], ac["base"] = pms.adsb.sil(msg, ac["ver"])
ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
if tc == 31:
ac["ver"] = pms.adsb.version(msg)
ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
ac["PE_RCu"], ac["PE_VPL"], ac["sil_base"] = pms.adsb.sil(
msg, ac["ver"]
)
if ac["ver"] == 1:
ac["nic_s"] = pms.adsb.nic_s(msg)
elif ac["ver"] == 2:
ac["nic_a"], ac["nic_bc"] = pms.adsb.nic_a_c(msg)
# process commb message
for t, msg in zip(commb_ts, commb_msg):
icao = pms.icao(msg)
if icao not in self.acs:
continue
self.acs[icao]["live"] = int(t)
bds = pms.bds.infer(msg)
if bds == "BDS50":
roll50 = pms.commb.roll50(msg)
trk50 = pms.commb.trk50(msg)
rtrk50 = pms.commb.rtrk50(msg)
gs50 = pms.commb.gs50(msg)
tas50 = pms.commb.tas50(msg)
self.acs[icao]["t50"] = t
if tas50:
self.acs[icao]["tas"] = tas50
output_buffer.append([t, icao, "tas50", tas50])
if roll50:
self.acs[icao]["roll"] = roll50
output_buffer.append([t, icao, "roll50", roll50])
if rtrk50:
self.acs[icao]["rtrk"] = rtrk50
output_buffer.append([t, icao, "rtrk50", rtrk50])
if trk50:
output_buffer.append([t, icao, "trk50", trk50])
if gs50:
output_buffer.append([t, icao, "gs50", gs50])
elif bds == "BDS60":
ias60 = pms.commb.ias60(msg)
hdg60 = pms.commb.hdg60(msg)
mach60 = pms.commb.mach60(msg)
roc60baro = pms.commb.vr60baro(msg)
roc60ins = pms.commb.vr60ins(msg)
if ias60 or hdg60 or mach60:
self.acs[icao]["t60"] = t
if ias60:
self.acs[icao]["ias"] = ias60
if hdg60:
self.acs[icao]["hdg"] = hdg60
if mach60:
self.acs[icao]["mach"] = mach60
if roc60baro:
output_buffer.append([t, icao, "roc60baro", roc60baro])
if roc60ins:
output_buffer.append([t, icao, "roc60ins", roc60ins])
# clear up old data
for icao in list(self.acs.keys()):
if self.t - self.acs[icao]["live"] > self.cache_timeout:
del self.acs[icao]
continue
if self.dumpto is not None:
dh = str(datetime.datetime.now().strftime("%Y%m%d_%H"))
fn = self.dumpto + "/pymodes_dump_%s.csv" % dh
output_buffer.sort(key=lambda x: x[0])
with open(fn, "a") as f:
writer = csv.writer(f)
writer.writerows(output_buffer)
return
def get_aircraft(self):
"""all aircraft that are stored in memory"""
acs = self.acs
return acs
def run(self, raw_pipe_out, ac_pipe_in, exception_queue):
local_buffer = []
while True:
try:
while raw_pipe_out.poll():
data = raw_pipe_out.recv()
local_buffer.append(data)
for data in local_buffer:
self.process_raw(
data["adsb_ts"],
data["adsb_msg"],
data["commb_ts"],
data["commb_msg"],
)
local_buffer = []
acs = self.get_aircraft()
ac_pipe_in.send(acs)
time.sleep(0.001)
except Exception as e:
tb = traceback.format_exc()
exception_queue.put((e, tb))

View File

@@ -1,120 +1,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)

View File

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

View File

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

View File

@@ -1,255 +0,0 @@
from __future__ import absolute_import, print_function, division
import os
import time
import datetime
import csv
import pyModeS as pms
class Stream():
def __init__(self, lat0, lon0, dumpto=None):
self.acs = dict()
self.lat0 = lat0
self.lon0 = lon0
self.t = 0
self.cache_timeout = 60 # seconds
if dumpto is not None and os.path.isdir(dumpto):
self.dumpto = dumpto
else:
self.dumpto = None
def process_raw(self, adsb_ts, adsb_msgs, commb_ts, commb_msgs, tnow=None):
"""process a chunk of adsb and commb messages recieved in the same
time period.
"""
if tnow is None:
tnow = time.time()
self.t = tnow
local_updated_acs_buffer = []
output_buffer = []
# process adsb message
for t, msg in zip(adsb_ts, adsb_msgs):
icao = pms.icao(msg)
tc = pms.adsb.typecode(msg)
if icao not in self.acs:
self.acs[icao] = {
'live': None,
'call': None,
'lat': None,
'lon': None,
'alt': None,
'gs': None,
'trk': None,
'roc': None,
'tas': None,
'roll': None,
'rtrk': None,
'ias': None,
'mach': None,
'hdg': None,
'ver' : None,
'HPL' : None,
'RCu' : None,
'RCv' : None,
'HVE' : None,
'VVE' : None,
'Rc' : None,
'VPL' : None,
'EPU' : None,
'VEPU' : None,
'HFOMr' : None,
'VFOMr' : None,
'PE_RCu' : None,
'PE_VPL' : None,
}
self.acs[icao]['t'] = t
self.acs[icao]['live'] = int(t)
if 1 <= tc <= 4:
cs = pms.adsb.callsign(msg)
self.acs[icao]['call'] = cs
output_buffer.append([t, icao, 'cs', cs])
if (5 <= tc <= 8) or (tc == 19):
vdata = pms.adsb.velocity(msg)
if vdata is None:
continue
spd, trk, roc, tag = vdata
if tag != 'GS':
continue
if (spd is None) or (trk is None):
continue
self.acs[icao]['gs'] = spd
self.acs[icao]['trk'] = trk
self.acs[icao]['roc'] = roc
self.acs[icao]['tv'] = t
output_buffer.append([t, icao, 'gs', spd])
output_buffer.append([t, icao, 'trk', trk])
output_buffer.append([t, icao, 'roc', roc])
if (5 <= tc <= 18):
oe = pms.adsb.oe_flag(msg)
self.acs[icao][oe] = msg
self.acs[icao]['t'+str(oe)] = t
if ('tpos' in self.acs[icao]) and (t - self.acs[icao]['tpos'] < 180):
# use single message decoding
rlat = self.acs[icao]['lat']
rlon = self.acs[icao]['lon']
latlon = pms.adsb.position_with_ref(msg, rlat, rlon)
elif ('t0' in self.acs[icao]) and ('t1' in self.acs[icao]) and \
(abs(self.acs[icao]['t0'] - self.acs[icao]['t1']) < 10):
# use multi message decoding
try:
latlon = pms.adsb.position(
self.acs[icao][0],
self.acs[icao][1],
self.acs[icao]['t0'],
self.acs[icao]['t1'],
self.lat0, self.lon0
)
except:
# mix of surface and airborne position message
continue
else:
latlon = None
if latlon is not None:
self.acs[icao]['tpos'] = t
self.acs[icao]['lat'] = latlon[0]
self.acs[icao]['lon'] = latlon[1]
alt = pms.adsb.altitude(msg)
self.acs[icao]['alt'] = alt
output_buffer.append([t, icao, 'lat', latlon[0]])
output_buffer.append([t, icao, 'lon', latlon[1]])
output_buffer.append([t, icao, 'alt', alt])
local_updated_acs_buffer.append(icao)
# Uncertainty & accuracy
ac = self.acs[icao]
if 9 <= tc <= 18:
ac['nic_bc'] = pms.adsb.nic_b(msg)
if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22):
ac['HPL'], ac['RCu'], ac['RCv'] = pms.adsb.nuc_p(msg)
if (ac['ver'] == 1) and ('nic_s' in ac.keys()):
ac['Rc'], ac['VPL'] = pms.adsb.nic_v1(msg, ac['nic_s'])
elif (ac['ver'] == 2) and ('nic_a' in ac.keys()) and ('nic_bc' in ac.keys()):
ac['Rc'] = pms.adsb.nic_v2(msg, ac['nic_a'], ac['nic_bc'])
if tc == 19:
ac['HVE'], ac['VVE'] = pms.adsb.nuc_v(msg)
if ac['ver'] in [1, 2]:
ac['HFOMr'], ac['VFOMr'] = pms.adsb.nac_v(msg)
if tc == 29:
ac['PE_RCu'], ac['PE_VPL'], ac['base'] = pms.adsb.sil(msg, ac['ver'])
ac['EPU'], ac['VEPU'] = pms.adsb.nac_p(msg)
if tc == 31:
ac['ver'] = pms.adsb.version(msg)
ac['EPU'], ac['VEPU'] = pms.adsb.nac_p(msg)
ac['PE_RCu'], ac['PE_VPL'], ac['sil_base'] = pms.adsb.sil(msg, ac['ver'])
if ac['ver'] == 1:
ac['nic_s'] = pms.adsb.nic_s(msg)
elif ac['ver'] == 2:
ac['nic_a'], ac['nic_bc'] = pms.adsb.nic_a_c(msg)
# process commb message
for t, msg in zip(commb_ts, commb_msgs):
icao = pms.icao(msg)
if icao not in self.acs:
continue
bds = pms.bds.infer(msg)
if bds == 'BDS50':
roll50 = pms.commb.roll50(msg)
trk50 = pms.commb.trk50(msg)
rtrk50 = pms.commb.rtrk50(msg)
gs50 = pms.commb.gs50(msg)
tas50 = pms.commb.tas50(msg)
self.acs[icao]['t50'] = t
if tas50:
self.acs[icao]['tas'] = tas50
output_buffer.append([t, icao, 'tas50', tas50])
if roll50:
self.acs[icao]['roll'] = roll50
output_buffer.append([t, icao, 'roll50', roll50])
if rtrk50:
self.acs[icao]['rtrk'] = rtrk50
output_buffer.append([t, icao, 'rtrk50', rtrk50])
if trk50:
output_buffer.append([t, icao, 'trk50', trk50])
if gs50:
output_buffer.append([t, icao, 'gs50', gs50])
elif bds == 'BDS60':
ias60 = pms.commb.ias60(msg)
hdg60 = pms.commb.hdg60(msg)
mach60 = pms.commb.mach60(msg)
roc60baro = pms.commb.vr60baro(msg)
roc60ins = pms.commb.vr60ins(msg)
if ias60 or hdg60 or mach60:
self.acs[icao]['t60'] = t
if ias60:
self.acs[icao]['ias'] = ias60
if hdg60:
self.acs[icao]['hdg'] = hdg60
if mach60:
self.acs[icao]['mach'] = mach60
if roc60baro:
output_buffer.append([t, icao, 'roc60baro', roc60baro])
if roc60ins:
output_buffer.append([t, icao, 'roc60ins', roc60ins])
# clear up old data
for icao in list(self.acs.keys()):
if self.t - self.acs[icao]['live'] > self.cache_timeout:
del self.acs[icao]
continue
if self.dumpto is not None:
dh = str(datetime.datetime.now().strftime("%Y%m%d_%H"))
fn = self.dumpto + '/pymodes_dump_%s.csv' % dh
output_buffer.sort(key=lambda x: x[0])
with open(fn, "a") as f:
writer = csv.writer(f)
writer.writerows(output_buffer)
return
def get_aircraft(self):
"""all aircraft that are stored in memeory"""
acs = self.acs
icaos = list(acs.keys())
for icao in icaos:
if acs[icao]['lat'] is None:
acs.pop(icao)
return acs

130
setup.py
View File

@@ -4,16 +4,16 @@ 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/*
"""
# 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 +21,41 @@ 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.8",
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"]),
install_requires=["numpy", "pyzmq"],
extras_require={"fast": ["Cython"]},
package_data={"pyModeS": ["*.pyx", "*.pxd"]},
scripts=["pyModeS/streamer/modeslive"],
)
try:
from setuptools.extension import Extension
from Cython.Build import cythonize
extensions = [Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"])]
setup(**dict(details, ext_modules=cythonize(extensions)))
except:
setup(**details)

130
tests/benchmark.py Normal file
View File

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

View File

@@ -1,41 +1,46 @@
from __future__ import print_function
from pyModeS import adsb, ehs
import sys
import time
import csv
if len(sys.argv) > 1 and sys.argv[1] == "cython":
from pyModeS.c_decoder import adsb
else:
from pyModeS.decoder import adsb
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))

View File

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

View File

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

13
tests/test_allcall.py Normal file
View File

@@ -0,0 +1,13 @@
from pyModeS import allcall
def test_icao():
assert allcall.icao("5D484FDEA248F5") == "484FDE"
def test_interrogator():
assert allcall.interrogator("5D484FDEA248F5") == 22
def test_capability():
assert allcall.capability("5D484FDEA248F5")[0] == 5

View File

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

60
tests/test_c_common.py Normal file
View File

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

View File

@@ -1,34 +1,36 @@
from pyModeS import bds, commb
# from pyModeS import ehs, els # deprecated
def test_bds20_callsign():
assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
assert commb.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert commb.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
def test_bds20_callsign():
assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == "KLM1017_"
assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == "IBK2873_"
assert commb.cs20("A000083E202CC371C31DE0AA1CCF") == "KLM1017_"
assert commb.cs20("A0001993202422F2E37CE038738E") == "IBK2873_"
def test_bds40_functions():
assert bds.bds40.alt40mcp("A000029C85E42F313000007047D3") == 3008
assert bds.bds40.alt40fms("A000029C85E42F313000007047D3") == 3008
assert bds.bds40.selalt40mcp("A000029C85E42F313000007047D3") == 3008
assert bds.bds40.selalt40fms("A000029C85E42F313000007047D3") == 3008
assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0
assert commb.alt40mcp("A000029C85E42F313000007047D3") == 3008
assert commb.alt40fms("A000029C85E42F313000007047D3") == 3008
assert commb.selalt40mcp("A000029C85E42F313000007047D3") == 3008
assert commb.selalt40fms("A000029C85E42F313000007047D3") == 3008
assert commb.p40baro("A000029C85E42F313000007047D3") == 1020.0
def test_bds50_functions():
assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1
assert bds.bds50.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
assert bds.bds50.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
assert bds.bds50.trk50("A000139381951536E024D4CCF6B5") == 114.258
assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
assert bds.bds50.tas50("A000139381951536E024D4CCF6B5") == 424
assert commb.roll50("A000139381951536E024D4CCF6B5") == 2.1
assert commb.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
assert commb.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
assert commb.trk50("A000139381951536E024D4CCF6B5") == 114.258
assert commb.gs50("A000139381951536E024D4CCF6B5") == 438
assert commb.rtrk50("A000139381951536E024D4CCF6B5") == 0.125

View File

@@ -1,42 +0,0 @@
from pyModeS import common
def test_hex2bin():
assert common.hex2bin('6E406B') == "011011100100000001101011"
def test_crc_decode():
checksum = common.crc("8D406B902015A678D4D220AA4BDA")
assert checksum == "000000000000000000000000"
def test_crc_encode():
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert common.hex2bin("AA4BDA") == parity
def test_icao():
assert common.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
assert common.icao("A0001839CA3800315800007448D9") == '400940'
assert common.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
assert common.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
def test_modes_altcode():
assert common.altcode("A02014B400000000000000F9D514") == 32300
def test_modes_idcode():
assert common.idcode("A800292DFFBBA9383FFCEB903D01") == '1346'
def test_graycode_to_altitude():
assert common.gray2alt('00000000010') == -1000
assert common.gray2alt('00000001010') == -500
assert common.gray2alt('00000011011') == -100
assert common.gray2alt('00000011010') == 0
assert common.gray2alt('00000011110') == 100
assert common.gray2alt('00000010011') == 600
assert common.gray2alt('00000110010') == 1000
assert common.gray2alt('00001001001') == 5800
assert common.gray2alt('00011100100') == 10300
assert common.gray2alt('01100011010') == 32000
assert common.gray2alt('01110000100') == 46300
assert common.gray2alt('01010101100') == 50200
assert common.gray2alt('11011110100') == 73200
assert common.gray2alt('10000000011') == 126600
assert common.gray2alt('10000000001') == 126700

64
tests/test_py_common.py Normal file
View File

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

22
tests/test_surv.py Normal file
View File

@@ -0,0 +1,22 @@
from pyModeS import surv
def test_fs():
assert surv.fs("2A00516D492B80")[0] == 2
def test_dr():
assert surv.dr("2A00516D492B80")[0] == 0
def test_um():
assert surv.um("200CBE4ED80137")[0] == 9
assert surv.um("200CBE4ED80137")[1] == 1
def test_identity():
assert surv.identity("2A00516D492B80") == "0356"
def test_altitude():
assert surv.altitude("20001718029FCD") == 36000

20
tests/test_tell.py Normal file
View File

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

View File

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