Compare commits
254 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb67c63326 | ||
|
|
bddaf9aec6 | ||
|
|
00fc1475ff | ||
|
|
489c405de0 | ||
|
|
ec2721cfdc | ||
|
|
2f13524a7c | ||
|
|
fb24d4f25c | ||
|
|
9ad535dc93 | ||
|
|
9a89766766 | ||
|
|
26173b4038 | ||
|
|
3619d52760 | ||
|
|
e2ece806c2 | ||
|
|
a683e40c41 | ||
|
|
0c1a3b06e1 | ||
|
|
f960cd71bc | ||
|
|
bd54ac1d10 | ||
|
|
2c1db13122 | ||
|
|
695fc34988 | ||
|
|
0eb333ba8c | ||
|
|
d058e9f8b3 | ||
|
|
3d99deb049 | ||
|
|
03f81d120b | ||
|
|
b9b95320d8 | ||
|
|
0ea2cf7ade | ||
|
|
458b02028d | ||
|
|
b062bdf998 | ||
|
|
cfcd21b692 | ||
|
|
86f302f05e | ||
|
|
28a6e53d49 | ||
|
|
81d7cef6e8 | ||
|
|
4906a49e9c | ||
|
|
7cb75ea8ca | ||
|
|
785584aff5 | ||
|
|
fdc34497c0 | ||
|
|
4a3c9438f7 | ||
|
|
b723374337 | ||
|
|
8f4dff5b30 | ||
|
|
7267859548 | ||
|
|
7a3bd089c4 | ||
|
|
17fdaca1c7 | ||
|
|
6f6b50776d | ||
|
|
0b7b9ad3dd | ||
|
|
24806f7e88 | ||
|
|
57ad40ec57 | ||
|
|
d230bdc4a9 | ||
|
|
4a6d3334a7 | ||
|
|
9f371fe86b | ||
|
|
0ea4969393 | ||
|
|
312c77629b | ||
|
|
6159691c3d | ||
|
|
7edbf3fd30 | ||
|
|
e19d0cd945 | ||
|
|
6fc68841ce | ||
|
|
f27fe6c8aa | ||
|
|
81a46e5070 | ||
|
|
6c2adbe990 | ||
|
|
3c14579040 | ||
|
|
fc9b05b6f1 | ||
|
|
b813e41343 | ||
|
|
c9159feb7d | ||
|
|
8cd5655a04 | ||
|
|
8ded3500d4 | ||
|
|
c348a2295d | ||
|
|
040a1879c9 | ||
|
|
61b55531e8 | ||
|
|
abdafd7dea | ||
|
|
b97299ce1b | ||
|
|
652ef65bbb | ||
|
|
72fe72c16c | ||
|
|
31df5c2042 | ||
|
|
10e9b234ad | ||
|
|
546d69c129 | ||
|
|
c4c285266c | ||
|
|
ebc11e5e84 | ||
|
|
f5c2b36209 | ||
|
|
759380b5a9 | ||
|
|
3f6389a67d | ||
|
|
c252647e77 | ||
|
|
1795d2f3c0 | ||
|
|
fb4cef7085 | ||
|
|
b24c1101b6 | ||
|
|
8a43d711aa | ||
|
|
495f320988 | ||
|
|
44a9c8d2aa | ||
|
|
ecfc4037a1 | ||
|
|
61f2191feb | ||
|
|
9e1cc2c5a2 | ||
|
|
500c3ca9bd | ||
|
|
78f1ca77c7 | ||
|
|
576713f13b | ||
|
|
3502fedf02 | ||
|
|
de9ec43912 | ||
|
|
bfdb8221a0 | ||
|
|
32e6ee3904 | ||
|
|
d82dfd5537 | ||
|
|
16c83d1505 | ||
|
|
e5d5633535 | ||
|
|
8adacd8e91 | ||
|
|
fd15b13c17 | ||
|
|
405d8ed108 | ||
|
|
0085d03d4a | ||
|
|
6f139d4ae9 | ||
|
|
e5ca76ac0d | ||
|
|
ef9d2cfd16 | ||
|
|
ea1ccc0c70 | ||
|
|
648f4660b7 | ||
|
|
0df6a664a3 | ||
|
|
44b277f0ad | ||
|
|
715d0a3c66 | ||
|
|
6db5ea8023 | ||
|
|
7685f1590f | ||
|
|
5fa090b95f | ||
|
|
6c5ae2141b | ||
|
|
24f3658673 | ||
|
|
205725872a | ||
|
|
01a573a1af | ||
|
|
c0476f5e16 | ||
|
|
140b68afbc | ||
|
|
70b3af2c8b | ||
|
|
0ef64be934 | ||
|
|
bbe6e50fb2 | ||
|
|
a3e44b5626 | ||
|
|
457a948879 | ||
|
|
54b2038a41 | ||
|
|
82b912bc05 | ||
|
|
6d5869a9e0 | ||
|
|
36840e0225 | ||
|
|
972ffe264e | ||
|
|
d4ca81e0ca | ||
|
|
9fa475ab9a | ||
|
|
c91bd4bb03 | ||
|
|
4f0946c4da | ||
|
|
46a99852d6 | ||
|
|
2389c12b98 | ||
|
|
6128c5a18d | ||
|
|
72c1a9f645 | ||
|
|
f221c67295 | ||
|
|
14a537030d | ||
|
|
b7afd841ff | ||
|
|
11b85b6959 | ||
|
|
8a54f927f6 | ||
|
|
9bfc116516 | ||
|
|
be38acabfd | ||
|
|
0b6d5576d3 | ||
|
|
dfdddb77f2 | ||
|
|
a872cd253e | ||
|
|
edbfdc68de | ||
|
|
f246c88dd6 | ||
|
|
e821b8fa77 | ||
|
|
1b3dcef659 | ||
|
|
7653b2459d | ||
|
|
7c52db318d | ||
|
|
362b92de7a | ||
|
|
ef8f4b3b7f | ||
|
|
773efff9bc | ||
|
|
05e34e7516 | ||
|
|
161ea31ee1 | ||
|
|
12506e7c7f | ||
|
|
fcd98d6bcc | ||
|
|
ef40acdbfd | ||
|
|
8b48fabf5a | ||
|
|
d91ad261cd | ||
|
|
4911e69171 | ||
|
|
5697e5b88e | ||
|
|
3073187d24 | ||
|
|
441cd27761 | ||
|
|
c4406ba276 | ||
|
|
de6238f5e9 | ||
|
|
711fd889e6 | ||
|
|
e692d30d66 | ||
|
|
d6a04865dc | ||
|
|
fd8bb8386f | ||
|
|
6baa218596 | ||
|
|
8a9045e730 | ||
|
|
d13e1bd12e | ||
|
|
2b1f2a5878 | ||
|
|
3538645e22 | ||
|
|
a9887d6238 | ||
|
|
7c8fd74db7 | ||
|
|
35b0d63fa9 | ||
|
|
2ae7bf4c19 | ||
|
|
e8449154ca | ||
|
|
fbe5b63286 | ||
|
|
45c32cd7aa | ||
|
|
c708d57fcc | ||
|
|
53a258bd35 | ||
|
|
d9e277dc54 | ||
|
|
854386fbd4 | ||
|
|
3117febac0 | ||
|
|
8693c51998 | ||
|
|
8c90371111 | ||
|
|
c6952e4e63 | ||
|
|
98e5d81ae1 | ||
|
|
fd557d1c40 | ||
|
|
cdbcf47bc2 | ||
|
|
5f7e28950c | ||
|
|
c1d0a925d5 | ||
|
|
b0a71717f0 | ||
|
|
25e5a4e412 | ||
|
|
d3022c6fe5 | ||
|
|
aa9f49b470 | ||
|
|
27daf52850 | ||
|
|
0e29a4d18a | ||
|
|
1e842e4789 | ||
|
|
1220368ada | ||
|
|
fb32ace095 | ||
|
|
abafd97b3f | ||
|
|
0a38231713 | ||
|
|
2fd822d275 | ||
|
|
8de58bb01f | ||
|
|
140f312c11 | ||
|
|
fefd26a787 | ||
|
|
1e82b5c59c | ||
|
|
a1615767b6 | ||
|
|
03a62dc68c | ||
|
|
46fee6b7dc | ||
|
|
8aa821c8dd | ||
|
|
621d3e7580 | ||
|
|
3b3609bf2b | ||
|
|
ef2268127c | ||
|
|
cc66e2f4e4 | ||
|
|
128163b41d | ||
|
|
8933afb1c1 | ||
|
|
15f2833aee | ||
|
|
2b3e2c62d0 | ||
|
|
cadcbb1756 | ||
|
|
b9c6db6f65 | ||
|
|
220b8e9716 | ||
|
|
3691d6f73d | ||
|
|
dad1cd89a9 | ||
|
|
3b3fc27a42 | ||
|
|
4bafa1de19 | ||
|
|
b648f4e7a5 | ||
|
|
a08c91a3a1 | ||
|
|
567fcda931 | ||
|
|
56dbb618c6 | ||
|
|
58447346aa | ||
|
|
bccc319856 | ||
|
|
375041717b | ||
|
|
23192a97fd | ||
|
|
240f706e81 | ||
|
|
168acfb88d | ||
|
|
70e9aa7c8d | ||
|
|
fc286299ec | ||
|
|
700b290047 | ||
|
|
55fbdcd029 | ||
|
|
7a62d1dbfb | ||
|
|
2f89d1a95c | ||
|
|
2fccacd724 | ||
|
|
556e499064 | ||
|
|
5518816b32 | ||
|
|
faf43134e5 | ||
|
|
4666de403d | ||
|
|
a196d673df |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
@@ -54,3 +55,12 @@ docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
|
||||
352
README.rst
352
README.rst
@@ -1,104 +1,328 @@
|
||||
A Python Mode-S Decoder
|
||||
=======================
|
||||
The Python ADS-B/Mode-S Decoder
|
||||
===============================
|
||||
|
||||
Python library for Mode-S message decoding. Two seprate methods are
|
||||
develop to decode the following messages:
|
||||
If you find this project useful for your research, please considering cite this tool as::
|
||||
|
||||
- Automatic Dependent Surveillance - Broadcast (ADS-B) (DF17)
|
||||
|
||||
- aircraft infomation that cotains: icao address, position,
|
||||
altitude, velocity (ground speed), and callsign, etc.
|
||||
|
||||
- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21)
|
||||
|
||||
- additional information in response to SSR interogation, such as:
|
||||
true airspeed, indicated airspeed, mach number, track angle,
|
||||
heading, and roll angle, etc.
|
||||
|
||||
A detailed manuel on Mode-S decoding is published by the author, at:
|
||||
http://adsb-decode-guide.readthedocs.org
|
||||
@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},
|
||||
}
|
||||
|
||||
|
||||
Source code
|
||||
|
||||
Introduction
|
||||
---------------------
|
||||
PyModeS is a Python library designed to decode Mode-S (including ADS-B) message. It can be imported to your python project or be used as a standalone tool to view and save live traffic data.
|
||||
|
||||
Messages with following Downlink Formats (DF) are supported:
|
||||
|
||||
**DF17 / DF18: Automatic Dependent Surveillance-Broadcast (ADS-B)**
|
||||
|
||||
- TC=1-4 / BDS 0,8: Aircraft identification and category
|
||||
- TC=5-8 / BDS 0,6: Surface position
|
||||
- TC=9-18 / BDS 0,5: Airborne position
|
||||
- TC=19 / BDS 0,9: Airborne velocity
|
||||
- TC=28 / BDS 6,1: Airborne status [to be implemented]
|
||||
- TC=29 / BDS 6,2: Target state and status information [to be implemented]
|
||||
- TC=31 / BDS 6,5: Aircraft operational status [to be implemented]
|
||||
|
||||
|
||||
**DF20 / DF21: Mode-S Comm-B replies**
|
||||
|
||||
- BDS 1,0: Data link capability report
|
||||
- BDS 1,7: Common usage GICB capability report
|
||||
- BDS 2,0: Aircraft identification
|
||||
- BDS 3,0: ACAS active resolution advisory
|
||||
- BDS 4,0: Selected vertical intention
|
||||
- BDS 4,4: Meteorological routine air report (experimental)
|
||||
- BDS 4,5: Meteorological hazard report (experimental)
|
||||
- BDS 5,0: Track and turn report
|
||||
- BDS 6,0: Heading and speed report
|
||||
|
||||
|
||||
**DF4 / DF20: Altitude code**
|
||||
|
||||
**DF5 / DF21: Identity code (squawk code)**
|
||||
|
||||
|
||||
Resources
|
||||
-----------
|
||||
Checkourt and contribute to this open source project at:
|
||||
Check out and contribute to this open-source project at:
|
||||
https://github.com/junzis/pyModeS
|
||||
|
||||
Detailed manual on Mode-S decoding is published at:
|
||||
https://mode-s.org/decode.
|
||||
|
||||
The API documentation of pyModeS is at:
|
||||
http://pymodes.readthedocs.io
|
||||
|
||||
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
Checkout source code, or install using pip:
|
||||
Installation examples::
|
||||
|
||||
::
|
||||
# stable version
|
||||
pip install pyModeS
|
||||
|
||||
pip install pyModeS
|
||||
# development version
|
||||
pip install git+https://github.com/junzis/pyModeS
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Dependencies ``numpy``, ``pyzmq`` and ``pyrtlsdr`` are installed automatically during previous installations processes.
|
||||
|
||||
|
||||
View live traffic (modeslive)
|
||||
----------------------------------------------------
|
||||
|
||||
General usage::
|
||||
|
||||
$ modeslive [-h] --source SOURCE [--connect SERVER PORT DATAYPE]
|
||||
[--latlon LAT LON] [--show-uncertainty] [--dumpto DUMPTO]
|
||||
|
||||
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
|
||||
|
||||
|
||||
Live with RTL-SDR
|
||||
*******************
|
||||
|
||||
If you have an RTL-SDR receiver plugged to the computer, you can connect it with ``rtlsdr`` source switch, shown as follows::
|
||||
|
||||
$ modeslive --source rtlsdr
|
||||
|
||||
|
||||
Live with network data
|
||||
***************************
|
||||
|
||||
If you want to connect to a TCP server that broadcast raw data. use can use ``net`` source switch, for example::
|
||||
|
||||
$ modeslive --source net --connect localhost 30002 raw
|
||||
$ modeslive --source net --connect 127.0.0.1 30005 beast
|
||||
|
||||
|
||||
|
||||
Example screenshot:
|
||||
|
||||
.. image:: https://github.com/junzis/pyModeS/raw/master/doc/modeslive-screenshot.png
|
||||
:width: 700px
|
||||
|
||||
|
||||
Use the library
|
||||
---------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pyModeS as pms
|
||||
import pyModeS as pms
|
||||
|
||||
|
||||
Common function for Mode-S message:
|
||||
Common functions
|
||||
*****************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.df(msg) # Downlink Format
|
||||
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
|
||||
pms.df(msg) # Downlink Format
|
||||
pms.icao(msg) # Infer the ICAO address from the message
|
||||
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
|
||||
|
||||
pms.hex2bin(str) # Convert hexadecimal string to binary string
|
||||
pms.bin2int(str) # Convert binary string to integer
|
||||
pms.hex2int(str) # Convert hexadecimal string to integer
|
||||
pms.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 integer
|
||||
|
||||
|
||||
Core functions for ADS-B decoding:
|
||||
Core functions for ADS-B decoding
|
||||
*********************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.adsb.icao(msg)
|
||||
pms.adsb.callsign(msg)
|
||||
pms.adsb.position(msg_odd, msg_even, t_odd, t_even)
|
||||
pms.adsb.altitude(msg)
|
||||
pms.adsb.velocity(msg)
|
||||
pms.adsb.speed_heading(msg)
|
||||
pms.adsb.icao(msg)
|
||||
pms.adsb.typecode(msg)
|
||||
|
||||
Core functions for EHS decoding:
|
||||
# Typecode 1-4
|
||||
pms.adsb.callsign(msg)
|
||||
|
||||
# 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)
|
||||
pms.adsb.surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
pms.adsb.altitude(msg)
|
||||
|
||||
# Typecode: 19
|
||||
pms.adsb.velocity(msg) # Handles both surface & airborne messages
|
||||
pms.adsb.speed_heading(msg) # Handles both surface & airborne messages
|
||||
pms.adsb.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 within 180NM (airborne) or 45NM (surface) of the true position.
|
||||
|
||||
|
||||
Decode altitude replies in DF4 / DF20
|
||||
**************************************
|
||||
.. code:: python
|
||||
|
||||
pms.common.altcode(msg) # Downlink format must be 4 or 20
|
||||
|
||||
|
||||
Decode identity replies in DF5 / DF21
|
||||
**************************************
|
||||
.. code:: python
|
||||
|
||||
pms.common.idcode(msg) # Downlink format must be 5 or 21
|
||||
|
||||
|
||||
|
||||
Common Mode-S functions
|
||||
************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.ehs.icao(msg) # icao address
|
||||
pms.ehs.BDS(msg) # Comm-B Data Selector Version
|
||||
pms.icao(msg) # Infer the ICAO address from the message
|
||||
pms.bds.infer(msg) # Infer the Modes-S BDS register
|
||||
|
||||
# for BDS version 2,0
|
||||
pms.ehs.callsign(msg) # Aircraft callsign
|
||||
# Check if BDS is 5,0 or 6,0, give reference speed, track, altitude (from ADS-B)
|
||||
pms.bds.is50or60(msg, spd_ref, trk_ref, alt_ref)
|
||||
|
||||
# for BDS version 4,0
|
||||
pms.ehs.alt_mcp(msg) # MCP/FCU selected altitude (ft)
|
||||
pms.ehs.alt_fms(msg) # FMS selected altitude (ft)
|
||||
pms.ehs.alt_pbaro(msg) # Barometric pressure (mb)
|
||||
# Check each BDS explicitly
|
||||
pms.bds.bds10.is10(msg)
|
||||
pms.bds.bds17.is17(msg)
|
||||
pms.bds.bds20.is20(msg)
|
||||
pms.bds.bds30.is30(msg)
|
||||
pms.bds.bds40.is40(msg)
|
||||
pms.bds.bds44.is44(msg)
|
||||
pms.bds.bds50.is50(msg)
|
||||
pms.bds.bds60.is60(msg)
|
||||
|
||||
# for BDS version 5,0
|
||||
pms.ehs.roll(msg) # roll angle (deg)
|
||||
pms.ehs.track(msg) # track angle (deg)
|
||||
pms.ehs.gs(msg) # ground speed (kt)
|
||||
pms.ehs.rtrack(msg) # track angle rate (deg/sec)
|
||||
pms.ehs.tas(msg) # true airspeed (kt)
|
||||
|
||||
# for BDS version 6,0
|
||||
pms.ehs.heading(msg) # heading (deg)
|
||||
pms.ehs.ias(msg) # indicated airspeed (kt)
|
||||
pms.ehs.mach(msg) # MACH number
|
||||
pms.ehs.baro_vr(msg) # barometric altitude rate (ft/min)
|
||||
pms.ehs.ins_vr(msg) # inertial vertical speed (ft/min)
|
||||
|
||||
Some helper functions:
|
||||
Mode-S Elementary Surveillance (ELS)
|
||||
*************************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
pms.df(msg) # downlink format of a Mode-S message
|
||||
pms.hex2bin(msg) # convert hexadecimal string to binary string
|
||||
pms.hex2int(msg) # convert hexadecimal string to integer
|
||||
pms.bin2int(msg) # convert binary string to integer
|
||||
pms.commb.ovc10(msg) # Overlay capability, BDS 1,0
|
||||
pms.commb.cap17(msg) # GICB capability, BDS 1,7
|
||||
pms.commb.cs20(msg) # Callsign, BDS 2,0
|
||||
|
||||
|
||||
Mode-S Enhanced Surveillance (EHS)
|
||||
***********************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# BDS 6,0
|
||||
pms.commb.hdg60(msg) # Magnetic heading (deg)
|
||||
pms.commb.ias60(msg) # Indicated airspeed (kt)
|
||||
pms.commb.mach60(msg) # Mach number (-)
|
||||
pms.commb.vr60baro(msg) # Barometric altitude rate (ft/min)
|
||||
pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min)
|
||||
|
||||
|
||||
Meteorological routine air report (MRAR) [Experimental]
|
||||
********************************************************
|
||||
|
||||
.. code:: python
|
||||
|
||||
# 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) [Experimental]
|
||||
*******************************************************
|
||||
|
||||
.. 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. First, install ``tox`` through pip. Then, run the following commands:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ tox
|
||||
|
||||
21
doc/Makefile
Normal file
21
doc/Makefile
Normal file
@@ -0,0 +1,21 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# 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
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
7
doc/README.rst
Normal file
7
doc/README.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
How to generate the apidoc
|
||||
====================================
|
||||
|
||||
::
|
||||
|
||||
cd doc
|
||||
make html
|
||||
BIN
doc/modeslive-screenshot.png
Normal file
BIN
doc/modeslive-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
188
doc/source/conf.py
Normal file
188
doc/source/conf.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# -*- 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.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'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 = ['']
|
||||
|
||||
# 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'
|
||||
|
||||
# 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
|
||||
30
doc/source/index.rst
Normal file
30
doc/source/index.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
.. 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::
|
||||
:maxdepth: 3
|
||||
|
||||
pyModeS.decoder
|
||||
pyModeS.streamer
|
||||
pyModeS.extra
|
||||
|
||||
|
||||
----
|
||||
|
||||
.. include:: ../../README.rst
|
||||
|
||||
----
|
||||
|
||||
Indices and tables
|
||||
**********************
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
35
doc/source/make.bat
Normal file
35
doc/source/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
7
doc/source/modules.rst
Normal file
7
doc/source/modules.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS
|
||||
=======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyModeS
|
||||
7
doc/source/pyModeS.decoder.acas.rst
Normal file
7
doc/source/pyModeS.decoder.acas.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.acas module
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.acas
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.adsb.rst
Normal file
7
doc/source/pyModeS.decoder.adsb.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.adsb module
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.adsb
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.allcall.rst
Normal file
7
doc/source/pyModeS.decoder.allcall.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.allcall module
|
||||
==============================
|
||||
|
||||
.. automodule:: pyModeS.decoder.allcall
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds05.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds05.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds05 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds05
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds06.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds06.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds06 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds06
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds08.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds08.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds08 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds08
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds09.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds09.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds09 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds09
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds10.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds10.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds10 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds10
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds17.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds17.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds17 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds17
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds20.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds20.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds20 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds20
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds30.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds30.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds30 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds30
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds40.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds40.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds40 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds40
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds44.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds44.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds44 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds44
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds45.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds45.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds45 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds45
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds50.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds50.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds50 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds50
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds53.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds53.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds53 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds53
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.bds.bds60.rst
Normal file
7
doc/source/pyModeS.decoder.bds.bds60.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.bds.bds60 module
|
||||
================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds.bds60
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
28
doc/source/pyModeS.decoder.bds.rst
Normal file
28
doc/source/pyModeS.decoder.bds.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pyModeS.decoder.bds package
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.bds
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyModeS.decoder.bds.bds05
|
||||
pyModeS.decoder.bds.bds06
|
||||
pyModeS.decoder.bds.bds08
|
||||
pyModeS.decoder.bds.bds09
|
||||
pyModeS.decoder.bds.bds10
|
||||
pyModeS.decoder.bds.bds17
|
||||
pyModeS.decoder.bds.bds20
|
||||
pyModeS.decoder.bds.bds30
|
||||
pyModeS.decoder.bds.bds40
|
||||
pyModeS.decoder.bds.bds44
|
||||
pyModeS.decoder.bds.bds45
|
||||
pyModeS.decoder.bds.bds50
|
||||
pyModeS.decoder.bds.bds53
|
||||
pyModeS.decoder.bds.bds60
|
||||
|
||||
7
doc/source/pyModeS.decoder.commb.rst
Normal file
7
doc/source/pyModeS.decoder.commb.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.commb module
|
||||
============================
|
||||
|
||||
.. automodule:: pyModeS.decoder.commb
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.common.rst
Normal file
7
doc/source/pyModeS.decoder.common.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.common module
|
||||
=============================
|
||||
|
||||
.. automodule:: pyModeS.decoder.common
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.ehs.rst
Normal file
7
doc/source/pyModeS.decoder.ehs.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.ehs module
|
||||
==========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.ehs
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.els.rst
Normal file
7
doc/source/pyModeS.decoder.els.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.els module
|
||||
==========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.els
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
30
doc/source/pyModeS.decoder.rst
Normal file
30
doc/source/pyModeS.decoder.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
pyModeS.decoder package
|
||||
=======================
|
||||
|
||||
.. automodule:: pyModeS.decoder
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyModeS.decoder.bds
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyModeS.decoder.acas
|
||||
pyModeS.decoder.adsb
|
||||
pyModeS.decoder.allcall
|
||||
pyModeS.decoder.commb
|
||||
pyModeS.decoder.common
|
||||
pyModeS.decoder.ehs
|
||||
pyModeS.decoder.els
|
||||
pyModeS.decoder.surv
|
||||
pyModeS.decoder.uncertainty
|
||||
|
||||
7
doc/source/pyModeS.decoder.surv.rst
Normal file
7
doc/source/pyModeS.decoder.surv.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.surv module
|
||||
===========================
|
||||
|
||||
.. automodule:: pyModeS.decoder.surv
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.decoder.uncertainty.rst
Normal file
7
doc/source/pyModeS.decoder.uncertainty.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.decoder.uncertainty module
|
||||
==================================
|
||||
|
||||
.. automodule:: pyModeS.decoder.uncertainty
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.extra.aero.rst
Normal file
7
doc/source/pyModeS.extra.aero.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.extra.aero module
|
||||
=========================
|
||||
|
||||
.. automodule:: pyModeS.extra.aero
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
16
doc/source/pyModeS.extra.rst
Normal file
16
doc/source/pyModeS.extra.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
pyModeS.extra package
|
||||
=====================
|
||||
|
||||
.. automodule:: pyModeS.extra
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyModeS.extra.aero
|
||||
pyModeS.extra.tcpclient
|
||||
|
||||
7
doc/source/pyModeS.extra.tcpclient.rst
Normal file
7
doc/source/pyModeS.extra.tcpclient.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.extra.tcpclient module
|
||||
==============================
|
||||
|
||||
.. automodule:: pyModeS.extra.tcpclient
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
17
doc/source/pyModeS.rst
Normal file
17
doc/source/pyModeS.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
pyModeS package
|
||||
===============
|
||||
|
||||
.. automodule:: pyModeS
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyModeS.decoder
|
||||
pyModeS.extra
|
||||
pyModeS.streamer
|
||||
|
||||
16
doc/source/pyModeS.streamer.rst
Normal file
16
doc/source/pyModeS.streamer.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
pyModeS.streamer package
|
||||
========================
|
||||
|
||||
.. automodule:: pyModeS.streamer
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyModeS.streamer.screen
|
||||
pyModeS.streamer.stream
|
||||
|
||||
7
doc/source/pyModeS.streamer.screen.rst
Normal file
7
doc/source/pyModeS.streamer.screen.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.streamer.screen module
|
||||
==============================
|
||||
|
||||
.. automodule:: pyModeS.streamer.screen
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
doc/source/pyModeS.streamer.stream.rst
Normal file
7
doc/source/pyModeS.streamer.stream.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
pyModeS.streamer.stream module
|
||||
==============================
|
||||
|
||||
.. automodule:: pyModeS.streamer.stream
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -1,3 +1,17 @@
|
||||
from util import *
|
||||
import adsb
|
||||
import ehs
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from .decoder.common import *
|
||||
from .decoder import tell
|
||||
from .decoder import adsb
|
||||
from .decoder import commb
|
||||
from .decoder import common
|
||||
from .decoder import bds
|
||||
from .extra import aero
|
||||
from .extra import tcpclient
|
||||
|
||||
warnings.simplefilter("once", DeprecationWarning)
|
||||
|
||||
dirpath = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
361
pyModeS/adsb.py
361
pyModeS/adsb.py
@@ -1,361 +0,0 @@
|
||||
"""
|
||||
A python package for decoding ABS-D messages.
|
||||
|
||||
Copyright (C) 2015 Junzi Sun (TU Delft)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import math
|
||||
import util
|
||||
from util import crc
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Get the downlink format (DF) number
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: DF number
|
||||
"""
|
||||
return util.df(msg)
|
||||
|
||||
|
||||
def icao(msg):
|
||||
"""Get the ICAO 24 bits address, bytes 3 to 8.
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
String: ICAO address in 6 bytes hexadecimal string
|
||||
"""
|
||||
return msg[2:8]
|
||||
|
||||
|
||||
def data(msg):
|
||||
"""Return the data frame in the message, bytes 9 to 22"""
|
||||
return msg[8:22]
|
||||
|
||||
|
||||
def typecode(msg):
|
||||
"""Type code of ADS-B message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: type code number
|
||||
"""
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[32:37])
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Aircraft Identification
|
||||
# ---------------------------------------------
|
||||
def category(msg):
|
||||
"""Aircraft category number
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: category number
|
||||
"""
|
||||
|
||||
if typecode(msg) < 1 or typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[5:8])
|
||||
|
||||
|
||||
def callsign(msg):
|
||||
"""Aircraft callsign
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
string: callsign
|
||||
"""
|
||||
|
||||
if typecode(msg) < 1 or typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
|
||||
msgbin = util.hex2bin(msg)
|
||||
csbin = msgbin[40:96]
|
||||
|
||||
cs = ''
|
||||
cs += chars[util.bin2int(csbin[0:6])]
|
||||
cs += chars[util.bin2int(csbin[6:12])]
|
||||
cs += chars[util.bin2int(csbin[12:18])]
|
||||
cs += chars[util.bin2int(csbin[18:24])]
|
||||
cs += chars[util.bin2int(csbin[24:30])]
|
||||
cs += chars[util.bin2int(csbin[30:36])]
|
||||
cs += chars[util.bin2int(csbin[36:42])]
|
||||
cs += chars[util.bin2int(csbin[42:48])]
|
||||
|
||||
# clean string, remove spaces and marks, if any.
|
||||
# cs = cs.replace('_', '')
|
||||
cs = cs.replace('#', '')
|
||||
return cs
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Positions
|
||||
# ---------------------------------------------
|
||||
|
||||
def oe_flag(msg):
|
||||
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: 0 or 1, for even or odd frame
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
return int(msgbin[53])
|
||||
|
||||
|
||||
def cprlat(msg):
|
||||
"""CPR encoded latitude
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: encoded latitude
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[54:71])
|
||||
|
||||
|
||||
def cprlon(msg):
|
||||
"""CPR encoded longitude
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: encoded longitude
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
return util.bin2int(msgbin[71:88])
|
||||
|
||||
|
||||
def position(msg0, msg1, t0, t1):
|
||||
"""Decode position from the combination of even and odd position message
|
||||
131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
if typecode(msg0) < 5 or typecode(msg0) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg0)
|
||||
|
||||
if typecode(msg1) < 5 or typecode(msg1) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg1)
|
||||
|
||||
msgbin0 = util.hex2bin(msg0)
|
||||
msgbin1 = util.hex2bin(msg1)
|
||||
|
||||
cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0
|
||||
cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0
|
||||
cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0
|
||||
cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0
|
||||
|
||||
air_d_lat_even = 360.0 / 60
|
||||
air_d_lat_odd = 360.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = int(math.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5))
|
||||
|
||||
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
if lat_even >= 270:
|
||||
lat_even = lat_even - 360
|
||||
|
||||
if lat_odd >= 270:
|
||||
lat_odd = lat_odd - 360
|
||||
|
||||
# check if both are in the same latidude zone, exit if not
|
||||
if _cprNL(lat_even) != _cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if (t0 > t1):
|
||||
ni = _cprN(lat_even, 0)
|
||||
m = math.floor(cprlon_even * (_cprNL(lat_even)-1) -
|
||||
cprlon_odd * _cprNL(lat_even) + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_even)
|
||||
lat = lat_even
|
||||
else:
|
||||
ni = _cprN(lat_odd, 1)
|
||||
m = math.floor(cprlon_even * (_cprNL(lat_odd)-1) -
|
||||
cprlon_odd * _cprNL(lat_odd) + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_odd)
|
||||
lat = lat_odd
|
||||
|
||||
if lon > 180:
|
||||
lon = lon - 360
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def _cprN(lat, is_odd):
|
||||
nl = _cprNL(lat) - is_odd
|
||||
return nl if nl > 1 else 1
|
||||
|
||||
|
||||
def _cprNL(lat):
|
||||
try:
|
||||
nz = 60
|
||||
a = 1 - math.cos(math.pi * 2 / nz)
|
||||
b = math.cos(math.pi / 180.0 * abs(lat)) ** 2
|
||||
nl = 2 * math.pi / (math.acos(1 - a/b))
|
||||
return int(nl)
|
||||
except:
|
||||
# happens when latitude is +/-90 degree
|
||||
return 1
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
if typecode(msg) < 9 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
q = msgbin[47]
|
||||
if q:
|
||||
n = util.bin2int(msgbin[40:47]+msgbin[48:52])
|
||||
alt = n * 25 - 1000
|
||||
return alt
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def nic(msg):
|
||||
"""Calculate NIC, navigation integrity category
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: NIC number (from 0 to 11), -1 if not applicable
|
||||
"""
|
||||
if typecode(msg) < 9 or typecode(msg) > 18:
|
||||
raise RuntimeError("%s: Not a airborne position message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
tc = typecode(msg)
|
||||
nic_sup_b = util.bin2int(msgbin[39])
|
||||
|
||||
if tc in [0, 18, 22]:
|
||||
nic = 0
|
||||
elif tc == 17:
|
||||
nic = 1
|
||||
elif tc == 16:
|
||||
if nic_sup_b:
|
||||
nic = 3
|
||||
else:
|
||||
nic = 2
|
||||
elif tc == 15:
|
||||
nic = 4
|
||||
elif tc == 14:
|
||||
nic = 5
|
||||
elif tc == 13:
|
||||
nic = 6
|
||||
elif tc == 12:
|
||||
nic = 7
|
||||
elif tc == 11:
|
||||
if nic_sup_b:
|
||||
nic = 9
|
||||
else:
|
||||
nic = 8
|
||||
elif tc in [10, 21]:
|
||||
nic = 10
|
||||
elif tc in [9, 20]:
|
||||
nic = 11
|
||||
else:
|
||||
nic = -1
|
||||
return nic
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Velocity
|
||||
# ---------------------------------------------
|
||||
|
||||
def velocity(msg):
|
||||
"""Calculate the speed, heading, and vertical rate
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
(int, float, int, string): speed (kt), heading (degree),
|
||||
rate of climb/descend (ft/min), and speed type
|
||||
('GS' for ground speed, 'AS' for airspeed)
|
||||
"""
|
||||
|
||||
if typecode(msg) != 19:
|
||||
raise RuntimeError("%s: Not a airborne velocity message" % msg)
|
||||
|
||||
msgbin = util.hex2bin(msg)
|
||||
|
||||
subtype = util.bin2int(msgbin[37:40])
|
||||
|
||||
if subtype in (1, 2):
|
||||
v_ew_sign = util.bin2int(msgbin[45])
|
||||
v_ew = util.bin2int(msgbin[46:56]) - 1 # east-west velocity
|
||||
|
||||
v_ns_sign = util.bin2int(msgbin[56])
|
||||
v_ns = util.bin2int(msgbin[57:67]) - 1 # north-south velocity
|
||||
|
||||
v_we = -1*v_ew if v_ew_sign else v_ew
|
||||
v_sn = -1*v_ns if v_ns_sign else v_ns
|
||||
|
||||
spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts
|
||||
|
||||
hdg = math.atan2(v_we, v_sn)
|
||||
hdg = math.degrees(hdg) # convert to degrees
|
||||
hdg = hdg if hdg >= 0 else hdg + 360 # no negative val
|
||||
|
||||
tag = 'GS'
|
||||
|
||||
else:
|
||||
hdg = util.bin2int(msgbin[46:56]) / 1024.0 * 360.0
|
||||
spd = util.bin2int(msgbin[57:67])
|
||||
|
||||
tag = 'AS'
|
||||
|
||||
vr_sign = util.bin2int(msgbin[68])
|
||||
vr = util.bin2int(msgbin[68:77]) # vertical rate
|
||||
rocd = -1*vr if vr_sign else vr # rate of climb/descend
|
||||
|
||||
return int(spd), round(hdg, 1), int(rocd), tag
|
||||
|
||||
|
||||
def speed_heading(msg):
|
||||
"""Get speed and heading only from the velocity message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
(int, float): speed (kt), heading (degree)
|
||||
"""
|
||||
spd, hdg, rocd, tag = velocity(msg)
|
||||
return spd, hdg
|
||||
145
pyModeS/decoder/__init__.py
Normal file
145
pyModeS/decoder/__init__.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
from pyModeS.decoder import adsb, commb, common, bds
|
||||
|
||||
|
||||
def tell(msg):
|
||||
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")
|
||||
23
pyModeS/decoder/acas.py
Normal file
23
pyModeS/decoder/acas.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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
|
||||
|
||||
[To be implemented]
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
558
pyModeS/decoder/adsb.py
Normal file
558
pyModeS/decoder/adsb.py
Normal file
@@ -0,0 +1,558 @@
|
||||
# Copyright (C) 2015 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""ADS-B Wrapper.
|
||||
|
||||
The ADS-B wrapper also imports functions from the following modules:
|
||||
|
||||
- pyModeS.decoder.bds.bds05
|
||||
Functions: ``airborne_position``, ``airborne_position_with_ref``, ``altitude``
|
||||
- pyModeS.decoder.bds.bds06
|
||||
Functions: ``surface_position``, ``surface_position_with_ref``, ``surface_velocity``
|
||||
- pyModeS.decoder.bds.bds08
|
||||
Functions: ``category``, ``callsign``
|
||||
- pyModeS.decoder.bds.bds09
|
||||
Functions: ``airborne_velocity``, ``altitude_diff``
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
import pyModeS as pms
|
||||
from pyModeS.decoder import common
|
||||
from pyModeS.decoder import uncertainty
|
||||
|
||||
# from pyModeS.decoder.bds import bds05, bds06, bds09
|
||||
from pyModeS.decoder.bds.bds05 import (
|
||||
airborne_position,
|
||||
airborne_position_with_ref,
|
||||
altitude,
|
||||
)
|
||||
from pyModeS.decoder.bds.bds06 import (
|
||||
surface_position,
|
||||
surface_position_with_ref,
|
||||
surface_velocity,
|
||||
)
|
||||
from pyModeS.decoder.bds.bds08 import category, callsign
|
||||
from pyModeS.decoder.bds.bds09 import airborne_velocity, altitude_diff
|
||||
|
||||
|
||||
def df(msg):
|
||||
return common.df(msg)
|
||||
|
||||
|
||||
def icao(msg):
|
||||
return common.icao(msg)
|
||||
|
||||
|
||||
def typecode(msg):
|
||||
return common.typecode(msg)
|
||||
|
||||
|
||||
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
|
||||
"""Decode position from a pair of even and odd position message
|
||||
(works with both airborne and surface position messages)
|
||||
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
tc0 = typecode(msg0)
|
||||
tc1 = typecode(msg1)
|
||||
|
||||
if 5 <= tc0 <= 8 and 5 <= tc1 <= 8:
|
||||
if (not lat_ref) or (not lon_ref):
|
||||
raise RuntimeError(
|
||||
"Surface position encountered, a reference \
|
||||
position lat/lon required. Location of \
|
||||
receiver can be used."
|
||||
)
|
||||
else:
|
||||
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
|
||||
|
||||
elif 9 <= tc0 <= 18 and 9 <= tc1 <= 18:
|
||||
# Airborne position with barometric height
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
elif 20 <= tc0 <= 22 and 20 <= tc1 <= 22:
|
||||
# Airborne position with GNSS height
|
||||
return airborne_position(msg0, msg1, t0, t1)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistent message types")
|
||||
|
||||
|
||||
def position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode position with only one message,
|
||||
knowing reference nearby location, such as previously
|
||||
calculated location, ground station, or airport location, etc.
|
||||
Works with both airborne and surface position messages.
|
||||
The reference position shall be with in 180NM (airborne) or 45NM (surface)
|
||||
of the true position.
|
||||
|
||||
Args:
|
||||
msg (string): even message (28 bytes hexadecimal string)
|
||||
lat_ref: previous known latitude
|
||||
lon_ref: previous known longitude
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
tc = typecode(msg)
|
||||
|
||||
if 5 <= tc <= 8:
|
||||
return surface_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
elif 9 <= tc <= 18 or 20 <= tc <= 22:
|
||||
return airborne_position_with_ref(msg, lat_ref, lon_ref)
|
||||
|
||||
else:
|
||||
raise RuntimeError("incorrect or inconsistent message types")
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc < 5 or tc == 19 or tc > 22:
|
||||
raise RuntimeError("%s: Not a position message" % msg)
|
||||
|
||||
if tc >= 5 and tc <= 8:
|
||||
# surface position, altitude 0
|
||||
return 0
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
q = msgbin[47]
|
||||
if q:
|
||||
n = common.bin2int(msgbin[40:47] + msgbin[48:52])
|
||||
alt = n * 25 - 1000
|
||||
return alt
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def velocity(msg, rtn_sources=False):
|
||||
"""Calculate the speed, heading, and vertical rate
|
||||
(handles both airborne or surface message)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
rtn_source (boolean): If the function will return
|
||||
the sources for direction of travel and vertical
|
||||
rate. This will change the return value from a four
|
||||
element array to a six element array.
|
||||
|
||||
Returns:
|
||||
(int, float, int, string, string, string): speed (kt),
|
||||
ground track or heading (degree),
|
||||
rate of climb/descent (ft/min), speed type
|
||||
('GS' for ground speed, 'AS' for airspeed),
|
||||
direction source ('true_north' for ground track / true north
|
||||
as reference, 'mag_north' for magnetic north as reference),
|
||||
rate of climb/descent source ('Baro' for barometer, 'GNSS'
|
||||
for GNSS constellation).
|
||||
|
||||
In the case of surface messages, None will be put in place
|
||||
for vertical rate and its respective sources.
|
||||
"""
|
||||
|
||||
if 5 <= typecode(msg) <= 8:
|
||||
return surface_velocity(msg, rtn_sources)
|
||||
|
||||
elif typecode(msg) == 19:
|
||||
return airborne_velocity(msg, rtn_sources)
|
||||
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"incorrect or inconsistent message types, expecting 4<TC<9 or TC=19"
|
||||
)
|
||||
|
||||
|
||||
def speed_heading(msg):
|
||||
"""Get speed and ground track (or heading) from the velocity message
|
||||
(handles both airborne or surface message)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(int, float): speed (kt), ground track or heading (degree)
|
||||
"""
|
||||
spd, trk_or_hdg, rocd, tag = velocity(msg)
|
||||
return spd, trk_or_hdg
|
||||
|
||||
|
||||
def oe_flag(msg):
|
||||
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: 0 or 1, for even or odd frame
|
||||
"""
|
||||
msgbin = common.hex2bin(msg)
|
||||
return int(msgbin[53])
|
||||
|
||||
|
||||
def version(msg):
|
||||
"""ADS-B Version
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 31
|
||||
|
||||
Returns:
|
||||
int: version number
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 31:
|
||||
raise RuntimeError(
|
||||
"%s: Not a status operation message, expecting TC = 31" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
version = common.bin2int(msgbin[72:75])
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def nuc_p(msg):
|
||||
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string,
|
||||
|
||||
Returns:
|
||||
int: Horizontal Protection Limit
|
||||
int: 95% Containment Radius - Horizontal (meters)
|
||||
int: 95% Containment Radius - Vertical (meters)
|
||||
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)"
|
||||
% msg
|
||||
)
|
||||
|
||||
try:
|
||||
NUCp = uncertainty.TC_NUCp_lookup[tc]
|
||||
HPL = uncertainty.NUCp[NUCp]["HPL"]
|
||||
RCu = uncertainty.NUCp[NUCp]["RCu"]
|
||||
RCv = uncertainty.NUCp[NUCp]["RCv"]
|
||||
except KeyError:
|
||||
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
|
||||
|
||||
if tc in [20, 21]:
|
||||
RCv = uncertainty.NA
|
||||
|
||||
return HPL, RCu, RCv
|
||||
|
||||
|
||||
def nuc_v(msg):
|
||||
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string,
|
||||
|
||||
Returns:
|
||||
int or string: 95% Horizontal Velocity Error
|
||||
int or string: 95% Vertical Velocity Error
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError(
|
||||
"%s: Not an airborne velocity message, expecting TC = 19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
NUCv = common.bin2int(msgbin[42:45])
|
||||
|
||||
try:
|
||||
HVE = uncertainty.NUCv[NUCv]["HVE"]
|
||||
VVE = uncertainty.NUCv[NUCv]["VVE"]
|
||||
except KeyError:
|
||||
HVE, VVE = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return HVE, VVE
|
||||
|
||||
|
||||
def nic_v1(msg, NICs):
|
||||
"""Calculate NIC, navigation integrity category, for ADS-B version 1
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
NICs (int or string): NIC supplement
|
||||
|
||||
Returns:
|
||||
int or string: Horizontal Radius of Containment
|
||||
int or string: Vertical Protection Limit
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)"
|
||||
% msg
|
||||
)
|
||||
|
||||
tc = typecode(msg)
|
||||
NIC = uncertainty.TC_NICv1_lookup[tc]
|
||||
|
||||
if isinstance(NIC, dict):
|
||||
NIC = NIC[NICs]
|
||||
|
||||
try:
|
||||
Rc = uncertainty.NICv1[NIC][NICs]["Rc"]
|
||||
VPL = uncertainty.NICv1[NIC][NICs]["VPL"]
|
||||
except KeyError:
|
||||
Rc, VPL = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return Rc, VPL
|
||||
|
||||
|
||||
def nic_v2(msg, NICa, NICbc):
|
||||
"""Calculate NIC, navigation integrity category, for ADS-B version 2
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
NICa (int or string): NIC supplement - A
|
||||
NICbc (int or srting): NIC supplement - B or C
|
||||
|
||||
Returns:
|
||||
int or string: Horizontal Radius of Containment
|
||||
"""
|
||||
if typecode(msg) < 5 or typecode(msg) > 22:
|
||||
raise RuntimeError(
|
||||
"%s: Not a surface position message (5<TC<8), \
|
||||
airborne position message (8<TC<19), \
|
||||
or airborne position with GNSS height (20<TC<22)"
|
||||
% msg
|
||||
)
|
||||
|
||||
tc = typecode(msg)
|
||||
NIC = uncertainty.TC_NICv2_lookup[tc]
|
||||
|
||||
if 20 <= tc <= 22:
|
||||
NICs = 0
|
||||
else:
|
||||
NICs = NICa * 2 + NICbc
|
||||
|
||||
try:
|
||||
if isinstance(NIC, dict):
|
||||
NIC = NIC[NICs]
|
||||
|
||||
Rc = uncertainty.NICv2[NIC][NICs]["Rc"]
|
||||
except KeyError:
|
||||
Rc = uncertainty.NA
|
||||
|
||||
return Rc
|
||||
|
||||
|
||||
def nic_s(msg):
|
||||
"""Obtain NIC supplement bit, TC=31 message
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: NICs number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 31:
|
||||
raise RuntimeError(
|
||||
"%s: Not a status operation message, expecting TC = 31" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_s = int(msgbin[75])
|
||||
|
||||
return nic_s
|
||||
|
||||
|
||||
def nic_a_c(msg):
|
||||
"""Obtain NICa/c, navigation integrity category supplements a and c
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(int, int): NICa and NICc number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 31:
|
||||
raise RuntimeError(
|
||||
"%s: Not a status operation message, expecting TC = 31" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_a = int(msgbin[75])
|
||||
nic_c = int(msgbin[51])
|
||||
|
||||
return nic_a, nic_c
|
||||
|
||||
|
||||
def nic_b(msg):
|
||||
"""Obtain NICb, navigation integrity category supplement-b
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: NICb number (0 or 1)
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc < 9 or tc > 18:
|
||||
raise RuntimeError(
|
||||
"%s: Not a airborne position message, expecting 8<TC<19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
nic_b = int(msgbin[39])
|
||||
|
||||
return nic_b
|
||||
|
||||
|
||||
def nac_p(msg):
|
||||
"""Calculate NACp, Navigation Accuracy Category - Position
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 29 or 31
|
||||
|
||||
Returns:
|
||||
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
|
||||
int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc not in [29, 31]:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, \
|
||||
or operation status message, expecting TC = 29 or 31"
|
||||
% msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
if tc == 29:
|
||||
NACp = common.bin2int(msgbin[71:75])
|
||||
elif tc == 31:
|
||||
NACp = common.bin2int(msgbin[76:80])
|
||||
|
||||
try:
|
||||
EPU = uncertainty.NACp[NACp]["EPU"]
|
||||
VEPU = uncertainty.NACp[NACp]["VEPU"]
|
||||
except KeyError:
|
||||
EPU, VEPU = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return EPU, VEPU
|
||||
|
||||
|
||||
def nac_v(msg):
|
||||
"""Calculate NACv, Navigation Accuracy Category - Velocity
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC = 19
|
||||
|
||||
Returns:
|
||||
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
|
||||
int or string: 95% vertical accuracy bounds for velocity, Vertical Figure of Merit
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError(
|
||||
"%s: Not an airborne velocity message, expecting TC = 19" % msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
NACv = common.bin2int(msgbin[42:45])
|
||||
|
||||
try:
|
||||
HFOMr = uncertainty.NACv[NACv]["HFOMr"]
|
||||
VFOMr = uncertainty.NACv[NACv]["VFOMr"]
|
||||
except KeyError:
|
||||
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
|
||||
|
||||
return HFOMr, VFOMr
|
||||
|
||||
|
||||
def sil(msg, version):
|
||||
"""Calculate SIL, Surveillance Integrity Level
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string with TC = 29, 31
|
||||
|
||||
Returns:
|
||||
int or string: Probability of exceeding Horizontal Radius of Containment RCu
|
||||
int or string: Probability of exceeding Vertical Integrity Containment Region VPL
|
||||
string: SIL supplement based on per "hour" or "sample", or 'unknown'
|
||||
"""
|
||||
tc = typecode(msg)
|
||||
|
||||
if tc not in [29, 31]:
|
||||
raise RuntimeError(
|
||||
"%s: Not a target state and status message, \
|
||||
or operation status message, expecting TC = 29 or 31"
|
||||
% msg
|
||||
)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
|
||||
if tc == 29:
|
||||
SIL = common.bin2int(msgbin[76:78])
|
||||
elif tc == 31:
|
||||
SIL = common.bin2int(msgbin[82:84])
|
||||
|
||||
try:
|
||||
PE_RCu = uncertainty.SIL[SIL]["PE_RCu"]
|
||||
PE_VPL = uncertainty.SIL[SIL]["PE_VPL"]
|
||||
except KeyError:
|
||||
PE_RCu, PE_VPL = uncertainty.NA, uncertainty.NA
|
||||
|
||||
base = "unknown"
|
||||
|
||||
if version == 2:
|
||||
if tc == 29:
|
||||
SIL_SUP = common.bin2int(msgbin[39])
|
||||
elif tc == 31:
|
||||
SIL_SUP = common.bin2int(msgbin[86])
|
||||
|
||||
if SIL_SUP == 0:
|
||||
base = "hour"
|
||||
elif SIL_SUP == 1:
|
||||
base = "sample"
|
||||
|
||||
return PE_RCu, PE_VPL, base
|
||||
23
pyModeS/decoder/allcall.py
Normal file
23
pyModeS/decoder/allcall.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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
|
||||
|
||||
[To be implemented]
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
181
pyModeS/decoder/bds/__init__.py
Normal file
181
pyModeS/decoder/bds/__init__.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# Copyright (C) 2015 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
Common functions for Mode-S decoding
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import numpy as np
|
||||
|
||||
from pyModeS.extra import aero
|
||||
from pyModeS.decoder import common
|
||||
from pyModeS.decoder.bds import (
|
||||
bds05,
|
||||
bds06,
|
||||
bds08,
|
||||
bds09,
|
||||
bds10,
|
||||
bds17,
|
||||
bds20,
|
||||
bds30,
|
||||
bds40,
|
||||
bds44,
|
||||
bds45,
|
||||
bds50,
|
||||
bds53,
|
||||
bds60,
|
||||
)
|
||||
|
||||
|
||||
def is50or60(msg, spd_ref, trk_ref, alt_ref):
|
||||
"""Use reference ground speed and trk to determine BDS50 and DBS60.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
spd_ref (float): reference speed (ADS-B ground speed), kts
|
||||
trk_ref (float): reference track (ADS-B track angle), deg
|
||||
alt_ref (float): reference altitude (ADS-B altitude), ft
|
||||
|
||||
Returns:
|
||||
String or None: BDS version, or possible versions, or None if nothing matches.
|
||||
|
||||
"""
|
||||
|
||||
def vxy(v, angle):
|
||||
vx = v * np.sin(np.radians(angle))
|
||||
vy = v * np.cos(np.radians(angle))
|
||||
return vx, vy
|
||||
|
||||
if not (bds50.is50(msg) and bds60.is60(msg)):
|
||||
return None
|
||||
|
||||
h50 = bds50.trk50(msg)
|
||||
v50 = bds50.gs50(msg)
|
||||
|
||||
if h50 is None or v50 is None:
|
||||
return "BDS50,BDS60"
|
||||
|
||||
h60 = bds60.hdg60(msg)
|
||||
m60 = bds60.mach60(msg)
|
||||
i60 = bds60.ias60(msg)
|
||||
|
||||
if h60 is None or (m60 is None and i60 is None):
|
||||
return "BDS50,BDS60"
|
||||
|
||||
m60 = np.nan if m60 is None else m60
|
||||
i60 = np.nan if i60 is None else i60
|
||||
|
||||
XY5 = vxy(v50 * aero.kts, h50)
|
||||
XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60)
|
||||
XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60)
|
||||
|
||||
allbds = ["BDS50", "BDS60", "BDS60"]
|
||||
|
||||
X = np.array([XY5, XY6m, XY6i])
|
||||
Mu = np.array(vxy(spd_ref * aero.kts, trk_ref))
|
||||
|
||||
# compute Mahalanobis distance matrix
|
||||
# Cov = [[20**2, 0], [0, 20**2]]
|
||||
# mmatrix = np.sqrt(np.dot(np.dot(X-Mu, np.linalg.inv(Cov)), (X-Mu).T))
|
||||
# dist = np.diag(mmatrix)
|
||||
|
||||
# since the covariance matrix is identity matrix,
|
||||
# M-dist is same as eculidian distance
|
||||
try:
|
||||
dist = np.linalg.norm(X - Mu, axis=1)
|
||||
BDS = allbds[np.nanargmin(dist)]
|
||||
except ValueError:
|
||||
return "BDS50,BDS60"
|
||||
|
||||
return BDS
|
||||
|
||||
|
||||
def infer(msg, mrar=False):
|
||||
"""Estimate the most likely BDS code of an message.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message 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"
|
||||
|
||||
# For ADS-B / Mode-S extended squitter
|
||||
if df == 17:
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if 1 <= tc <= 4:
|
||||
return "BDS08" # identification and category
|
||||
if 5 <= tc <= 8:
|
||||
return "BDS06" # surface movement
|
||||
if 9 <= tc <= 18:
|
||||
return "BDS05" # airborne position, baro-alt
|
||||
if tc == 19:
|
||||
return "BDS09" # airborne velocity
|
||||
if 20 <= tc <= 22:
|
||||
return "BDS05" # airborne position, gnss-alt
|
||||
if tc == 28:
|
||||
return "BDS61" # aircraft status
|
||||
if tc == 29:
|
||||
return "BDS62" # target state and status
|
||||
if tc == 31:
|
||||
return "BDS65" # operational status
|
||||
|
||||
# For Comm-B replies
|
||||
IS10 = bds10.is10(msg)
|
||||
IS17 = bds17.is17(msg)
|
||||
IS20 = bds20.is20(msg)
|
||||
IS30 = bds30.is30(msg)
|
||||
IS40 = bds40.is40(msg)
|
||||
IS50 = bds50.is50(msg)
|
||||
IS60 = bds60.is60(msg)
|
||||
IS44 = bds44.is44(msg)
|
||||
IS45 = bds45.is45(msg)
|
||||
|
||||
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]
|
||||
|
||||
bds = ",".join(sorted(allbds[mask]))
|
||||
|
||||
if len(bds) == 0:
|
||||
return None
|
||||
else:
|
||||
return bds
|
||||
173
pyModeS/decoder/bds/bds05.py
Normal file
173
pyModeS/decoder/bds/bds05.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 0,5
|
||||
# ADS-B TC=9-18
|
||||
# Airborn position
|
||||
# ------------------------------------------
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
|
||||
def airborne_position(msg0, msg1, t0, t1):
|
||||
"""Decode airborn position from a pair of even and odd position message
|
||||
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
mb0 = common.hex2bin(msg0)[32:]
|
||||
mb1 = common.hex2bin(msg1)[32:]
|
||||
|
||||
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
|
||||
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
|
||||
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
|
||||
|
||||
air_d_lat_even = 360.0 / 60
|
||||
air_d_lat_odd = 360.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
|
||||
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
if lat_even >= 270:
|
||||
lat_even = lat_even - 360
|
||||
|
||||
if lat_odd >= 270:
|
||||
lat_odd = lat_odd - 360
|
||||
|
||||
# check if both are in the same latidude zone, exit if not
|
||||
if common.cprNL(lat_even) != common.cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if t0 > t1:
|
||||
lat = lat_even
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = common.cprNL(lat)
|
||||
ni = max(common.cprNL(lat) - 1, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (360.0 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
if lon > 180:
|
||||
lon = lon - 360
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def airborne_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode airborne position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 180NM of the true position.
|
||||
|
||||
Args:
|
||||
msg (string): even message (28 bytes hexadecimal string)
|
||||
lat_ref: previous known latitude
|
||||
lon_ref: previous known longitude
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
||||
|
||||
i = int(mb[21])
|
||||
d_lat = 360.0 / 59 if i else 360.0 / 60
|
||||
|
||||
j = common.floor(lat_ref / d_lat) + common.floor(
|
||||
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
||||
)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = common.cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 360.0 / ni
|
||||
else:
|
||||
d_lon = 360.0
|
||||
|
||||
m = common.floor(lon_ref / d_lon) + common.floor(
|
||||
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
||||
)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def altitude(msg):
|
||||
"""Decode aircraft altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if tc < 9 or tc == 19 or tc > 22:
|
||||
raise RuntimeError("%s: Not a airborn position message" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
if tc < 19:
|
||||
# barometric altitude
|
||||
q = mb[15]
|
||||
if q:
|
||||
n = common.bin2int(mb[8:15] + mb[16:20])
|
||||
alt = n * 25 - 1000
|
||||
else:
|
||||
alt = None
|
||||
else:
|
||||
# GNSS altitude, meters -> feet
|
||||
alt = common.bin2int(mb[8:20]) * 3.28084
|
||||
|
||||
return alt
|
||||
198
pyModeS/decoder/bds/bds06.py
Normal file
198
pyModeS/decoder/bds/bds06.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 0,6
|
||||
# ADS-B TC=5-8
|
||||
# Surface position
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
import math
|
||||
|
||||
|
||||
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
|
||||
"""Decode surface position from a pair of even and odd position message,
|
||||
the lat/lon of receiver must be provided to yield the correct solution.
|
||||
|
||||
Args:
|
||||
msg0 (string): even message (28 bytes hexadecimal string)
|
||||
msg1 (string): odd message (28 bytes hexadecimal string)
|
||||
t0 (int): timestamps for the even message
|
||||
t1 (int): timestamps for the odd message
|
||||
lat_ref (float): latitude of the receiver
|
||||
lon_ref (float): longitude of the receiver
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
msgbin0 = common.hex2bin(msg0)
|
||||
msgbin1 = common.hex2bin(msg1)
|
||||
|
||||
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
|
||||
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
|
||||
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
|
||||
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
|
||||
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
|
||||
|
||||
air_d_lat_even = 90.0 / 60
|
||||
air_d_lat_odd = 90.0 / 59
|
||||
|
||||
# compute latitude index 'j'
|
||||
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even))
|
||||
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
|
||||
|
||||
# solution for north hemisphere
|
||||
lat_even_s = lat_even_n - 90.0
|
||||
lat_odd_s = lat_odd_n - 90.0
|
||||
|
||||
# chose which solution corrispondes to receiver location
|
||||
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
|
||||
lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s
|
||||
|
||||
# check if both are in the same latidude zone, rare but possible
|
||||
if common.cprNL(lat_even) != common.cprNL(lat_odd):
|
||||
return None
|
||||
|
||||
# compute ni, longitude index m, and longitude
|
||||
if t0 > t1:
|
||||
lat = lat_even
|
||||
nl = common.cprNL(lat_even)
|
||||
ni = max(common.cprNL(lat_even) - 0, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_even)
|
||||
else:
|
||||
lat = lat_odd
|
||||
nl = common.cprNL(lat_odd)
|
||||
ni = max(common.cprNL(lat_odd) - 1, 1)
|
||||
m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5)
|
||||
lon = (90.0 / ni) * (m % ni + cprlon_odd)
|
||||
|
||||
# four possible longitude solutions
|
||||
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
|
||||
|
||||
# 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__)
|
||||
lon = lons[imin]
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_position_with_ref(msg, lat_ref, lon_ref):
|
||||
"""Decode surface position with only one message,
|
||||
knowing reference nearby location, such as previously calculated location,
|
||||
ground station, or airport location, etc. The reference position shall
|
||||
be with in 45NM of the true position.
|
||||
|
||||
Args:
|
||||
msg (string): even message (28 bytes hexadecimal string)
|
||||
lat_ref: previous known latitude
|
||||
lon_ref: previous known longitude
|
||||
|
||||
Returns:
|
||||
(float, float): (latitude, longitude) of the aircraft
|
||||
"""
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
cprlat = common.bin2int(mb[22:39]) / 131072.0
|
||||
cprlon = common.bin2int(mb[39:56]) / 131072.0
|
||||
|
||||
i = int(mb[21])
|
||||
d_lat = 90.0 / 59 if i else 90.0 / 60
|
||||
|
||||
j = common.floor(lat_ref / d_lat) + common.floor(
|
||||
0.5 + ((lat_ref % d_lat) / d_lat) - cprlat
|
||||
)
|
||||
|
||||
lat = d_lat * (j + cprlat)
|
||||
|
||||
ni = common.cprNL(lat) - i
|
||||
|
||||
if ni > 0:
|
||||
d_lon = 90.0 / ni
|
||||
else:
|
||||
d_lon = 90.0
|
||||
|
||||
m = common.floor(lon_ref / d_lon) + common.floor(
|
||||
0.5 + ((lon_ref % d_lon) / d_lon) - cprlon
|
||||
)
|
||||
|
||||
lon = d_lon * (m + cprlon)
|
||||
|
||||
return round(lat, 5), round(lon, 5)
|
||||
|
||||
|
||||
def surface_velocity(msg, rtn_sources=False):
|
||||
"""Decode surface velocity from from a surface position message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
rtn_source (boolean): If the function will return
|
||||
the sources for direction of travel and vertical
|
||||
rate. This will change the return value from a four
|
||||
element array to a six element array.
|
||||
|
||||
Returns:
|
||||
(int, float, int, string, string, None): speed (kt),
|
||||
ground track (degree), None for rate of climb/descend (ft/min),
|
||||
and speed type ('GS' for ground speed), direction source
|
||||
('true_north' for ground track / true north as reference),
|
||||
None rate of climb/descent source.
|
||||
"""
|
||||
|
||||
if common.typecode(msg) < 5 or common.typecode(msg) > 8:
|
||||
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
# ground track
|
||||
trk_status = int(mb[12])
|
||||
if trk_status == 1:
|
||||
trk = common.bin2int(mb[13:20]) * 360.0 / 128.0
|
||||
trk = round(trk, 1)
|
||||
else:
|
||||
trk = None
|
||||
|
||||
# ground movement / speed
|
||||
mov = common.bin2int(mb[5:12])
|
||||
|
||||
if mov == 0 or mov > 124:
|
||||
spd = None
|
||||
elif mov == 1:
|
||||
spd = 0
|
||||
elif mov == 124:
|
||||
spd = 175
|
||||
else:
|
||||
movs = [2, 9, 13, 39, 94, 109, 124]
|
||||
kts = [0.125, 1, 2, 15, 70, 100, 175]
|
||||
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
|
||||
step = (kts[i] - kts[i - 1]) * 1.0 / (movs[i] - movs[i - 1])
|
||||
spd = kts[i - 1] + (mov - movs[i - 1]) * step
|
||||
spd = round(spd, 2)
|
||||
|
||||
if rtn_sources:
|
||||
return spd, trk, 0, "GS", "true_north", None
|
||||
else:
|
||||
return spd, trk, 0, "GS"
|
||||
75
pyModeS/decoder/bds/bds08.py
Normal file
75
pyModeS/decoder/bds/bds08.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 0,8
|
||||
# ADS-B TC=1-4
|
||||
# Aircraft identitification and category
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
|
||||
|
||||
def category(msg):
|
||||
"""Aircraft category number
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: category number
|
||||
"""
|
||||
|
||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
mebin = msgbin[32:87]
|
||||
return common.bin2int(mebin[5:8])
|
||||
|
||||
|
||||
def callsign(msg):
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: callsign
|
||||
"""
|
||||
|
||||
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
|
||||
raise RuntimeError("%s: Not a identification message" % msg)
|
||||
|
||||
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
||||
msgbin = common.hex2bin(msg)
|
||||
csbin = msgbin[40:96]
|
||||
|
||||
cs = ""
|
||||
cs += chars[common.bin2int(csbin[0:6])]
|
||||
cs += chars[common.bin2int(csbin[6:12])]
|
||||
cs += chars[common.bin2int(csbin[12:18])]
|
||||
cs += chars[common.bin2int(csbin[18:24])]
|
||||
cs += chars[common.bin2int(csbin[24:30])]
|
||||
cs += chars[common.bin2int(csbin[30:36])]
|
||||
cs += chars[common.bin2int(csbin[36:42])]
|
||||
cs += chars[common.bin2int(csbin[42:48])]
|
||||
|
||||
# clean string, remove spaces and marks, if any.
|
||||
# cs = cs.replace('_', '')
|
||||
cs = cs.replace("#", "")
|
||||
return cs
|
||||
138
pyModeS/decoder/bds/bds09.py
Normal file
138
pyModeS/decoder/bds/bds09.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# 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, rtn_sources=False):
|
||||
"""Calculate the speed, track (or heading), and vertical rate
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
rtn_source (boolean): If the function will return
|
||||
the sources for direction of travel and vertical
|
||||
rate. This will change the return value from a four
|
||||
element array to a six element array.
|
||||
|
||||
Returns:
|
||||
(int, float, int, string, string, string): speed (kt),
|
||||
ground track or heading (degree),
|
||||
rate of climb/descent (ft/min), speed type
|
||||
('GS' for ground speed, 'AS' for airspeed),
|
||||
direction source ('true_north' for ground track / true north
|
||||
as reference, 'mag_north' for magnetic north as reference),
|
||||
rate of climb/descent source ('Baro' for barometer, 'GNSS'
|
||||
for GNSS constellation).
|
||||
"""
|
||||
|
||||
if common.typecode(msg) != 19:
|
||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
||||
|
||||
mb = common.hex2bin(msg)[32:]
|
||||
|
||||
subtype = common.bin2int(mb[5:8])
|
||||
|
||||
if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0:
|
||||
return None
|
||||
|
||||
if subtype in (1, 2):
|
||||
v_ew_sign = -1 if mb[13] == "1" else 1
|
||||
v_ew = common.bin2int(mb[14:24]) - 1 # east-west velocity
|
||||
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
|
||||
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 = int(spd)
|
||||
|
||||
trk = math.atan2(v_we, v_sn)
|
||||
trk = math.degrees(trk) # convert to degrees
|
||||
trk = trk if trk >= 0 else trk + 360 # no negative val
|
||||
|
||||
tag = "GS"
|
||||
trk_or_hdg = round(trk, 2)
|
||||
dir_type = "true_north"
|
||||
|
||||
else:
|
||||
if mb[13] == "0":
|
||||
hdg = None
|
||||
else:
|
||||
hdg = common.bin2int(mb[14:24]) / 1024.0 * 360.0
|
||||
hdg = round(hdg, 2)
|
||||
|
||||
trk_or_hdg = hdg
|
||||
|
||||
spd = common.bin2int(mb[25:35])
|
||||
spd = None if spd == 0 else spd - 1
|
||||
if subtype == 4: # Supersonic
|
||||
spd *= 4
|
||||
|
||||
if mb[24] == "0":
|
||||
tag = "IAS"
|
||||
else:
|
||||
tag = "TAS"
|
||||
|
||||
dir_type = "mag_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)
|
||||
|
||||
if rtn_sources:
|
||||
return spd, trk_or_hdg, rocd, tag, dir_type, vr_source
|
||||
else:
|
||||
return spd, trk_or_hdg, rocd, tag
|
||||
|
||||
|
||||
def altitude_diff(msg):
|
||||
"""Decode the differece between GNSS and barometric altitude
|
||||
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string, TC=19
|
||||
|
||||
Returns:
|
||||
int: Altitude difference in ft. Negative value indicates GNSS altitude
|
||||
below barometric altitude.
|
||||
"""
|
||||
tc = common.typecode(msg)
|
||||
|
||||
if tc != 19:
|
||||
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
|
||||
|
||||
msgbin = common.hex2bin(msg)
|
||||
sign = -1 if int(msgbin[80]) else 1
|
||||
value = common.bin2int(msgbin[81:88])
|
||||
|
||||
if value == 0 or value == 127:
|
||||
return None
|
||||
else:
|
||||
return sign * (value - 1) * 25 # in ft.
|
||||
68
pyModeS/decoder/bds/bds10.py
Normal file
68
pyModeS/decoder/bds/bds10.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 1,0
|
||||
# Data link capability report
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
|
||||
def is10(msg):
|
||||
"""Check if a message is likely to be BDS code 1,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# first 8 bits must be 0x10
|
||||
if d[0:8] != "00010000":
|
||||
return False
|
||||
|
||||
# bit 10 to 14 are reserved
|
||||
if bin2int(d[9:14]) != 0:
|
||||
return False
|
||||
|
||||
# overlay capability conflict
|
||||
if d[14] == "1" and bin2int(d[16:23]) < 5:
|
||||
return False
|
||||
if d[14] == "0" and bin2int(d[16:23]) > 4:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def ovc10(msg):
|
||||
"""Return the overlay control capability
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Whether the transponder is OVC capable
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
return int(d[14])
|
||||
103
pyModeS/decoder/bds/bds17.py
Normal file
103
pyModeS/decoder/bds/bds17.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 1,7
|
||||
# Common usage GICB capability report
|
||||
# ------------------------------------------
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
|
||||
def is17(msg):
|
||||
"""Check if a message is likely to be BDS code 1,7
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if bin2int(d[28:56]) != 0:
|
||||
return False
|
||||
|
||||
caps = cap17(msg)
|
||||
|
||||
# basic BDS codes for ADS-B shall be supported
|
||||
# assuming ADS-B out is installed (2017EU/2020US mandate)
|
||||
# if not set(['BDS05', 'BDS06', 'BDS08', 'BDS09', 'BDS20']).issubset(caps):
|
||||
# return False
|
||||
|
||||
# at least you can respond who you are
|
||||
if "BDS20" not in caps:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def cap17(msg):
|
||||
"""Extract capacities from BDS 1,7 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
list: list of 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",
|
||||
]
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
idx = [i for i, v in enumerate(d[:28]) if v == "1"]
|
||||
capacity = ["BDS" + allbds[i] for i in idx if allbds[i] is not "NA"]
|
||||
|
||||
return capacity
|
||||
74
pyModeS/decoder/bds/bds20.py
Normal file
74
pyModeS/decoder/bds/bds20.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 2,0
|
||||
# Aircraft identification
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
|
||||
def is20(msg):
|
||||
"""Check if a message is likely to be BDS code 2,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[0:8] != "00100000":
|
||||
return False
|
||||
|
||||
cs = cs20(msg)
|
||||
|
||||
if "#" in cs:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def cs20(msg):
|
||||
"""Aircraft callsign
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
|
||||
Returns:
|
||||
string: callsign, max. 8 chars
|
||||
"""
|
||||
chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
cs = ""
|
||||
cs += chars[bin2int(d[8:14])]
|
||||
cs += chars[bin2int(d[14:20])]
|
||||
cs += chars[bin2int(d[20:26])]
|
||||
cs += chars[bin2int(d[26:32])]
|
||||
cs += chars[bin2int(d[32:38])]
|
||||
cs += chars[bin2int(d[38:44])]
|
||||
cs += chars[bin2int(d[44:50])]
|
||||
cs += chars[bin2int(d[50:56])]
|
||||
|
||||
return cs
|
||||
51
pyModeS/decoder/bds/bds30.py
Normal file
51
pyModeS/decoder/bds/bds30.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 3,0
|
||||
# ACAS active resolution advisory
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros
|
||||
|
||||
|
||||
def is30(msg):
|
||||
"""Check if a message is likely to be BDS code 2,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[0:8] != "00110000":
|
||||
return False
|
||||
|
||||
# threat type 3 not assigned
|
||||
if d[28:30] == "11":
|
||||
return False
|
||||
|
||||
# reserved for ACAS III, in far future
|
||||
if bin2int(d[15:22]) >= 48:
|
||||
return False
|
||||
|
||||
return True
|
||||
136
pyModeS/decoder/bds/bds40.py
Normal file
136
pyModeS/decoder/bds/bds40.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,0
|
||||
# Selected vertical intention
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import warnings
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
|
||||
def is40(msg):
|
||||
"""Check if a message is likely to be BDS code 4,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# status bit 1, 14, and 27
|
||||
|
||||
if wrongstatus(d, 1, 2, 13):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 14, 15, 26):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 27, 28, 39):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 48, 49, 51):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 54, 55, 56):
|
||||
return False
|
||||
|
||||
# bits 40-47 and 52-53 shall all be zero
|
||||
|
||||
if bin2int(d[39:47]) != 0:
|
||||
return False
|
||||
|
||||
if bin2int(d[51:53]) != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def selalt40mcp(msg):
|
||||
"""Selected altitude, MCP/FCU
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
alt = bin2int(d[1:13]) * 16 # ft
|
||||
return alt
|
||||
|
||||
|
||||
def selalt40fms(msg):
|
||||
"""Selected altitude, FMS
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[13] == "0":
|
||||
return None
|
||||
|
||||
alt = bin2int(d[14:26]) * 16 # ft
|
||||
return alt
|
||||
|
||||
|
||||
def p40baro(msg):
|
||||
"""Barometric pressure setting
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
|
||||
Returns:
|
||||
float: pressure in millibar
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[26] == "0":
|
||||
return None
|
||||
|
||||
p = bin2int(d[27:39]) * 0.1 + 800 # millibar
|
||||
return p
|
||||
|
||||
|
||||
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 selalt40mcp(msg)
|
||||
177
pyModeS/decoder/bds/bds44.py
Normal file
177
pyModeS/decoder/bds/bds44.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,4
|
||||
# Meteorological routine air report
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
|
||||
"""
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# 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
|
||||
|
||||
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):
|
||||
"""Wind speed and direction.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
(int, float): speed (kt), direction (degree)
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
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
|
||||
|
||||
return round(speed, 0), round(direction, 1)
|
||||
|
||||
|
||||
def temp44(msg):
|
||||
"""Static air temperature.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
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))
|
||||
|
||||
sign = int(d[23])
|
||||
value = bin2int(d[24:34])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
temp = value * 0.25 # celsius
|
||||
temp = round(temp, 2)
|
||||
|
||||
temp_alternative = value * 0.125 # celsius
|
||||
temp_alternative = round(temp_alternative, 3)
|
||||
|
||||
return temp, temp_alternative
|
||||
|
||||
|
||||
def p44(msg):
|
||||
"""Static pressure.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: static pressure in hPa
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[34] == "0":
|
||||
return None
|
||||
|
||||
p = bin2int(d[35:46]) # hPa
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def hum44(msg):
|
||||
"""humidity
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
float: percentage of humidity, [0 - 100] %
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[49] == "0":
|
||||
return None
|
||||
|
||||
hm = bin2int(d[50:56]) * 100.0 / 64 # %
|
||||
|
||||
return round(hm, 1)
|
||||
|
||||
|
||||
def turb44(msg):
|
||||
"""Turblence.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[46] == "0":
|
||||
return None
|
||||
|
||||
turb = bin2int(d[47:49])
|
||||
|
||||
return turb
|
||||
224
pyModeS/decoder/bds/bds45.py
Normal file
224
pyModeS/decoder/bds/bds45.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 4,5
|
||||
# Meteorological hazard report
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
|
||||
def is45(msg):
|
||||
"""Check if a message is likely to be BDS code 4,5.
|
||||
|
||||
Meteorological hazard report
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
|
||||
"""
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# status bit 1, 4, 7, 10, 13, 16, 27, 39
|
||||
if wrongstatus(d, 1, 2, 3):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 4, 5, 6):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 7, 8, 9):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 10, 11, 12):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 13, 14, 15):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 16, 17, 26):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 27, 28, 38):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 39, 40, 51):
|
||||
return False
|
||||
|
||||
# reserved
|
||||
if 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 (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
turb = bin2int(d[1:3])
|
||||
return turb
|
||||
|
||||
|
||||
def ws45(msg):
|
||||
"""Wind shear.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Wind shear level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[3] == "0":
|
||||
return None
|
||||
|
||||
ws = bin2int(d[4:6])
|
||||
return ws
|
||||
|
||||
|
||||
def mb45(msg):
|
||||
"""Microburst.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Microburst level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[6] == "0":
|
||||
return None
|
||||
|
||||
mb = bin2int(d[7:9])
|
||||
return mb
|
||||
|
||||
|
||||
def ic45(msg):
|
||||
"""Icing.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Icing level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[9] == "0":
|
||||
return None
|
||||
|
||||
ic = bin2int(d[10:12])
|
||||
return ic
|
||||
|
||||
|
||||
def wv45(msg):
|
||||
"""Wake vortex.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: Wake vortex level. 0=NIL, 1=Light, 2=Moderate, 3=Severe
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[12] == "0":
|
||||
return None
|
||||
|
||||
ws = bin2int(d[13:15])
|
||||
return ws
|
||||
|
||||
|
||||
def temp45(msg):
|
||||
"""Static air temperature.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
float: tmeperature in Celsius degree
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
sign = int(d[16])
|
||||
value = 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 (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: static pressure in hPa
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[26] == "0":
|
||||
return None
|
||||
p = bin2int(d[27:38]) # hPa
|
||||
return p
|
||||
|
||||
|
||||
def rh45(msg):
|
||||
"""Radio height.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: radio height in ft
|
||||
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
if d[38] == "0":
|
||||
return None
|
||||
rh = bin2int(d[39:51]) * 16
|
||||
return rh
|
||||
189
pyModeS/decoder/bds/bds50.py
Normal file
189
pyModeS/decoder/bds/bds50.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 5,0
|
||||
# Track and turn report
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
|
||||
def is50(msg):
|
||||
"""Check if a message is likely to be BDS code 5,0
|
||||
(Track and turn report)
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# status bit 1, 12, 24, 35, 46
|
||||
|
||||
if wrongstatus(d, 1, 3, 11):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 12, 13, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 34):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 35, 36, 45):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 46, 47, 56):
|
||||
return False
|
||||
|
||||
roll = roll50(msg)
|
||||
if (roll is not None) and abs(roll) > 60:
|
||||
return False
|
||||
|
||||
gs = gs50(msg)
|
||||
if gs is not None and gs > 600:
|
||||
return False
|
||||
|
||||
tas = tas50(msg)
|
||||
if tas is not None and tas > 500:
|
||||
return False
|
||||
|
||||
if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def roll50(msg):
|
||||
"""Roll angle, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
|
||||
Returns:
|
||||
float: angle in degrees,
|
||||
negative->left wing down, positive->right wing down
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[1]) # 1 -> left wing down
|
||||
value = bin2int(d[2:11])
|
||||
|
||||
if sign:
|
||||
value = value - 512
|
||||
|
||||
angle = value * 45.0 / 256.0 # degree
|
||||
return round(angle, 1)
|
||||
|
||||
|
||||
def trk50(msg):
|
||||
"""True track angle, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
|
||||
Returns:
|
||||
float: angle in degrees to true north (from 0 to 360)
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[11] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[12]) # 1 -> west
|
||||
value = bin2int(d[13:23])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
trk = value * 90.0 / 512.0
|
||||
|
||||
# convert from [-180, 180] to [0, 360]
|
||||
if trk < 0:
|
||||
trk = 360 + trk
|
||||
|
||||
return round(trk, 3)
|
||||
|
||||
|
||||
def gs50(msg):
|
||||
"""Ground speed, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
|
||||
Returns:
|
||||
int: ground speed in knots
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[23] == "0":
|
||||
return None
|
||||
|
||||
spd = bin2int(d[24:34]) * 2 # kts
|
||||
return spd
|
||||
|
||||
|
||||
def rtrk50(msg):
|
||||
"""Track angle rate, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
|
||||
Returns:
|
||||
float: angle rate in degrees/second
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[34] == "0":
|
||||
return None
|
||||
|
||||
if d[36:45] == "111111111":
|
||||
return None
|
||||
|
||||
sign = int(d[35]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[36:45])
|
||||
if sign:
|
||||
value = value - 512
|
||||
|
||||
angle = value * 8.0 / 256.0 # degree / sec
|
||||
return round(angle, 3)
|
||||
|
||||
|
||||
def tas50(msg):
|
||||
"""Aircraft true airspeed, BDS 5,0 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
|
||||
Returns:
|
||||
int: true airspeed in knots
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[45] == "0":
|
||||
return None
|
||||
|
||||
tas = bin2int(d[46:56]) * 2 # kts
|
||||
return tas
|
||||
183
pyModeS/decoder/bds/bds53.py
Normal file
183
pyModeS/decoder/bds/bds53.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 5,3
|
||||
# Air-referenced state vector
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
|
||||
def is53(msg):
|
||||
"""Check if a message is likely to be BDS code 5,3
|
||||
(Air-referenced state vector)
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# status bit 1, 13, 24, 34, 47
|
||||
|
||||
if wrongstatus(d, 1, 3, 12):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 13, 14, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 33):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 34, 35, 46):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 47, 49, 56):
|
||||
return False
|
||||
|
||||
ias = ias53(msg)
|
||||
if ias is not None and ias > 500:
|
||||
return False
|
||||
|
||||
mach = mach53(msg)
|
||||
if mach is not None and mach > 1:
|
||||
return False
|
||||
|
||||
tas = tas53(msg)
|
||||
if tas is not None and tas > 500:
|
||||
return False
|
||||
|
||||
vr = vr53(msg)
|
||||
if vr is not None and abs(vr) > 8000:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def hdg53(msg):
|
||||
"""Magnetic heading, BDS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS53) string
|
||||
|
||||
Returns:
|
||||
float: angle in degrees to true north (from 0 to 360)
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = bin2int(d[2:12])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
hdg = value * 90.0 / 512.0 # degree
|
||||
|
||||
# convert from [-180, 180] to [0, 360]
|
||||
if hdg < 0:
|
||||
hdg = 360 + hdg
|
||||
|
||||
return round(hdg, 3)
|
||||
|
||||
|
||||
def ias53(msg):
|
||||
"""Indicated airspeed, DBS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message
|
||||
|
||||
Returns:
|
||||
int: indicated arispeed in knots
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[12] == "0":
|
||||
return None
|
||||
|
||||
ias = bin2int(d[13:23]) # knots
|
||||
return ias
|
||||
|
||||
|
||||
def mach53(msg):
|
||||
"""MACH number, DBS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message
|
||||
|
||||
Returns:
|
||||
float: MACH number
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[23] == "0":
|
||||
return None
|
||||
|
||||
mach = bin2int(d[24:33]) * 0.008
|
||||
return round(mach, 3)
|
||||
|
||||
|
||||
def tas53(msg):
|
||||
"""Aircraft true airspeed, BDS 5,3 message
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message
|
||||
|
||||
Returns:
|
||||
float: true airspeed in knots
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[33] == "0":
|
||||
return None
|
||||
|
||||
tas = bin2int(d[34:46]) * 0.5 # kts
|
||||
return round(tas, 1)
|
||||
|
||||
|
||||
def vr53(msg):
|
||||
"""Vertical rate
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
|
||||
Returns:
|
||||
int: vertical rate in feet/minutes
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[46] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[47]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[48:56])
|
||||
|
||||
if value == 0 or value == 255: # all zeros or all ones
|
||||
return 0
|
||||
|
||||
value = value - 256 if sign else value
|
||||
roc = value * 64 # feet/min
|
||||
|
||||
return roc
|
||||
190
pyModeS/decoder/bds/bds60.py
Normal file
190
pyModeS/decoder/bds/bds60.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# Copyright (C) 2018 Junzi Sun (TU Delft)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------
|
||||
# BDS 6,0
|
||||
# Heading and speed report
|
||||
# ------------------------------------------
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder.common import hex2bin, bin2int, data, allzeros, wrongstatus
|
||||
|
||||
|
||||
def is60(msg):
|
||||
"""Check if a message is likely to be BDS code 6,0
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
|
||||
if allzeros(msg):
|
||||
return False
|
||||
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
# status bit 1, 13, 24, 35, 46
|
||||
|
||||
if wrongstatus(d, 1, 2, 12):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 13, 14, 23):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 24, 25, 34):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 35, 36, 45):
|
||||
return False
|
||||
|
||||
if wrongstatus(d, 46, 47, 56):
|
||||
return False
|
||||
|
||||
ias = ias60(msg)
|
||||
if ias is not None and ias > 500:
|
||||
return False
|
||||
|
||||
mach = mach60(msg)
|
||||
if mach is not None and mach > 1:
|
||||
return False
|
||||
|
||||
vr_baro = vr60baro(msg)
|
||||
if vr_baro is not None and abs(vr_baro) > 6000:
|
||||
return False
|
||||
|
||||
vr_ins = vr60ins(msg)
|
||||
if vr_ins is not None and abs(vr_ins) > 6000:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def hdg60(msg):
|
||||
"""Megnetic heading of aircraft
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
|
||||
Returns:
|
||||
float: heading in degrees to megnetic north (from 0 to 360)
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[0] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = bin2int(d[2:12])
|
||||
|
||||
if sign:
|
||||
value = value - 1024
|
||||
|
||||
hdg = value * 90 / 512.0 # degree
|
||||
|
||||
# convert from [-180, 180] to [0, 360]
|
||||
if hdg < 0:
|
||||
hdg = 360 + hdg
|
||||
|
||||
return round(hdg, 3)
|
||||
|
||||
|
||||
def ias60(msg):
|
||||
"""Indicated airspeed
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
|
||||
Returns:
|
||||
int: indicated airspeed in knots
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[12] == "0":
|
||||
return None
|
||||
|
||||
ias = bin2int(d[13:23]) # kts
|
||||
return ias
|
||||
|
||||
|
||||
def mach60(msg):
|
||||
"""Aircraft MACH number
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
|
||||
Returns:
|
||||
float: MACH number
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[23] == "0":
|
||||
return None
|
||||
|
||||
mach = bin2int(d[24:34]) * 2.048 / 512.0
|
||||
return round(mach, 3)
|
||||
|
||||
|
||||
def vr60baro(msg):
|
||||
"""Vertical rate from barometric measurement, this value may be very noisy.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
|
||||
Returns:
|
||||
int: vertical rate in feet/minutes
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[34] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[35]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[36:45])
|
||||
|
||||
if value == 0 or value == 511: # all zeros or all ones
|
||||
return 0
|
||||
|
||||
value = value - 512 if sign else value
|
||||
|
||||
roc = value * 32 # feet/min
|
||||
return roc
|
||||
|
||||
|
||||
def vr60ins(msg):
|
||||
"""Vertical rate measurd by onbard equiments (IRS, AHRS)
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
|
||||
Returns:
|
||||
int: vertical rate in feet/minutes
|
||||
"""
|
||||
d = hex2bin(data(msg))
|
||||
|
||||
if d[45] == "0":
|
||||
return None
|
||||
|
||||
sign = int(d[46]) # 1 -> negative value, two's complement
|
||||
value = bin2int(d[47:56])
|
||||
|
||||
if value == 0 or value == 511: # all zeros or all ones
|
||||
return 0
|
||||
|
||||
value = value - 512 if sign else value
|
||||
|
||||
roc = value * 32 # feet/min
|
||||
return roc
|
||||
37
pyModeS/decoder/commb.py
Normal file
37
pyModeS/decoder/commb.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Comm-B Wrapper.
|
||||
|
||||
The Comm-B wrapper 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
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
|
||||
# ELS - elementary surveillance
|
||||
from pyModeS.decoder.bds.bds10 import *
|
||||
from pyModeS.decoder.bds.bds17 import *
|
||||
from pyModeS.decoder.bds.bds20 import *
|
||||
from pyModeS.decoder.bds.bds30 import *
|
||||
|
||||
# ELS - enhanced surveillance
|
||||
from pyModeS.decoder.bds.bds40 import *
|
||||
from pyModeS.decoder.bds.bds50 import *
|
||||
from pyModeS.decoder.bds.bds60 import *
|
||||
|
||||
# MRAR and MHR
|
||||
from pyModeS.decoder.bds.bds44 import *
|
||||
from pyModeS.decoder.bds.bds45 import *
|
||||
383
pyModeS/decoder/common.py
Normal file
383
pyModeS/decoder/common.py
Normal file
@@ -0,0 +1,383 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import numpy as np
|
||||
from textwrap import wrap
|
||||
|
||||
|
||||
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 hex2int(hexstr):
|
||||
"""Convert a hexdecimal string to integer."""
|
||||
return int(hexstr, 16)
|
||||
|
||||
|
||||
def int2hex(n):
|
||||
"""Convert a integer to hexadecimal string."""
|
||||
# strip 'L' for python 2
|
||||
return hex(n)[2:].rjust(6, "0").upper().rstrip("L")
|
||||
|
||||
|
||||
def bin2int(binstr):
|
||||
"""Convert a binary string to integer."""
|
||||
return int(binstr, 2)
|
||||
|
||||
|
||||
def bin2hex(hexstr):
|
||||
"""Convert a hexdecimal string to integer."""
|
||||
return int2hex(bin2int(hexstr))
|
||||
|
||||
|
||||
def bin2np(binstr):
|
||||
"""Convert a binary string to numpy array."""
|
||||
return np.array([int(i) for i in binstr])
|
||||
|
||||
|
||||
def np2bin(npbin):
|
||||
"""Convert a binary numpy array to string."""
|
||||
return np.array2string(npbin, separator="")[1:-1]
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Decode Downlink Format value, bits 1 to 5."""
|
||||
dfbin = hex2bin(msg[:2])
|
||||
return min(bin2int(dfbin[0:5]), 24)
|
||||
|
||||
|
||||
def crc(msg, 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)]
|
||||
|
||||
if encode and isinstance(msg, str):
|
||||
msg = msg[:-6] + "000000"
|
||||
elif encode:
|
||||
msg = msg[:-6] + b"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, encode=False):
|
||||
"""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 = 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 = bin2int(np2bin(msgnpbin[-24:]))
|
||||
return reminder
|
||||
|
||||
|
||||
def floor(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 int(np.floor(x))
|
||||
|
||||
|
||||
def icao(msg):
|
||||
"""Calculate the ICAO address from an Mode-S message.
|
||||
|
||||
Applicable only with DF4, DF5, DF20, DF21 messages.
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
String: ICAO address in 6 bytes hexadecimal string
|
||||
|
||||
"""
|
||||
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 = 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
|
||||
|
||||
tcbin = hex2bin(msg[8:10])
|
||||
return bin2int(tcbin[0:5])
|
||||
|
||||
|
||||
def cprNL(lat):
|
||||
"""NL() function in CPR decoding."""
|
||||
|
||||
if lat == 0:
|
||||
return 59
|
||||
|
||||
if lat == 87 or lat == -87:
|
||||
return 2
|
||||
|
||||
if lat > 87 or lat < -87:
|
||||
return 1
|
||||
|
||||
nz = 15
|
||||
a = 1 - 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):
|
||||
"""Compute identity (squawk code).
|
||||
|
||||
Applicable only for DF5 or DF21 messages, bit 20-32.
|
||||
credit: @fbyrkjeland
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
string: squawk code
|
||||
|
||||
"""
|
||||
if df(msg) not in [5, 21]:
|
||||
raise RuntimeError("Message must be Downlink Format 5 or 21.")
|
||||
|
||||
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):
|
||||
"""Compute the altitude.
|
||||
|
||||
Applicable only for DF4 or DF20 message, bit 20-32.
|
||||
credit: @fbyrkjeland
|
||||
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
|
||||
Returns:
|
||||
int: altitude in ft
|
||||
|
||||
"""
|
||||
if df(msg) not in [0, 4, 16, 20]:
|
||||
raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.")
|
||||
|
||||
# Altitude code, bit 20-32
|
||||
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
|
||||
36
pyModeS/decoder/ehs.py
Normal file
36
pyModeS/decoder/ehs.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""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
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
import warnings
|
||||
|
||||
from pyModeS.decoder.bds.bds40 import *
|
||||
from pyModeS.decoder.bds.bds50 import *
|
||||
from pyModeS.decoder.bds.bds60 import *
|
||||
from pyModeS.decoder.bds import infer
|
||||
|
||||
warnings.simplefilter("once", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"pms.ehs module is deprecated. Please use pms.commb instead.", DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
def BDS(msg):
|
||||
warnings.warn(
|
||||
"pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", DeprecationWarning
|
||||
)
|
||||
return infer(msg)
|
||||
|
||||
|
||||
def icao(msg):
|
||||
from pyModeS.decoder.common import icao
|
||||
|
||||
return icao(msg)
|
||||
25
pyModeS/decoder/els.py
Normal file
25
pyModeS/decoder/els.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""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 __future__ import absolute_import, print_function, division
|
||||
|
||||
from pyModeS.decoder.bds.bds10 import *
|
||||
from pyModeS.decoder.bds.bds17 import *
|
||||
from pyModeS.decoder.bds.bds20 import *
|
||||
from pyModeS.decoder.bds.bds30 import *
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.simplefilter("once", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"pms.els module is deprecated. Please use pms.commb instead.", DeprecationWarning
|
||||
)
|
||||
23
pyModeS/decoder/surv.py
Normal file
23
pyModeS/decoder/surv.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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
|
||||
|
||||
[To be implemented]
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, division
|
||||
from pyModeS.decoder import common
|
||||
152
pyModeS/decoder/uncertainty.py
Normal file
152
pyModeS/decoder/uncertainty.py
Normal file
@@ -0,0 +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,
|
||||
}
|
||||
|
||||
TC_NICv1_lookup = {
|
||||
5: 11,
|
||||
6: 10,
|
||||
7: 9,
|
||||
8: 0,
|
||||
9: 11,
|
||||
10: 10,
|
||||
11: {1: 9, 0: 8},
|
||||
12: 7,
|
||||
13: 6,
|
||||
14: 5,
|
||||
15: 4,
|
||||
16: {1: 3, 0: 2},
|
||||
17: 1,
|
||||
18: 0,
|
||||
20: 11,
|
||||
21: 10,
|
||||
22: 0,
|
||||
}
|
||||
|
||||
TC_NICv2_lookup = {
|
||||
5: 11,
|
||||
6: 10,
|
||||
7: {2: 9, 0: 8},
|
||||
8: {3: 7, 2: 6, 1: 6, 0: 0},
|
||||
9: 11,
|
||||
10: 10,
|
||||
11: {3: 9, 0: 8},
|
||||
12: 7,
|
||||
13: 6,
|
||||
14: 5,
|
||||
15: 4,
|
||||
16: {3: 3, 0: 2},
|
||||
17: 1,
|
||||
18: 0,
|
||||
20: 11,
|
||||
21: 10,
|
||||
22: 0,
|
||||
}
|
||||
|
||||
|
||||
NUCp = {
|
||||
9: {"HPL": 7.5, "RCu": 3, "RCv": 4},
|
||||
8: {"HPL": 25, "RCu": 10, "RCv": 15},
|
||||
7: {"HPL": 185, "RCu": 93, "RCv": NA},
|
||||
6: {"HPL": 370, "RCu": 185, "RCv": NA},
|
||||
5: {"HPL": 926, "RCu": 463, "RCv": NA},
|
||||
4: {"HPL": 1852, "RCu": 926, "RCv": NA},
|
||||
3: {"HPL": 3704, "RCu": 1852, "RCv": NA},
|
||||
2: {"HPL": 18520, "RCu": 9260, "RCv": NA},
|
||||
1: {"HPL": 37040, "RCu": 18520, "RCv": NA},
|
||||
0: {"HPL": NA, "RCu": NA, "RCv": NA},
|
||||
}
|
||||
|
||||
NUCv = {
|
||||
0: {"HVE": NA, "VVE": NA},
|
||||
1: {"HVE": 10, "VVE": 15.2},
|
||||
2: {"HVE": 3, "VVE": 4.5},
|
||||
3: {"HVE": 1, "VVE": 1.5},
|
||||
4: {"HVE": 0.3, "VVE": 0.46},
|
||||
}
|
||||
|
||||
NACp = {
|
||||
11: {"EPU": 3, "VEPU": 4},
|
||||
10: {"EPU": 10, "VEPU": 15},
|
||||
9: {"EPU": 30, "VEPU": 45},
|
||||
8: {"EPU": 93, "VEPU": NA},
|
||||
7: {"EPU": 185, "VEPU": NA},
|
||||
6: {"EPU": 556, "VEPU": NA},
|
||||
5: {"EPU": 926, "VEPU": NA},
|
||||
4: {"EPU": 1852, "VEPU": NA},
|
||||
3: {"EPU": 3704, "VEPU": NA},
|
||||
2: {"EPU": 7408, "VEPU": NA},
|
||||
1: {"EPU": 18520, "VEPU": NA},
|
||||
0: {"EPU": NA, "VEPU": NA},
|
||||
}
|
||||
|
||||
NACv = {
|
||||
0: {"HFOMr": NA, "VFOMr": NA},
|
||||
1: {"HFOMr": 10, "VFOMr": 15.2},
|
||||
2: {"HFOMr": 3, "VFOMr": 4.5},
|
||||
3: {"HFOMr": 1, "VFOMr": 1.5},
|
||||
4: {"HFOMr": 0.3, "VFOMr": 0.46},
|
||||
}
|
||||
|
||||
SIL = {
|
||||
3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7},
|
||||
2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5},
|
||||
1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3},
|
||||
0: {"PE_RCu": NA, "PE_VPL": NA},
|
||||
}
|
||||
|
||||
|
||||
NICv1 = {
|
||||
# NIC is used as the index at second Level
|
||||
11: {0: {"Rc": 7.5, "VPL": 11}},
|
||||
10: {0: {"Rc": 25, "VPL": 37.5}},
|
||||
9: {1: {"Rc": 75, "VPL": 112}},
|
||||
8: {0: {"Rc": 185, "VPL": NA}},
|
||||
7: {0: {"Rc": 370, "VPL": NA}},
|
||||
6: {0: {"Rc": 926, "VPL": NA}, 1: {"Rc": 1111, "VPL": NA}},
|
||||
5: {0: {"Rc": 1852, "VPL": NA}},
|
||||
4: {0: {"Rc": 3702, "VPL": NA}},
|
||||
3: {1: {"Rc": 7408, "VPL": NA}},
|
||||
2: {0: {"Rc": 14008, "VPL": NA}},
|
||||
1: {0: {"Rc": 37000, "VPL": NA}},
|
||||
0: {0: {"Rc": NA, "VPL": NA}},
|
||||
}
|
||||
|
||||
NICv2 = {
|
||||
# Decimal value of [NICa NICb/NICc] is used as the index at second Level
|
||||
11: {0: {"Rc": 7.5}},
|
||||
10: {0: {"Rc": 25}},
|
||||
9: {2: {"Rc": 75}, 3: {"Rc": 75}},
|
||||
8: {0: {"Rc": 185}},
|
||||
7: {0: {"Rc": 370}, 3: {"Rc": 370}},
|
||||
6: {0: {"Rc": 926}, 1: {"Rc": 556}, 2: {"Rc": 556}, 3: {"Rc": 1111}},
|
||||
5: {0: {"Rc": 1852}},
|
||||
4: {0: {"Rc": 3702}},
|
||||
3: {3: {"Rc": 7408}},
|
||||
2: {0: {"Rc": 14008}},
|
||||
1: {0: {"Rc": 37000}},
|
||||
0: {0: {"Rc": NA}},
|
||||
}
|
||||
24
pyModeS/decoder/uplink.py
Normal file
24
pyModeS/decoder/uplink.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from pyModeS.decoder import common
|
||||
|
||||
def uplink_icao(msg):
|
||||
"""Calculate the ICAO address from a Mode-S interrogation (uplink message)"""
|
||||
p_gen = 0xfffa0480 << ((len(msg)-14)*4)
|
||||
data = int(msg[:-6],16)
|
||||
PA = int(msg[-6:],16)
|
||||
ad = 0
|
||||
topbit = 0b1 << (len(msg)*4-25)
|
||||
for j in range(0,len(msg)*4,1):
|
||||
if (data & topbit):
|
||||
data^=p_gen
|
||||
data = (data << 1) + ((PA >> 23) & 1)
|
||||
PA = PA << 1
|
||||
if (j>(len(msg)*4-26)):
|
||||
ad = ad + ((data >> (len(msg)*4-25)) & 1)
|
||||
ad = ad << 1
|
||||
return "%06X" % (ad >> 2)
|
||||
|
||||
|
||||
def uf(msg):
|
||||
"""Decode Uplink Format value, bits 1 to 5."""
|
||||
ufbin = common.hex2bin(msg[:2])
|
||||
return min(common.bin2int(ufbin[0:5]), 24)
|
||||
416
pyModeS/ehs.py
416
pyModeS/ehs.py
@@ -1,416 +0,0 @@
|
||||
"""
|
||||
A python package for decoding ModeS (DF20, DF21) messages.
|
||||
|
||||
Copyright (C) 2016 Junzi Sun (TU Delft)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import util
|
||||
from util import crc
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Get the downlink format (DF) number
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
int: DF number
|
||||
"""
|
||||
return util.df(msg)
|
||||
|
||||
|
||||
def data(msg):
|
||||
"""Return the data frame in the message, bytes 9 to 22"""
|
||||
return msg[8:22]
|
||||
|
||||
|
||||
def icao(msg):
|
||||
"""Calculate the ICAO address from an Mode-S message
|
||||
with DF4, DF5, DF20, DF21
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
String: ICAO address in 6 bytes hexadecimal string
|
||||
"""
|
||||
|
||||
if df(msg) not in (4, 5, 20, 21):
|
||||
# raise RuntimeError("Message DF must be in (4, 5, 20, 21)")
|
||||
return None
|
||||
|
||||
c0 = util.bin2int(crc(msg, encode=True))
|
||||
c1 = util.hex2int(msg[-6:])
|
||||
icao = '%06X' % (c0 ^ c1)
|
||||
return icao
|
||||
|
||||
|
||||
def checkbits(data, sb, msb, lsb):
|
||||
"""Check if the status bit and field bits are consistency. This Function
|
||||
is used for checking BDS code versions.
|
||||
"""
|
||||
# status bit, most significant bit, least significant bit
|
||||
status = int(data[sb-1])
|
||||
value = util.bin2int(data[msb-1:lsb])
|
||||
|
||||
if not status:
|
||||
if value != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# DF 20/21, BDS 2,0
|
||||
# ------------------------------------------
|
||||
|
||||
def isBDS20(msg):
|
||||
"""Check if a message is likely to be BDS code 2,0
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
# status bit 1, 14, and 27
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
if util.bin2int(d[0:4]) != 2 or util.bin2int(d[4:8]) != 0:
|
||||
result &= False
|
||||
|
||||
cs = callsign(msg)
|
||||
|
||||
if '#' in cs:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def callsign(msg):
|
||||
"""Aircraft callsign
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
Returns:
|
||||
string: callsign, max. 8 chars
|
||||
"""
|
||||
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
|
||||
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
cs = ''
|
||||
cs += chars[util.bin2int(d[8:14])]
|
||||
cs += chars[util.bin2int(d[14:20])]
|
||||
cs += chars[util.bin2int(d[20:26])]
|
||||
cs += chars[util.bin2int(d[26:32])]
|
||||
cs += chars[util.bin2int(d[32:38])]
|
||||
cs += chars[util.bin2int(d[38:44])]
|
||||
cs += chars[util.bin2int(d[44:50])]
|
||||
cs += chars[util.bin2int(d[50:56])]
|
||||
|
||||
return cs
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# DF 20/21, BDS 4,0
|
||||
# ------------------------------------------
|
||||
|
||||
def isBDS40(msg):
|
||||
"""Check if a message is likely to be BDS code 4,0
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
# status bit 1, 14, and 27
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
result = result & checkbits(d, 1, 2, 13) \
|
||||
& checkbits(d, 14, 15, 26) & checkbits(d, 27, 28, 39)
|
||||
|
||||
# bits 40-47 and 52-53 shall all be zero
|
||||
if util.bin2int(d[39:47]) != 0:
|
||||
result &= False
|
||||
|
||||
if util.bin2int(d[51:53]) != 0:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def alt_mcp(msg):
|
||||
"""Selected altitude, MCP/FCU
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
alt = util.bin2int(d[1:13]) * 16 # ft
|
||||
return alt
|
||||
|
||||
|
||||
def alt_fms(msg):
|
||||
"""Selected altitude, FMS
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
Returns:
|
||||
int: altitude in feet
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
alt = util.bin2int(d[14:26]) * 16 # ft
|
||||
return alt
|
||||
|
||||
|
||||
def pbaro(msg):
|
||||
"""Barometric pressure setting
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS40) string
|
||||
Returns:
|
||||
float: pressure in millibar
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
p = util.bin2int(d[27:39]) * 0.1 + 800 # millibar
|
||||
return p
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# DF 20/21, BDS 5,0
|
||||
# ------------------------------------------
|
||||
|
||||
def isBDS50(msg):
|
||||
"""Check if a message is likely to be BDS code 5,0
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
# status bit 1, 12, 24, 35, 46
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
result = result & checkbits(d, 1, 3, 11) & checkbits(d, 12, 13, 23) \
|
||||
& checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \
|
||||
& checkbits(d, 46, 47, 56)
|
||||
|
||||
if d[2:11] == "000000000":
|
||||
result &= True
|
||||
else:
|
||||
if abs(roll(msg)) > 30:
|
||||
result &= False
|
||||
|
||||
if gs(msg) > 500:
|
||||
result &= False
|
||||
|
||||
if tas(msg) > 500:
|
||||
result &= False
|
||||
|
||||
if abs(tas(msg) - gs(msg)) > 100:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def roll(msg):
|
||||
"""Aircraft roll angle
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
Returns:
|
||||
float: angle in degrees,
|
||||
negative->left wing down, positive->right wing down
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
sign = int(d[1]) # 1 -> left wing down
|
||||
value = util.bin2int(d[2:11]) * 45 / 256.0 # degree
|
||||
angle = -1 * value if sign else value
|
||||
return round(angle, 1)
|
||||
|
||||
|
||||
def track(msg):
|
||||
"""True track angle
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
Returns:
|
||||
float: angle in degrees to true north (from 0 to 360)
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
sign = int(d[12]) # 1 -> west
|
||||
value = util.bin2int(d[13:23]) * 90 / 512.0 # degree
|
||||
angle = 360 - value if sign else value
|
||||
return round(angle, 1)
|
||||
|
||||
|
||||
def gs(msg):
|
||||
"""Aircraft ground speed
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
Returns:
|
||||
int: ground speed in knots
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
spd = util.bin2int(d[24:34]) * 2 # kts
|
||||
return spd
|
||||
|
||||
|
||||
def rtrack(msg):
|
||||
"""Track angle rate
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
Returns:
|
||||
float: angle rate in degrees/second
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
sign = int(d[35]) # 1 -> minus
|
||||
value = util.bin2int(d[36:45]) * 8 / 256.0 # degree / sec
|
||||
angle = -1 * value if sign else value
|
||||
return round(angle, 3)
|
||||
|
||||
|
||||
def tas(msg):
|
||||
"""Aircraft true airspeed
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS50) string
|
||||
Returns:
|
||||
int: true airspeed in knots
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
spd = util.bin2int(d[46:56]) * 2 # kts
|
||||
return spd
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
# DF 20/21, BDS 6,0
|
||||
# ------------------------------------------
|
||||
|
||||
def isBDS60(msg):
|
||||
"""Check if a message is likely to be BDS code 6,0
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
# status bit 1, 13, 24, 35, 46
|
||||
d = util.hex2bin(data(msg))
|
||||
|
||||
result = True
|
||||
|
||||
result = result & checkbits(d, 1, 2, 12) & checkbits(d, 13, 14, 23) \
|
||||
& checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \
|
||||
& checkbits(d, 46, 47, 56)
|
||||
|
||||
if not (1 < ias(msg) < 500):
|
||||
result &= False
|
||||
|
||||
if not (0.0 < mach(msg) < 1.0):
|
||||
result &= False
|
||||
|
||||
if abs(baro_vr(msg)) > 5000:
|
||||
result &= False
|
||||
|
||||
if abs(ins_vr(msg)) > 5000:
|
||||
result &= False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def heading(msg):
|
||||
"""Megnetic heading of aircraft
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
Returns:
|
||||
float: heading in degrees to megnetic north (from 0 to 360)
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
sign = int(d[1]) # 1 -> west
|
||||
value = util.bin2int(d[2:12]) * 90 / 512.0 # degree
|
||||
hdg = 360 - value if sign else value
|
||||
return round(hdg, 1)
|
||||
|
||||
|
||||
def ias(msg):
|
||||
"""Indicated airspeed
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
Returns:
|
||||
int: indicated airspeed in knots
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
ias = util.bin2int(d[13:23]) # kts
|
||||
return ias
|
||||
|
||||
|
||||
def mach(msg):
|
||||
"""Aircraft MACH number
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
Returns:
|
||||
float: MACH number
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
mach = util.bin2int(d[24:34]) * 2.048 / 512.0
|
||||
return round(mach, 3)
|
||||
|
||||
|
||||
def baro_vr(msg):
|
||||
"""Vertical rate from barometric measurement
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
Returns:
|
||||
int: vertical rate in feet/minutes
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
sign = d[35] # 1 -> minus
|
||||
value = util.bin2int(d[36:45]) * 32 # feet/min
|
||||
roc = -1*value if sign else value
|
||||
return roc
|
||||
|
||||
|
||||
def ins_vr(msg):
|
||||
"""Vertical rate messured by onbard equiments (IRS, AHRS)
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message (BDS60) string
|
||||
Returns:
|
||||
int: vertical rate in feet/minutes
|
||||
"""
|
||||
d = util.hex2bin(data(msg))
|
||||
sign = d[46] # 1 -> minus
|
||||
value = util.bin2int(d[47:56]) * 32 # feet/min
|
||||
roc = -1*value if sign else value
|
||||
return roc
|
||||
|
||||
|
||||
def BDS(msg):
|
||||
"""Estimate the most likely BDS code of an message
|
||||
Args:
|
||||
msg (String): 28 bytes hexadecimal message string
|
||||
Returns:
|
||||
String|None: Version: "BDS40", "BDS50", or "BDS60". Or None, if nothing
|
||||
matched
|
||||
"""
|
||||
is2 = isBDS20(msg)
|
||||
is4 = isBDS40(msg)
|
||||
is5 = isBDS50(msg)
|
||||
is6 = isBDS60(msg)
|
||||
if is2 and not is4 and not is5 and not is6:
|
||||
return "BDS20"
|
||||
elif not is2 and is4 and not is5 and not is6:
|
||||
return "BDS40"
|
||||
elif not is2 and not is4 and is5 and not is6:
|
||||
return "BDS50"
|
||||
elif not is2 and not is4 and not is5 and is6:
|
||||
return "BDS60"
|
||||
else:
|
||||
return None
|
||||
1
pyModeS/extra/__init__.py
Normal file
1
pyModeS/extra/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
185
pyModeS/extra/aero.py
Normal file
185
pyModeS/extra/aero.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Functions for aeronautics in this module
|
||||
|
||||
- physical quantities always in SI units
|
||||
- lat,lon,course and heading in degrees
|
||||
|
||||
International Standard Atmosphere
|
||||
::
|
||||
|
||||
p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m]
|
||||
a = vsound(H) # speed of sound [m/s] as function of H[m]
|
||||
p = pressure(H) # calls atmos but returns only pressure [Pa]
|
||||
T = temperature(H) # calculates temperature [K]
|
||||
rho = density(H) # calls atmos but returns only pressure [Pa]
|
||||
|
||||
Speed conversion at altitude H[m] in ISA
|
||||
::
|
||||
|
||||
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
|
||||
Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion
|
||||
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
|
||||
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
|
||||
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
|
||||
Vcas = tas2cas(Vtas,H) # Vtas to Vcas conversion both m/s, H in [m]
|
||||
Vcas = mach2cas(Mach,H) # Mach to Vcas conversion Vcas in m/s, H in [m]
|
||||
Mach = cas2mach(Vcas,H) # Vcas to mach copnversion Vcas in m/s, H in [m]
|
||||
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
"""Aero and geo Constants """
|
||||
kts = 0.514444 # knot -> m/s
|
||||
ft = 0.3048 # ft -> m
|
||||
fpm = 0.00508 # ft/min -> m/s
|
||||
inch = 0.0254 # inch -> m
|
||||
sqft = 0.09290304 # 1 square foot
|
||||
nm = 1852.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.0, H - 11000.0)
|
||||
rho = rhotrop * np.exp(-dhstrat / 6341.552161)
|
||||
p = rho * R * T
|
||||
return p, rho, T
|
||||
|
||||
|
||||
def temperature(H):
|
||||
p, r, T = atmos(H)
|
||||
return T
|
||||
|
||||
|
||||
def pressure(H):
|
||||
p, r, T = atmos(H)
|
||||
return p
|
||||
|
||||
|
||||
def density(H):
|
||||
p, r, T = atmos(H)
|
||||
return r
|
||||
|
||||
|
||||
def vsound(H):
|
||||
"""Speed of sound"""
|
||||
T = temperature(H)
|
||||
a = np.sqrt(gamma * R * T)
|
||||
return a
|
||||
|
||||
|
||||
def distance(lat1, lon1, lat2, lon2, H=0):
|
||||
"""
|
||||
Compute spherical distance from spherical coordinates.
|
||||
|
||||
For two locations in spherical coordinates
|
||||
(1, theta, phi) and (1, theta', phi')
|
||||
cosine( arc length ) =
|
||||
sin phi sin phi' cos(theta-theta') + cos phi cos phi'
|
||||
distance = rho * arc length
|
||||
"""
|
||||
|
||||
# phi = 90 - latitude
|
||||
phi1 = np.radians(90.0 - lat1)
|
||||
phi2 = np.radians(90.0 - lat2)
|
||||
|
||||
# theta = longitude
|
||||
theta1 = np.radians(lon1)
|
||||
theta2 = np.radians(lon2)
|
||||
|
||||
cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(
|
||||
phi2
|
||||
)
|
||||
cos = np.where(cos > 1, 1, cos)
|
||||
|
||||
arc = np.arccos(cos)
|
||||
dist = arc * (r_earth + H) # meters, radius of earth
|
||||
return dist
|
||||
|
||||
|
||||
def bearing(lat1, lon1, lat2, lon2):
|
||||
lat1 = np.radians(lat1)
|
||||
lon1 = np.radians(lon1)
|
||||
lat2 = np.radians(lat2)
|
||||
lon2 = np.radians(lon2)
|
||||
x = np.sin(lon2 - lon1) * np.cos(lat2)
|
||||
y = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(lon2 - lon1)
|
||||
initial_bearing = np.arctan2(x, y)
|
||||
initial_bearing = np.degrees(initial_bearing)
|
||||
bearing = (initial_bearing + 360) % 360
|
||||
return bearing
|
||||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Speed conversions, altitude H all in meters
|
||||
# -----------------------------------------------------
|
||||
def tas2mach(Vtas, H):
|
||||
"""True Airspeed to Mach number"""
|
||||
a = vsound(H)
|
||||
Mach = Vtas / a
|
||||
return Mach
|
||||
|
||||
|
||||
def mach2tas(Mach, H):
|
||||
"""Mach number to True Airspeed"""
|
||||
a = vsound(H)
|
||||
Vtas = Mach * a
|
||||
return Vtas
|
||||
|
||||
|
||||
def eas2tas(Veas, H):
|
||||
"""Equivalent Airspeed to True Airspeed"""
|
||||
rho = density(H)
|
||||
Vtas = Veas * np.sqrt(rho0 / rho)
|
||||
return Vtas
|
||||
|
||||
|
||||
def tas2eas(Vtas, H):
|
||||
"""True Airspeed to Equivalent Airspeed"""
|
||||
rho = density(H)
|
||||
Veas = Vtas * np.sqrt(rho / rho0)
|
||||
return Veas
|
||||
|
||||
|
||||
def cas2tas(Vcas, H):
|
||||
"""Calibrated Airspeed to True Airspeed"""
|
||||
p, rho, T = atmos(H)
|
||||
qdyn = p0 * ((1.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.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
|
||||
|
||||
|
||||
def mach2cas(Mach, H):
|
||||
"""Mach number to Calibrated Airspeed"""
|
||||
Vtas = mach2tas(Mach, H)
|
||||
Vcas = tas2cas(Vtas, H)
|
||||
return Vcas
|
||||
|
||||
|
||||
def cas2mach(Vcas, H):
|
||||
"""Calibrated Airspeed to Mach number"""
|
||||
Vtas = cas2tas(Vcas, H)
|
||||
Mach = tas2mach(Vtas, H)
|
||||
return Mach
|
||||
162
pyModeS/extra/rtlreader.py
Normal file
162
pyModeS/extra/rtlreader.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import numpy as np
|
||||
import pyModeS as pms
|
||||
from rtlsdr import RtlSdr
|
||||
import time
|
||||
|
||||
modes_sample_rate = 2e6
|
||||
modes_frequency = 1090e6
|
||||
buffer_size = 1024 * 100
|
||||
read_size = 1024 * 20
|
||||
|
||||
pbits = 8
|
||||
fbits = 112
|
||||
preamble = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]
|
||||
th_amp = 0.2 # signal amplitude threshold for 0 and 1 bit
|
||||
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 = []
|
||||
self.sdr = RtlSdr()
|
||||
self.sdr.sample_rate = modes_sample_rate
|
||||
self.sdr.center_freq = modes_frequency
|
||||
self.sdr.gain = "auto"
|
||||
# sdr.freq_correction = 75
|
||||
|
||||
self.debug = kwargs.get("debug", False)
|
||||
self.raw_pipe_in = None
|
||||
self.stop_flag = False
|
||||
|
||||
def _process_buffer(self):
|
||||
messages = []
|
||||
|
||||
# signal_array = np.array(self.signal_buffer)
|
||||
# pulses_array = np.where(np.array(self.signal_buffer) < th_amp, 0, 1)
|
||||
# pulses = "".join(str(x) for x in pulses_array)
|
||||
buffer_length = len(self.signal_buffer)
|
||||
|
||||
i = 0
|
||||
while i < buffer_length:
|
||||
if self.signal_buffer[i] < th_amp:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# if pulses[i : i + pbits * 2] == preamble:
|
||||
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]
|
||||
|
||||
msgbin = ""
|
||||
for j in range(0, frame_length, 2):
|
||||
p2 = frame_pulses[j : j + 2]
|
||||
if len(p2) < 2:
|
||||
break
|
||||
|
||||
if p2[0] < th_amp and p2[1] < th_amp:
|
||||
break
|
||||
elif p2[0] >= p2[1]:
|
||||
c = "1"
|
||||
elif p2[0] < p2[1]:
|
||||
c = "0"
|
||||
else:
|
||||
msgbin = ""
|
||||
break
|
||||
msgbin += c
|
||||
|
||||
# advance i with a jump
|
||||
i = frame_start + j
|
||||
|
||||
if len(msgbin) > 0:
|
||||
msghex = pms.bin2hex(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
|
||||
|
||||
# keep reminder of buffer for next iteration
|
||||
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):
|
||||
# scaling signal (imporatant)
|
||||
amp = np.absolute(data)
|
||||
amp_norm = np.interp(amp, (amp.min(), amp.max()), (0, 1))
|
||||
self.signal_buffer.extend(amp_norm.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.cancel_read_async()
|
||||
|
||||
def run(self, raw_pipe_in=None, stop_flag=None):
|
||||
self.raw_pipe_in = raw_pipe_in
|
||||
self.stop_flag = stop_flag
|
||||
self.sdr.read_samples_async(self._read_callback, read_size)
|
||||
|
||||
# count = 1
|
||||
# while count < 1000:
|
||||
# count += 1
|
||||
# data = self.sdr.read_samples(read_size)
|
||||
# self._read_callback(data, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import signal
|
||||
|
||||
rtl = RtlReader()
|
||||
signal.signal(signal.SIGINT, rtl.stop)
|
||||
|
||||
rtl.debug = True
|
||||
rtl.run()
|
||||
313
pyModeS/extra/tcpclient.py
Normal file
313
pyModeS/extra/tcpclient.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""Stream beast raw data from a TCP server, convert to mode-s messages."""
|
||||
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import pyModeS as pms
|
||||
import traceback
|
||||
import zmq
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
PY_VERSION = 3
|
||||
else:
|
||||
PY_VERSION = 2
|
||||
|
||||
|
||||
class TcpClient(object):
|
||||
def __init__(self, host, port, datatype):
|
||||
super(TcpClient, self).__init__()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.buffer = []
|
||||
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
|
||||
|
||||
def connect(self):
|
||||
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_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
|
||||
ts = time.time()
|
||||
messages.append([self.current_msg, ts])
|
||||
if b == 42:
|
||||
msg_stop = False
|
||||
self.current_msg = ""
|
||||
|
||||
if (not msg_stop) and (48 <= b <= 57 or 65 <= b <= 70 or 97 <= b <= 102):
|
||||
self.current_msg = self.current_msg + chr(b)
|
||||
|
||||
self.buffer = []
|
||||
|
||||
return messages
|
||||
|
||||
def read_beast_buffer(self):
|
||||
"""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,
|
||||
7 byte Mode-S short frame
|
||||
<esc> "3" : 6 byte MLAT timestamp, 1 byte signal level,
|
||||
14 byte Mode-S long frame
|
||||
<esc> "4" : 6 byte MLAT timestamp, status data, DIP switch
|
||||
configuration settings (not on Mode-S Beast classic)
|
||||
<esc><esc>: true 0x1a
|
||||
<esc> is 0x1a, and "1", "2" and "3" are 0x31, 0x32 and 0x33
|
||||
|
||||
timestamp:
|
||||
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
|
||||
"""
|
||||
messages_mlat = []
|
||||
msg = []
|
||||
i = 0
|
||||
|
||||
# process the buffer until the last divider <esc> 0x1a
|
||||
# then, reset the self.buffer with the remainder
|
||||
|
||||
while i < len(self.buffer):
|
||||
if self.buffer[i : i + 2] == [0x1A, 0x1A]:
|
||||
msg.append(0x1A)
|
||||
i += 1
|
||||
elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1A):
|
||||
# special case where the last bit is 0x1a
|
||||
msg.append(0x1A)
|
||||
elif self.buffer[i] == 0x1A:
|
||||
if i == len(self.buffer) - 1:
|
||||
# special case where the last bit is 0x1a
|
||||
msg.append(0x1A)
|
||||
elif len(msg) > 0:
|
||||
messages_mlat.append(msg)
|
||||
msg = []
|
||||
else:
|
||||
msg.append(self.buffer[i])
|
||||
i += 1
|
||||
|
||||
# save the reminder for next reading cycle, if not empty
|
||||
if len(msg) > 0:
|
||||
reminder = []
|
||||
for i, m in enumerate(msg):
|
||||
if (m == 0x1A) and (i < len(msg) - 1):
|
||||
# rewind 0x1a, except when it is at the last bit
|
||||
reminder.extend([m, m])
|
||||
else:
|
||||
reminder.append(m)
|
||||
self.buffer = [0x1A] + msg
|
||||
else:
|
||||
self.buffer = []
|
||||
|
||||
# extract messages
|
||||
messages = []
|
||||
for mm in messages_mlat:
|
||||
ts = time.time()
|
||||
|
||||
msgtype = mm[0]
|
||||
# print(''.join('%02X' % i for i in mm))
|
||||
|
||||
if msgtype == 0x32:
|
||||
# Mode-S Short Message, 7 byte, 14-len hexstr
|
||||
msg = "".join("%02X" % i for i in mm[8:15])
|
||||
elif msgtype == 0x33:
|
||||
# Mode-S Long Message, 14 byte, 28-len hexstr
|
||||
msg = "".join("%02X" % i for i in mm[8:22])
|
||||
else:
|
||||
# Other message tupe
|
||||
continue
|
||||
|
||||
if len(msg) not in [14, 28]:
|
||||
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):
|
||||
"""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
|
||||
|
||||
if len(self.buffer) <= SS_MSGLENGTH:
|
||||
return None
|
||||
|
||||
messages = []
|
||||
while len(self.buffer) > SS_MSGLENGTH:
|
||||
i = 0
|
||||
if (
|
||||
self.buffer[i] == SS_STARTCHAR
|
||||
and self.buffer[i + SS_MSGLENGTH] == SS_STARTCHAR
|
||||
):
|
||||
i += 1
|
||||
if self.buffer[i] >> 7:
|
||||
# Long message
|
||||
payload = self.buffer[i : i + 14]
|
||||
else:
|
||||
# Short message
|
||||
payload = self.buffer[i : i + 7]
|
||||
msg = "".join("%02X" % j for j in payload)
|
||||
i += 14 # Both message types use 14 bytes
|
||||
tsbin = self.buffer[i : i + 6]
|
||||
sec = ((tsbin[0] & 0x7F) << 10) | (tsbin[1] << 2) | (tsbin[2] >> 6)
|
||||
nano = (
|
||||
((tsbin[2] & 0x3F) << 24)
|
||||
| (tsbin[3] << 16)
|
||||
| (tsbin[4] << 8)
|
||||
| tsbin[5]
|
||||
)
|
||||
ts = sec + nano * 1.0e-9
|
||||
i += 6
|
||||
# Signal and noise level - Don't care for now
|
||||
i += 3
|
||||
self.buffer = self.buffer[SS_MSGLENGTH:]
|
||||
messages.append([msg, ts])
|
||||
else:
|
||||
self.buffer = self.buffer[1:]
|
||||
return messages
|
||||
|
||||
def handle_messages(self, messages):
|
||||
"""re-implement this method to handle the messages"""
|
||||
for msg, t in messages:
|
||||
print("%15.9f %s" % (t, msg))
|
||||
|
||||
def run(self, raw_pipe_in=None, stop_flag=None):
|
||||
self.raw_pipe_in = raw_pipe_in
|
||||
self.stop_flag = stop_flag
|
||||
self.connect()
|
||||
|
||||
while True:
|
||||
try:
|
||||
received = [i for i in self.socket.recv(4096)]
|
||||
|
||||
if PY_VERSION == 2:
|
||||
received = [ord(i) for i in received]
|
||||
|
||||
self.buffer.extend(received)
|
||||
# print(''.join(x.encode('hex') for x in self.buffer))
|
||||
|
||||
# process self.buffer when it is longer enough
|
||||
# if len(self.buffer) < 2048:
|
||||
# continue
|
||||
# -- Removed!! Cause delay in low data rate scenario --
|
||||
|
||||
if self.datatype == "beast":
|
||||
messages = self.read_beast_buffer()
|
||||
elif self.datatype == "raw":
|
||||
messages = self.read_raw_buffer()
|
||||
elif self.datatype == "skysense":
|
||||
messages = self.read_skysense_buffer()
|
||||
|
||||
if not messages:
|
||||
continue
|
||||
else:
|
||||
self.handle_messages(messages)
|
||||
|
||||
except Exception as e:
|
||||
# Provides the user an option to supply the environment
|
||||
# variable PYMODES_DEBUG to halt the execution
|
||||
# for debugging purposes
|
||||
debug_intent = os.environ.get("PYMODES_DEBUG", "false")
|
||||
if debug_intent.lower() == "true":
|
||||
traceback.print_exc()
|
||||
sys.exit()
|
||||
else:
|
||||
print("Unexpected Error:", e)
|
||||
|
||||
try:
|
||||
sock = self.connect()
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print("Unexpected Error:", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# for testing purpose only
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
datatype = sys.argv[3]
|
||||
client = TcpClient(host=host, port=port, datatype=datatype)
|
||||
client.run()
|
||||
0
pyModeS/streamer/__init__.py
Normal file
0
pyModeS/streamer/__init__.py
Normal file
284
pyModeS/streamer/decode.py
Normal file
284
pyModeS/streamer/decode.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from __future__ import absolute_import, print_function, division
|
||||
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):
|
||||
local_buffer = []
|
||||
while True:
|
||||
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)
|
||||
134
pyModeS/streamer/modeslive
Executable file
134
pyModeS/streamer/modeslive
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function, division
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import curses
|
||||
import signal
|
||||
import multiprocessing
|
||||
from pyModeS.streamer.decode import Decode
|
||||
from pyModeS.streamer.screen import Screen
|
||||
from pyModeS.streamer.source import NetSource, RtlSdrSource
|
||||
|
||||
|
||||
# redirect all stdout to null, avoiding messing up with the screen
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
|
||||
support_rawtypes = ["raw", "beast", "skysense"]
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
help='Choose data source, "rtlsdr" or "net"',
|
||||
required=True,
|
||||
default="net",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--connect",
|
||||
help="Define server, port and data type. Supported data types are: %s"
|
||||
% support_rawtypes,
|
||||
nargs=3,
|
||||
metavar=("SERVER", "PORT", "DATATYPE"),
|
||||
default=None,
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--latlon",
|
||||
help="Receiver latitude and longitude, needed for the surface position, default none",
|
||||
nargs=2,
|
||||
metavar=("LAT", "LON"),
|
||||
default=None,
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-uncertainty",
|
||||
dest="uncertainty",
|
||||
help="Display uncertainty values, default off",
|
||||
action="store_true",
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dumpto",
|
||||
help="Folder to dump decoded output, default none",
|
||||
required=False,
|
||||
default=None,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
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 not os.path.isdir(DUMPTO):
|
||||
print("Error: dump folder (%s) does not exist" % DUMPTO)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# raw_event = multiprocessing.Event()
|
||||
# ac_event = multiprocessing.Event()
|
||||
# raw_queue = multiprocessing.Queue()
|
||||
# ac_queue = multiprocessing.Queue()
|
||||
|
||||
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
|
||||
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
|
||||
stop_flag = multiprocessing.Value("b", False)
|
||||
|
||||
if SOURCE == "net":
|
||||
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
|
||||
elif SOURCE == "rtlsdr":
|
||||
source = RtlSdrSource()
|
||||
|
||||
|
||||
recv_process = multiprocessing.Process(target=source.run, args=(raw_pipe_in, stop_flag))
|
||||
|
||||
|
||||
decode = Decode(latlon=LATLON, dumpto=DUMPTO)
|
||||
decode_process = multiprocessing.Process(
|
||||
target=decode.run, args=(raw_pipe_out, ac_pipe_in)
|
||||
)
|
||||
|
||||
screen = Screen(uncertainty=UNCERTAINTY)
|
||||
screen_process = multiprocessing.Process(target=screen.run, args=(ac_pipe_out,))
|
||||
|
||||
|
||||
def closeall(signal, frame):
|
||||
print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal))
|
||||
stop_flag.value = True
|
||||
curses.endwin()
|
||||
recv_process.terminate()
|
||||
decode_process.terminate()
|
||||
screen_process.terminate()
|
||||
recv_process.join()
|
||||
decode_process.join()
|
||||
screen_process.join()
|
||||
exit(0)
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, closeall)
|
||||
|
||||
recv_process.start()
|
||||
decode_process.start()
|
||||
screen_process.start()
|
||||
210
pyModeS/streamer/screen.py
Normal file
210
pyModeS/streamer/screen.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from __future__ import print_function, division
|
||||
import curses
|
||||
import numpy as np
|
||||
import time
|
||||
import threading
|
||||
|
||||
COLUMNS = [
|
||||
("call", 10),
|
||||
("lat", 10),
|
||||
("lon", 10),
|
||||
("alt", 7),
|
||||
("gs", 5),
|
||||
("tas", 5),
|
||||
("ias", 5),
|
||||
("mach", 7),
|
||||
("roc", 7),
|
||||
("trk", 10),
|
||||
("hdg", 10),
|
||||
("live", 6),
|
||||
]
|
||||
|
||||
UNCERTAINTY_COLUMNS = [
|
||||
("|", 5),
|
||||
("ver", 4),
|
||||
("HPL", 5),
|
||||
("RCu", 5),
|
||||
("RCv", 5),
|
||||
("HVE", 5),
|
||||
("VVE", 5),
|
||||
("Rc", 4),
|
||||
("VPL", 5),
|
||||
("EPU", 5),
|
||||
("VEPU", 6),
|
||||
("HFOMr", 7),
|
||||
("VFOMr", 7),
|
||||
("PE_RCu", 8),
|
||||
("PE_VPL", 8),
|
||||
]
|
||||
|
||||
|
||||
class Screen(object):
|
||||
def __init__(self, uncertainty=False):
|
||||
super(Screen, self).__init__()
|
||||
self.screen = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.mousemask(1)
|
||||
self.screen.keypad(True)
|
||||
self.y = 3
|
||||
self.x = 1
|
||||
self.offset = 0
|
||||
self.acs = {}
|
||||
self.lock_icao = None
|
||||
|
||||
self.columns = COLUMNS
|
||||
if uncertainty:
|
||||
self.columns.extend(UNCERTAINTY_COLUMNS)
|
||||
|
||||
def reset_cursor_pos(self):
|
||||
self.screen.move(self.y, self.x)
|
||||
|
||||
def update_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),
|
||||
)
|
||||
|
||||
def update(self):
|
||||
if len(self.acs) == 0:
|
||||
return
|
||||
|
||||
resized = curses.is_term_resized(self.scr_h, self.scr_w)
|
||||
if resized is True:
|
||||
self.scr_h, self.scr_w = self.screen.getmaxyx()
|
||||
self.screen.clear()
|
||||
curses.resizeterm(self.scr_h, self.scr_w)
|
||||
|
||||
self.screen.refresh()
|
||||
self.draw_frame()
|
||||
|
||||
row = 1
|
||||
|
||||
header = " icao"
|
||||
for c, cw in self.columns:
|
||||
header += (cw - len(c)) * " " + c
|
||||
|
||||
# fill end with spaces
|
||||
header += (self.scr_w - 2 - len(header)) * " "
|
||||
|
||||
if len(header) > self.scr_w - 2:
|
||||
header = header[: self.scr_w - 3] + ">"
|
||||
|
||||
self.screen.addstr(row, 1, header)
|
||||
|
||||
row += 1
|
||||
self.screen.addstr(row, 1, "-" * (self.scr_w - 2))
|
||||
|
||||
icaos = np.array(list(self.acs.keys()))
|
||||
icaos = np.sort(icaos)
|
||||
|
||||
for row in range(3, self.scr_h - 3):
|
||||
icao = None
|
||||
idx = row + self.offset - 3
|
||||
|
||||
if idx > len(icaos) - 1:
|
||||
line = " " * (self.scr_w - 2)
|
||||
|
||||
else:
|
||||
line = ""
|
||||
|
||||
icao = icaos[idx]
|
||||
ac = self.acs[icao]
|
||||
|
||||
line += icao
|
||||
|
||||
for c, cw in self.columns:
|
||||
if c == "|":
|
||||
val = "|"
|
||||
elif c == "live":
|
||||
val = str(ac[c] - int(time.time())) + "s"
|
||||
elif ac[c] is None:
|
||||
val = ""
|
||||
else:
|
||||
val = ac[c]
|
||||
val_str = str(val)
|
||||
line += (cw - len(val_str)) * " " + val_str
|
||||
|
||||
# fill end with spaces
|
||||
line += (self.scr_w - 2 - len(line)) * " "
|
||||
|
||||
if len(line) > self.scr_w - 2:
|
||||
line = line[: self.scr_w - 3] + ">"
|
||||
|
||||
if (icao is not None) and (self.lock_icao == icao):
|
||||
self.screen.addstr(row, 1, line, curses.A_STANDOUT)
|
||||
elif row == self.y:
|
||||
self.screen.addstr(row, 1, line, curses.A_BOLD)
|
||||
else:
|
||||
self.screen.addstr(row, 1, line)
|
||||
|
||||
self.screen.addstr(self.scr_h - 3, 1, "-" * (self.scr_w - 2))
|
||||
|
||||
total_page = len(icaos) // (self.scr_h - 4) + 1
|
||||
current_page = self.offset // (self.scr_h - 4) + 1
|
||||
self.screen.addstr(self.scr_h - 2, 1, "(%d / %d)" % (current_page, total_page))
|
||||
|
||||
self.reset_cursor_pos()
|
||||
|
||||
def kye_handling(self):
|
||||
self.draw_frame()
|
||||
self.scr_h, self.scr_w = self.screen.getmaxyx()
|
||||
|
||||
while True:
|
||||
c = self.screen.getch()
|
||||
|
||||
if c == curses.KEY_HOME:
|
||||
self.x = 1
|
||||
self.y = 1
|
||||
elif c == curses.KEY_NPAGE:
|
||||
offset_intent = self.offset + (self.scr_h - 4)
|
||||
if offset_intent < len(self.acs) - 5:
|
||||
self.offset = offset_intent
|
||||
elif c == curses.KEY_PPAGE:
|
||||
offset_intent = self.offset - (self.scr_h - 4)
|
||||
if offset_intent > 0:
|
||||
self.offset = offset_intent
|
||||
else:
|
||||
self.offset = 0
|
||||
elif c == curses.KEY_DOWN:
|
||||
y_intent = self.y + 1
|
||||
if y_intent < self.scr_h - 3:
|
||||
self.y = y_intent
|
||||
elif c == curses.KEY_UP:
|
||||
y_intent = self.y - 1
|
||||
if y_intent > 2:
|
||||
self.y = y_intent
|
||||
elif c == curses.KEY_ENTER or c == 10 or c == 13:
|
||||
self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode()
|
||||
elif c == 27: # escape key
|
||||
self.lock_icao = None
|
||||
elif c == curses.KEY_F5:
|
||||
self.screen.refresh()
|
||||
self.draw_frame()
|
||||
|
||||
def run(self, ac_pipe_out):
|
||||
local_buffer = []
|
||||
key_thread = threading.Thread(target=self.kye_handling)
|
||||
key_thread.start()
|
||||
|
||||
while True:
|
||||
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 = []
|
||||
|
||||
try:
|
||||
self.update()
|
||||
except:
|
||||
pass
|
||||
|
||||
time.sleep(0.001)
|
||||
91
pyModeS/streamer/source.py
Normal file
91
pyModeS/streamer/source.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import pyModeS as pms
|
||||
from pyModeS.extra.tcpclient import TcpClient
|
||||
from pyModeS.extra.rtlreader import RtlReader
|
||||
|
||||
|
||||
class NetSource(TcpClient):
|
||||
def __init__(self, host, port, rawtype):
|
||||
super(NetSource, self).__init__(host, port, rawtype)
|
||||
self.reset_local_buffer()
|
||||
|
||||
def reset_local_buffer(self):
|
||||
self.local_buffer_adsb_msg = []
|
||||
self.local_buffer_adsb_ts = []
|
||||
self.local_buffer_commb_msg = []
|
||||
self.local_buffer_commb_ts = []
|
||||
|
||||
def handle_messages(self, messages):
|
||||
|
||||
if self.stop_flag.value is True:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
for msg, t in messages:
|
||||
if len(msg) < 28: # only process long messages
|
||||
continue
|
||||
|
||||
df = pms.df(msg)
|
||||
|
||||
if df == 17 or df == 18:
|
||||
self.local_buffer_adsb_msg.append(msg)
|
||||
self.local_buffer_adsb_ts.append(t)
|
||||
elif df == 20 or df == 21:
|
||||
self.local_buffer_commb_msg.append(msg)
|
||||
self.local_buffer_commb_ts.append(t)
|
||||
else:
|
||||
continue
|
||||
|
||||
if len(self.local_buffer_adsb_msg) > 1:
|
||||
self.raw_pipe_in.send(
|
||||
{
|
||||
"adsb_ts": self.local_buffer_adsb_ts,
|
||||
"adsb_msg": self.local_buffer_adsb_msg,
|
||||
"commb_ts": self.local_buffer_commb_ts,
|
||||
"commb_msg": self.local_buffer_commb_msg,
|
||||
}
|
||||
)
|
||||
self.reset_local_buffer()
|
||||
|
||||
|
||||
class RtlSdrSource(RtlReader):
|
||||
def __init__(self):
|
||||
super(RtlSdrSource, self).__init__()
|
||||
self.reset_local_buffer()
|
||||
|
||||
def reset_local_buffer(self):
|
||||
self.local_buffer_adsb_msg = []
|
||||
self.local_buffer_adsb_ts = []
|
||||
self.local_buffer_commb_msg = []
|
||||
self.local_buffer_commb_ts = []
|
||||
|
||||
def handle_messages(self, messages):
|
||||
|
||||
if self.stop_flag.value is True:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
for msg, t in messages:
|
||||
if len(msg) < 28: # only process long messages
|
||||
continue
|
||||
|
||||
df = pms.df(msg)
|
||||
|
||||
if df == 17 or df == 18:
|
||||
self.local_buffer_adsb_msg.append(msg)
|
||||
self.local_buffer_adsb_ts.append(t)
|
||||
elif df == 20 or df == 21:
|
||||
self.local_buffer_commb_msg.append(msg)
|
||||
self.local_buffer_commb_ts.append(t)
|
||||
else:
|
||||
continue
|
||||
|
||||
if len(self.local_buffer_adsb_msg) > 1:
|
||||
self.raw_pipe_in.send(
|
||||
{
|
||||
"adsb_ts": self.local_buffer_adsb_ts,
|
||||
"adsb_msg": self.local_buffer_adsb_msg,
|
||||
"commb_ts": self.local_buffer_commb_ts,
|
||||
"commb_msg": self.local_buffer_commb_msg,
|
||||
}
|
||||
)
|
||||
self.reset_local_buffer()
|
||||
@@ -1,73 +0,0 @@
|
||||
"""
|
||||
Common functions for ADS-B and Mode-S EHS decoder
|
||||
|
||||
Copyright (C) 2015 Junzi Sun (TU Delft)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
# the polynominal generattor code for CRC
|
||||
GENERATOR = "1111111111111010000001001"
|
||||
|
||||
|
||||
def hex2bin(hexstr):
|
||||
"""Convert a hexdecimal string to binary string, with zero fillings. """
|
||||
scale = 16
|
||||
num_of_bits = len(hexstr) * math.log(scale, 2)
|
||||
binstr = bin(int(hexstr, scale))[2:].zfill(int(num_of_bits))
|
||||
return binstr
|
||||
|
||||
|
||||
def bin2int(binstr):
|
||||
return int(binstr, 2)
|
||||
|
||||
|
||||
def hex2int(hexstr):
|
||||
return int(hexstr, 16)
|
||||
|
||||
|
||||
def df(msg):
|
||||
"""Decode Downlink Format vaule, bits 1 to 5."""
|
||||
msgbin = hex2bin(msg)
|
||||
return bin2int(msgbin[0:5])
|
||||
|
||||
|
||||
def crc(msg, encode=False):
|
||||
"""Mode-S Cyclic Redundancy Check
|
||||
Detect if bit error occurs in the Mode-S message
|
||||
Args:
|
||||
msg (string): 28 bytes hexadecimal message string
|
||||
encode (bool): True to encode the date only and return the checksum
|
||||
Returns:
|
||||
string: message checksum, or partity bits (encoder)
|
||||
"""
|
||||
|
||||
msgbin = list(hex2bin(msg))
|
||||
|
||||
if encode:
|
||||
msgbin[-24:] = ['0'] * 24
|
||||
|
||||
# loop all bits, except last 24 piraty bits
|
||||
for i in range(len(msgbin)-24):
|
||||
# if 1, perform modulo 2 multiplication,
|
||||
if msgbin[i] == '1':
|
||||
for j in range(len(GENERATOR)):
|
||||
# modulo 2 multiplication = XOR
|
||||
msgbin[i+j] = str((int(msgbin[i+j]) ^ int(GENERATOR[j])))
|
||||
|
||||
# last 24 bits
|
||||
reminder = ''.join(msgbin[-24:])
|
||||
return reminder
|
||||
65
setup.py
65
setup.py
@@ -3,10 +3,18 @@
|
||||
See:
|
||||
https://packaging.python.org/en/latest/distributing.html
|
||||
https://github.com/pypa/sampleproject
|
||||
|
||||
Steps for deploying a new version:
|
||||
1. Increase the version number
|
||||
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
|
||||
@@ -14,73 +22,54 @@ 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',
|
||||
|
||||
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='1.0.4',
|
||||
|
||||
description='Python Mode-S Decoder',
|
||||
version="2.4",
|
||||
description="Python Mode-S and ADS-B Decoder",
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
url='https://github.com/junzis/pyModes',
|
||||
|
||||
url="https://github.com/junzis/pyModeS",
|
||||
# Author details
|
||||
author='Junzi Sun',
|
||||
author_email='j.sun-1@tudelft.nl',
|
||||
|
||||
author="Junzi Sun",
|
||||
author_email="j.sun-1@tudelft.nl",
|
||||
# Choose your license
|
||||
license='GNU GPL v3',
|
||||
|
||||
license="GNU GPL v3",
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
# How mature is this project? Common values are
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 3 - Alpha',
|
||||
|
||||
"Development Status :: 4 - Beta",
|
||||
# Indicate who your project is intended for
|
||||
'Intended Audience :: Developers',
|
||||
'Topic :: Software Development :: Build Tools',
|
||||
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
# Pick your license as you wish (should match "license" above)
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
# 'Programming Language :: Python :: 3',
|
||||
# 'Programming Language :: Python :: 3.3',
|
||||
# 'Programming Language :: Python :: 3.4',
|
||||
# 'Programming Language :: Python :: 3.5',
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 3",
|
||||
],
|
||||
|
||||
# What does your project relate to?
|
||||
keywords='Mode-S ADS-B EHS decoding',
|
||||
|
||||
keywords="Mode-S ADS-B EHS ELS Comm-B",
|
||||
# You can just specify the packages manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||
|
||||
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=[''],
|
||||
|
||||
install_requires=["numpy", "argparse", "pyzmq", "pyrtlsdr"],
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
# dependencies). You can install these using the following syntax,
|
||||
# for example:
|
||||
@@ -89,20 +78,17 @@ setup(
|
||||
# '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.
|
||||
@@ -111,4 +97,5 @@ setup(
|
||||
# 'sample=sample:main',
|
||||
# ],
|
||||
# },
|
||||
scripts=["pyModeS/streamer/modeslive"],
|
||||
)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# the inclusion of the tests module is not meant to offer best practices for
|
||||
# testing in general, but rather to support the `find_packages` example in
|
||||
# setup.py that excludes installing the "tests" package
|
||||
5000
tests/data/sample_data_commb_df20.csv
Normal file
5000
tests/data/sample_data_commb_df20.csv
Normal file
File diff suppressed because it is too large
Load Diff
5000
tests/data/sample_data_commb_df21.csv
Normal file
5000
tests/data/sample_data_commb_df21.csv
Normal file
File diff suppressed because it is too large
Load Diff
2000
tests/ehs.csv
2000
tests/ehs.csv
File diff suppressed because it is too large
Load Diff
187
tests/run.py
187
tests/run.py
@@ -1,187 +0,0 @@
|
||||
import os, sys, inspect
|
||||
currentdir = os.path.dirname(os.path.abspath(
|
||||
inspect.getfile(inspect.currentframe())))
|
||||
parentdir = os.path.dirname(currentdir)
|
||||
sys.path.insert(0, parentdir)
|
||||
|
||||
import pyModeS as pms
|
||||
from pyModeS import adsb
|
||||
from pyModeS import ehs
|
||||
from pyModeS import util
|
||||
|
||||
|
||||
# === TEST common functions ===
|
||||
def test_hex2bin():
|
||||
assert util.hex2bin('6E406B') == "011011100100000001101011"
|
||||
|
||||
|
||||
def test_crc():
|
||||
# crc decoder
|
||||
checksum = util.crc("8D406B902015A678D4D220AA4BDA")
|
||||
assert checksum == "000000000000000000000000"
|
||||
|
||||
# crc encoder
|
||||
parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True)
|
||||
assert util.hex2bin("AA4BDA") == parity
|
||||
|
||||
|
||||
# === TEST ADS-B package ===
|
||||
|
||||
def test_adsb_icao():
|
||||
assert adsb.icao("8D406B902015A678D4D220AA4BDA") == "406B90"
|
||||
|
||||
|
||||
def test_adsb_category():
|
||||
assert adsb.category("8D406B902015A678D4D220AA4BDA") == 5
|
||||
|
||||
|
||||
def test_adsb_callsign():
|
||||
assert adsb.callsign("8D406B902015A678D4D220AA4BDA") == "EZY85MH_"
|
||||
|
||||
|
||||
def test_adsb_position():
|
||||
pos = adsb.position("8D40058B58C901375147EFD09357",
|
||||
"8D40058B58C904A87F402D3B8C59",
|
||||
1446332400, 1446332405)
|
||||
assert pos == (49.81755, 6.08442)
|
||||
|
||||
|
||||
def test_adsb_alt():
|
||||
assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000
|
||||
|
||||
|
||||
def test_adsb_velocity():
|
||||
vgs = adsb.velocity("8D485020994409940838175B284F")
|
||||
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
|
||||
assert vgs == (159, 182.9, -263, 'GS')
|
||||
assert vas == (376, 244.0, -274, 'AS')
|
||||
|
||||
|
||||
def test_nic():
|
||||
assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
|
||||
assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
|
||||
assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
|
||||
assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
|
||||
assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
|
||||
assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
|
||||
assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
|
||||
assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
|
||||
assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
|
||||
assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
|
||||
assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
|
||||
assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11
|
||||
|
||||
|
||||
# === TEST Mode-S EHS package ===
|
||||
|
||||
def test_ehs_icao():
|
||||
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
|
||||
assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
|
||||
assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
|
||||
|
||||
|
||||
def test_ehs_BDS():
|
||||
assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
|
||||
assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
|
||||
assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
|
||||
assert ehs.BDS("A000029CFFBAA11E2004727281F1") == 'BDS60'
|
||||
assert ehs.BDS("A0281838CAE9E12FA03FFF2DDDE5") is None
|
||||
|
||||
|
||||
def test_ehs_BDS20_callsign():
|
||||
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
|
||||
assert ehs.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
|
||||
|
||||
|
||||
def test_ehs_BDS40_functions():
|
||||
assert ehs.alt_mcp("A000029C85E42F313000007047D3") == 3008
|
||||
assert ehs.alt_fms("A000029C85E42F313000007047D3") == 3008
|
||||
assert ehs.pbaro("A000029C85E42F313000007047D3") == 1020.0
|
||||
|
||||
|
||||
def test_ehs_BDS50_functions():
|
||||
assert ehs.roll("A000139381951536E024D4CCF6B5") == 2.1
|
||||
assert ehs.track("A000139381951536E024D4CCF6B5") == 114.3
|
||||
assert ehs.gs("A000139381951536E024D4CCF6B5") == 438
|
||||
assert ehs.rtrack("A000139381951536E024D4CCF6B5") == 0.125
|
||||
assert ehs.tas("A000139381951536E024D4CCF6B5") == 424
|
||||
|
||||
|
||||
def test_ehs_BDS60_functions():
|
||||
assert ehs.heading("A000029CFFBAA11E2004727281F1") == 180.9
|
||||
assert ehs.ias("A000029CFFBAA11E2004727281F1") == 336
|
||||
assert ehs.mach("A000029CFFBAA11E2004727281F1") == 0.48
|
||||
assert ehs.baro_vr("A000029CFFBAA11E2004727281F1") == 0
|
||||
assert ehs.ins_vr("A000029CFFBAA11E2004727281F1") == -3648
|
||||
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
def adsb_decode_all(n=None):
|
||||
print "===== Decode all ADS-B sample data====="
|
||||
import csv
|
||||
f = open('adsb.csv', 'rt')
|
||||
|
||||
msg0 = None
|
||||
msg1 = None
|
||||
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
if n and i > n:
|
||||
break
|
||||
|
||||
ts = r[0]
|
||||
m = r[1]
|
||||
icao = adsb.icao(m)
|
||||
tc = adsb.typecode(m)
|
||||
if 1 <= tc <= 4:
|
||||
print ts, m, icao, tc, adsb.category(m), adsb.callsign(m)
|
||||
if tc == 19:
|
||||
print ts, m, icao, tc, adsb.velocity(m)
|
||||
if 5 <= tc <= 18:
|
||||
if adsb.oe_flag(m):
|
||||
msg1 = m
|
||||
t1 = ts
|
||||
else:
|
||||
msg0 = m
|
||||
t0 = ts
|
||||
|
||||
if msg0 and msg1:
|
||||
pos = adsb.position(msg0, msg1, t0, t1)
|
||||
alt = adsb.altitude(m)
|
||||
print ts, m, icao, tc, pos, alt
|
||||
|
||||
|
||||
def ehs_decode_all(n=None):
|
||||
print "===== Decode all Mode-S EHS sample data====="
|
||||
import csv
|
||||
f = open('ehs.csv', 'rt')
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
if n and i > n:
|
||||
break
|
||||
|
||||
ts = r[1]
|
||||
m = r[2]
|
||||
icao = ehs.icao(m)
|
||||
vBDS = ehs.BDS(m)
|
||||
|
||||
if vBDS:
|
||||
if vBDS == "BDS20":
|
||||
print ts, m, icao, vBDS, ehs.callsign(m)
|
||||
|
||||
if vBDS == "BDS40":
|
||||
print ts, m, icao, vBDS, ehs.alt_mcp(m), \
|
||||
ehs.alt_fms(m), ehs.pbaro(m)
|
||||
|
||||
if vBDS == "BDS50":
|
||||
print ts, m, icao, vBDS, ehs.roll(m), ehs.track(m), \
|
||||
ehs.gs(m), ehs.rtrack(m), ehs.tas(m)
|
||||
|
||||
if vBDS == "BDS60":
|
||||
print ts, m, icao, vBDS, ehs.heading(m), ehs.ias(m), \
|
||||
ehs.mach(m), ehs.baro_vr(m), ehs.ins_vr(m)
|
||||
else:
|
||||
print ts, m, icao, vBDS
|
||||
|
||||
if __name__ == '__main__':
|
||||
adsb_decode_all(100)
|
||||
ehs_decode_all(100)
|
||||
44
tests/sample_run_adsb.py
Normal file
44
tests/sample_run_adsb.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import adsb, ehs
|
||||
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
|
||||
def adsb_decode_all(n=None):
|
||||
print("===== Decode ADS-B sample data=====")
|
||||
import csv
|
||||
|
||||
f = open("tests/data/sample_data_adsb.csv", "rt")
|
||||
|
||||
msg0 = None
|
||||
msg1 = None
|
||||
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
if n and i > n:
|
||||
break
|
||||
|
||||
ts = r[0]
|
||||
m = r[1]
|
||||
icao = adsb.icao(m)
|
||||
tc = adsb.typecode(m)
|
||||
if 1 <= tc <= 4:
|
||||
print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m))
|
||||
if tc == 19:
|
||||
print(ts, m, icao, tc, adsb.velocity(m))
|
||||
if 5 <= tc <= 18:
|
||||
if adsb.oe_flag(m):
|
||||
msg1 = m
|
||||
t1 = ts
|
||||
else:
|
||||
msg0 = m
|
||||
t0 = ts
|
||||
|
||||
if msg0 and msg1:
|
||||
pos = adsb.position(msg0, msg1, t0, t1)
|
||||
alt = adsb.altitude(m)
|
||||
print(ts, m, icao, tc, pos, alt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
adsb_decode_all(n=100)
|
||||
92
tests/sample_run_commb.py
Normal file
92
tests/sample_run_commb.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import print_function
|
||||
from pyModeS import commb, common, bds
|
||||
|
||||
# === Decode sample data file ===
|
||||
|
||||
|
||||
def bds_info(BDS, m):
|
||||
if BDS == "BDS10":
|
||||
info = [commb.ovc10(m)]
|
||||
|
||||
elif BDS == "BDS17":
|
||||
info = [i[-2:] for i in commb.cap17(m)]
|
||||
|
||||
elif BDS == "BDS20":
|
||||
info = [commb.cs20(m)]
|
||||
|
||||
elif BDS == "BDS40":
|
||||
info = (commb.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),
|
||||
)
|
||||
|
||||
elif BDS == "BDS50":
|
||||
info = (
|
||||
commb.roll50(m),
|
||||
commb.trk50(m),
|
||||
commb.gs50(m),
|
||||
commb.rtrk50(m),
|
||||
commb.tas50(m),
|
||||
)
|
||||
|
||||
elif BDS == "BDS60":
|
||||
info = (
|
||||
commb.hdg60(m),
|
||||
commb.ias60(m),
|
||||
commb.mach60(m),
|
||||
commb.vr60baro(m),
|
||||
commb.vr60ins(m),
|
||||
)
|
||||
|
||||
else:
|
||||
info = None
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def commb_decode_all(df, n=None):
|
||||
import csv
|
||||
|
||||
print("===== Decode Comm-B sample data (DF=%s)=====" % df)
|
||||
|
||||
f = open("tests/data/sample_data_commb_df%s.csv" % df, "rt")
|
||||
|
||||
for i, r in enumerate(csv.reader(f)):
|
||||
if n and i > n:
|
||||
break
|
||||
|
||||
ts = r[0]
|
||||
m = r[2]
|
||||
|
||||
df = common.df(m)
|
||||
icao = common.icao(m)
|
||||
BDS = bds.infer(m)
|
||||
code = common.altcode(m) if df == 20 else common.idcode(m)
|
||||
|
||||
if not BDS:
|
||||
print(ts, m, icao, df, "%5s" % code, "UNKNOWN")
|
||||
continue
|
||||
|
||||
if len(BDS.split(",")) > 1:
|
||||
print(ts, m, icao, df, "%5s" % code, end=" ")
|
||||
for i, _bds in enumerate(BDS.split(",")):
|
||||
if i == 0:
|
||||
print(_bds, *bds_info(_bds, m))
|
||||
else:
|
||||
print(" " * 55, _bds, *bds_info(_bds, m))
|
||||
|
||||
else:
|
||||
print(ts, m, icao, df, "%5s" % code, BDS, *bds_info(BDS, m))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
commb_decode_all(df=20, n=100)
|
||||
commb_decode_all(df=21, n=100)
|
||||
95
tests/test_adsb.py
Normal file
95
tests/test_adsb.py
Normal file
@@ -0,0 +1,95 @@
|
||||
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") == 0
|
||||
|
||||
|
||||
def test_adsb_callsign():
|
||||
assert adsb.callsign("8D406B902015A678D4D220AA4BDA") == "EZY85MH_"
|
||||
|
||||
|
||||
def test_adsb_position():
|
||||
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)
|
||||
|
||||
|
||||
def test_adsb_position_with_ref():
|
||||
pos = adsb.position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0)
|
||||
assert pos == (49.82410, 6.06785)
|
||||
pos = adsb.position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5)
|
||||
assert pos == (-43.48564, 172.53942)
|
||||
|
||||
|
||||
def test_adsb_airborne_position_with_ref():
|
||||
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)
|
||||
assert pos == (49.81755, 6.08442)
|
||||
|
||||
|
||||
def test_adsb_surface_position_with_ref():
|
||||
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,
|
||||
)
|
||||
assert pos == (-43.48564, 172.53942)
|
||||
|
||||
|
||||
def test_adsb_alt():
|
||||
assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# def test_nic():
|
||||
# assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
|
||||
# assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
|
||||
# assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
|
||||
# assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
|
||||
# assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
|
||||
# assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
|
||||
# assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
|
||||
# assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
|
||||
# assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
|
||||
# assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
|
||||
# assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
|
||||
# assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11
|
||||
36
tests/test_bds_inference.py
Normal file
36
tests/test_bds_inference.py
Normal file
@@ -0,0 +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_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"
|
||||
|
||||
|
||||
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
|
||||
51
tests/test_commb.py
Normal file
51
tests/test_commb.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from pyModeS import bds, commb
|
||||
|
||||
# from pyModeS import ehs, els # deprecated
|
||||
|
||||
|
||||
def test_bds20_callsign():
|
||||
assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == "KLM1017_"
|
||||
assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == "IBK2873_"
|
||||
|
||||
assert commb.cs20("A000083E202CC371C31DE0AA1CCF") == "KLM1017_"
|
||||
assert commb.cs20("A0001993202422F2E37CE038738E") == "IBK2873_"
|
||||
|
||||
|
||||
def test_bds40_functions():
|
||||
assert bds.bds40.selalt40mcp("A000029C85E42F313000007047D3") == 3008
|
||||
assert bds.bds40.selalt40fms("A000029C85E42F313000007047D3") == 3008
|
||||
assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0
|
||||
|
||||
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.trk50("A000139381951536E024D4CCF6B5") == 114.258
|
||||
assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438
|
||||
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
|
||||
assert bds.bds50.tas50("A000139381951536E024D4CCF6B5") == 424
|
||||
|
||||
assert commb.roll50("A000139381951536E024D4CCF6B5") == 2.1
|
||||
assert commb.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
|
||||
assert commb.trk50("A000139381951536E024D4CCF6B5") == 114.258
|
||||
assert commb.gs50("A000139381951536E024D4CCF6B5") == 438
|
||||
assert commb.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
|
||||
assert commb.tas50("A000139381951536E024D4CCF6B5") == 424
|
||||
|
||||
|
||||
def test_bds60_functions():
|
||||
assert bds.bds60.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
|
||||
assert bds.bds60.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
||||
assert bds.bds60.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
||||
assert bds.bds60.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
assert bds.bds60.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
|
||||
assert commb.hdg60("A00004128F39F91A7E27C46ADC21") == 42.715
|
||||
assert commb.ias60("A00004128F39F91A7E27C46ADC21") == 252
|
||||
assert commb.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
|
||||
assert commb.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
assert commb.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
|
||||
64
tests/test_common.py
Normal file
64
tests/test_common.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from pyModeS import common
|
||||
|
||||
|
||||
def test_conversions():
|
||||
assert common.hex2bin("6E406B") == "011011100100000001101011"
|
||||
assert common.bin2hex("011011100100000001101011") == "6E406B"
|
||||
assert common.int2hex(11160538) == "AA4BDA"
|
||||
|
||||
|
||||
def test_crc_decode():
|
||||
assert common.crc_legacy("8D406B902015A678D4D220AA4BDA") == 0
|
||||
|
||||
assert common.crc("8D406B902015A678D4D220AA4BDA") == 0
|
||||
assert common.crc("8d8960ed58bf053cf11bc5932b7d") == 0
|
||||
assert common.crc("8d45cab390c39509496ca9a32912") == 0
|
||||
assert common.crc("8d49d3d4e1089d00000000744c3b") == 0
|
||||
assert common.crc("8d74802958c904e6ef4ba0184d5c") == 0
|
||||
assert common.crc("8d4400cd9b0000b4f87000e71a10") == 0
|
||||
assert common.crc("8d4065de58a1054a7ef0218e226a") == 0
|
||||
|
||||
assert common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924
|
||||
assert common.crc("a800089d8094e33a6004e4b8a522") == 4805588
|
||||
assert common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991
|
||||
assert common.crc("a0000410bc900010a40000f5f477") == 11727682
|
||||
assert common.crc("8d4ca251204994b1c36e60a5343d") == 16
|
||||
assert common.crc("b0001718c65632b0a82040715b65") == 353333
|
||||
|
||||
|
||||
def test_crc_encode():
|
||||
parity = common.crc("8D406B902015A678D4D220AA4BDA", encode=True)
|
||||
assert common.int2hex(parity) == "AA4BDA"
|
||||
|
||||
|
||||
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
|
||||
20
tests/test_tell.py
Normal file
20
tests/test_tell.py
Normal 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)
|
||||
Reference in New Issue
Block a user