135 Commits
v1.2.2 ... v2.2

Author SHA1 Message Date
Junzi Sun
7267859548 pyModeS v2.2 2019-08-05 16:37:30 +02:00
Junzi Sun
7a3bd089c4 update wind null return 2019-08-05 16:25:46 +02:00
Junzi Sun
17fdaca1c7 fix bug for alternative temperature 2019-08-05 14:21:21 +02:00
Junzi Sun
6f6b50776d rename functions for seleted altitudes in BDS40 2019-08-05 10:37:17 +02:00
Junzi Sun
0b7b9ad3dd rewrite CRC function 2019-07-09 15:02:06 +02:00
Junzi Sun
24806f7e88 add downlink format check 2019-06-06 11:46:44 +02:00
Junzi Sun
57ad40ec57 Merge pull request #44 from amhirsch/fix-velocity
Decoder recognizes supersonic velocity subtype
2019-06-06 11:33:26 +02:00
Junzi Sun
d230bdc4a9 Merge pull request #43 from amhirsch/debug-stream
Add debugging option
2019-06-06 11:32:33 +02:00
Alexander Hirsch
4a6d3334a7 Decoder recognizes supersonic velocity subtype
If the aircraft velocity message indicates a supersonic subtype
(2 or 4), multiply the derived velocity by four. This is in accordance
with Table A-2-9[a, b] in the "Technical Provisions for Mode-S Services
and Extended Squitter"
2019-06-05 16:11:50 -07:00
Alexander Hirsch
9f371fe86b Stream errors are printed with full traceback
The stream will now properly print the traceback of an error if the
debuging variable is set.
2019-06-03 17:42:26 -07:00
Alexander Hirsch
0ea4969393 Print the traceback of TCP client Exception
The function now prints the system execution information upon an
exception.
The last commit requested traceback information, but did not print the
information to the console.
2019-06-03 17:23:26 -07:00
Alexander Hirsch
312c77629b Provides debuging option for TCP stream
The TCP BaseClient class has been modified to recognize an environment
variable 'PYMODES_DEBUG'. When the variable is set to 'true', the
stream will halt execution rather than restarting. This is for
debugging purposes and does not alter the main functionality of
the program.
2019-06-03 16:08:28 -07:00
Junzi Sun
6159691c3d update CRC code 2019-05-27 21:26:18 +02:00
Junzi Sun
7edbf3fd30 Merge pull request #29 from alcibiade/fastcrc
FastCRC - A more efficient CRC calculation
2019-05-27 20:38:27 +02:00
Junzi Sun
e19d0cd945 Merge pull request #39 from amhirsch/DF-17Additions
Additional Information Extracted from TC-19/BDS0,9 Messages
2019-05-27 20:26:23 +02:00
Alexander Hirsch
6fc68841ce Updted docstrings for velocity messages 2019-05-27 08:25:11 -07:00
Alexander Hirsch
f27fe6c8aa Changed velocity source names 2019-05-27 08:13:02 -07:00
Junzi Sun
81a46e5070 add the BaseClient example 2019-05-27 10:59:32 +02:00
Junzi Sun
6c2adbe990 update citation info 2019-05-27 10:06:43 +02:00
Alexander Hirsch
3c14579040 velocity default argument for backwards compatibility 2019-05-17 09:45:16 -07:00
Alexander Hirsch
fc9b05b6f1 Added direction source to ground_velocity() 2019-05-17 06:06:06 -07:00
Alexander Hirsch
b813e41343 Fixed ground velocity return and updated velocity docstring 2019-05-17 05:59:14 -07:00
Alexander Hirsch
c9159feb7d BDS09 returns direction and v/s source 2019-05-16 19:05:19 -07:00
Junzi Sun
8cd5655a04 update api doc 2019-04-16 17:53:14 +02:00
Junzi Sun
8ded3500d4 version 2.1 2019-04-16 16:56:49 +02:00
Junzi Sun
c348a2295d add MHR to comm-b wapper 2019-04-16 16:45:52 +02:00
Junzi Sun
040a1879c9 update temp44() to produce two possible results 2019-04-16 16:44:50 +02:00
Junzi Sun
61b55531e8 update BDS 44 and 45, and MRAR switch in infer(). 2019-04-16 11:57:57 +02:00
Junzi Sun
abdafd7dea rework BDS 44 and 45 2019-04-12 17:18:03 +02:00
Junzi Sun
b97299ce1b update readme 2019-04-03 09:41:22 +02:00
Junzi Sun
652ef65bbb update api doc 2019-04-03 09:40:27 +02:00
Junzi Sun
72fe72c16c pypi release v2.0 2019-02-21 14:52:15 +01:00
Junzi Sun
31df5c2042 fix dump path 2019-02-21 14:39:57 +01:00
Junzi Sun
10e9b234ad sort dump output by time 2019-02-21 14:33:15 +01:00
Junzi Sun
546d69c129 bug fix 2019-02-16 01:10:12 +01:00
Junzi Sun
c4c285266c update readme for new dump option 2019-02-08 16:43:02 +01:00
Junzi Sun
ebc11e5e84 enable saving decode data to csv files 2019-02-08 16:41:24 +01:00
Junzi Sun
f5c2b36209 Merge pull request #35 from Ka-zam/skysense
Enable skysense raw type in the streamer.
2019-01-23 11:49:04 +01:00
Magnus Lundmark
759380b5a9 Updated README 2019-01-22 09:52:01 +01:00
Magnus Lundmark
3f6389a67d Clarifications of Skysense format 2019-01-22 09:44:58 +01:00
Magnus Lundmark
c252647e77 Bugfixes 2019-01-21 16:16:41 +01:00
Magnus Lundmark
1795d2f3c0 updates to handler 2019-01-18 17:44:19 +01:00
Magnus Lundmark
fb4cef7085 Initial Skysense support 2019-01-18 16:09:04 +01:00
Junzi Sun
b24c1101b6 add DF 0 and 16 to altcode 2018-12-14 17:22:55 +01:00
Junzi Sun
8a43d711aa add track rate from bds50 to streamer 2018-12-04 18:17:05 +01:00
Junzi Sun
495f320988 fix exceptions 2018-11-30 23:36:58 +01:00
Junzi Sun
44a9c8d2aa fix test for commb 2018-11-30 22:50:05 +01:00
Junzi Sun
ecfc4037a1 catch exceptions in uncertainty handeling 2018-11-30 22:47:00 +01:00
Junzi Sun
61f2191feb add BDS50 roll to streamer 2018-11-30 16:51:27 +01:00
Junzi Sun
9e1cc2c5a2 Merge pull request #32 from Akasch/fix_screen_offset
streamer.screen: fix missing flights in view
2018-11-27 17:40:56 +01:00
Junzi Sun
500c3ca9bd Merge pull request #33 from Akasch/fix_cpu_usage
modeslive: fix hight CPU usage
2018-11-27 17:16:57 +01:00
Nils Rokita
78f1ca77c7 modeslive: fix hight CPU usage
The core loop in modeslive is currently busy waiting for updates
I think an update rate at 50Hz is more reasonable and reduces cpu usage.
2018-11-27 15:59:37 +01:00
Nils Rokita
576713f13b streamer.screen: fix missing flights in view
the row is countin from 3 to the window hight, but the idx ist used
as index into the icaos list beginning at 0. the offset is 0 for the
first page. This results in the first 3 flights not shown.
2018-11-27 15:46:56 +01:00
Junzi Sun
3502fedf02 Merge pull request #31 from Akasch/fix_screen_clear
streamer.screen: fix undefined variable icao on emty flight list
2018-11-26 21:33:47 +01:00
Nils Rokita
de9ec43912 streamer.screen: fix undefined variable icao on emty flight list
The icao variable is tested for None on line 136 but is not defined if
no flight is known
2018-11-26 16:00:46 +01:00
alcibiade
bfdb8221a0 Remove remaining Python3 explicit typing.
The original version of this code is built against Python3.5+ and uses explicit typing. As I think the pyModeS supports Python2, I thought it was preferable top remove these types.
2018-11-05 21:55:45 +01:00
Yannick Kirschhoffer
32e6ee3904 Implement an alternative faster CRC. 2018-11-04 00:36:33 +01:00
Yannick Kirschhoffer
d82dfd5537 Extended test to stress the CRC function. 2018-11-04 00:17:09 +01:00
Junzi Sun
16c83d1505 fix bug in NACv 2018-10-02 23:32:07 +02:00
Junzi Sun
e5d5633535 relax bds17 identification 2018-08-24 16:45:12 +02:00
Junzi Sun
8adacd8e91 Merge pull request #27 from hv92/master
Update readme
2018-07-25 22:38:12 +02:00
Huy Vu
fd15b13c17 Update readme 2018-07-25 20:09:28 +02:00
Junzi Sun
405d8ed108 update screenshot 2018-07-04 21:35:48 +02:00
Junzi Sun
0085d03d4a add callsign to modeslive 2018-07-04 21:31:09 +02:00
Junzi Sun
6f139d4ae9 make modeslive uncertanty values optional 2018-07-04 21:20:23 +02:00
Junzi Sun
e5ca76ac0d rename live script to modeslive 2018-07-04 20:56:11 +02:00
Junzi Sun
ef9d2cfd16 fix NICv2 bug 2018-07-04 18:14:13 +02:00
Junzi Sun
ea1ccc0c70 add instructions for getting a raw stream from RTL-SDR 2018-06-29 14:13:20 +02:00
Junzi Sun
648f4660b7 Merge branch 'JoseAndresMR-master' 2018-06-27 22:09:05 +02:00
Junzi Sun
0df6a664a3 resturctue the uncertainty module, and some additional fixings. 2018-06-27 22:08:13 +02:00
JoseAndresMR
44b277f0ad various fixing 2018-06-26 23:15:57 +02:00
JoseAndresMR
715d0a3c66 implementation of uncertainty values from csv files 2018-06-26 16:22:42 +02:00
JoseAndresMR
6db5ea8023 "fetch from upstream" 2018-06-25 16:25:17 +02:00
JoseAndresMR
7685f1590f for first merge with upstream 2018-06-25 16:19:07 +02:00
Junzi Sun
5fa090b95f update screenshot 2018-06-23 16:05:06 +02:00
Junzi Sun
6c5ae2141b add example screenshot of pmslive 2018-06-23 15:59:26 +02:00
Junzi Sun
24f3658673 skeleton code for DF 0/4/5/11/16 2018-06-23 15:54:17 +02:00
Junzi Sun
205725872a update pmslive and tcpclient, add support for AVR. 2018-06-23 15:19:29 +02:00
Junzi Sun
01a573a1af update live screen 2018-06-23 14:40:27 +02:00
Junzi Sun
c0476f5e16 readme code format 2018-06-23 01:29:58 +02:00
Junzi Sun
140b68afbc add pmslive command reference 2018-06-23 01:28:41 +02:00
Junzi Sun
70b3af2c8b add script: pmslive 2018-06-23 01:24:15 +02:00
Junzi Sun
0ef64be934 update pmstream, add nic/nac/sil. 2018-06-23 00:20:38 +02:00
Junzi Sun
bbe6e50fb2 Merge branch 'JoseAndresMR-master', add ADS-B uncertainties 2018-06-22 22:52:36 +02:00
Junzi Sun
a3e44b5626 fixing code 2018-06-22 22:52:11 +02:00
JoseAndresMR
457a948879 Uncertainty and Accuracy parameters 2018-06-22 12:31:19 +02:00
Junzi Sun
54b2038a41 update readme, add id / alt code instruction 2018-06-21 17:38:59 +02:00
Junzi Sun
82b912bc05 update readme 2018-06-20 22:25:47 +02:00
Junzi Sun
6d5869a9e0 update readme 2018-06-20 22:22:42 +02:00
Junzi Sun
36840e0225 Merge branch 'master' into dev-2.0 2018-06-20 17:03:40 +02:00
Junzi Sun
972ffe264e Update README.rst 2018-06-20 16:29:36 +02:00
Junzi Sun
d4ca81e0ca Update README.rst 2018-06-20 16:18:39 +02:00
Junzi Sun
9fa475ab9a add more DF to icao function 2018-05-30 10:25:00 +02:00
Junzi Sun
c91bd4bb03 fix bug 2018-05-24 12:49:46 +02:00
Junzi Sun
4f0946c4da update readme 2018-05-17 10:49:40 +02:00
Junzi Sun
46a99852d6 update readme 2018-05-17 10:40:22 +02:00
Junzi Sun
2389c12b98 add [commb], depricate [ehs] and [els] 2018-05-17 10:34:14 +02:00
Junzi Sun
6128c5a18d update sample run script for comm-b 2018-05-17 09:52:50 +02:00
Junzi Sun
72c1a9f645 Merge pull request #21 from federicoorta/dev-2.0
Run samples updated in dev-2.0
2018-05-17 09:32:12 +02:00
Junzi Sun
f221c67295 keep only bds 40 50 60
Other bds code does not belong to Enhanced Mode-S definition.
2018-05-17 09:24:31 +02:00
forta
14a537030d Sample run of EHS decoding has been fixed. 2018-05-16 19:09:02 +02:00
forta
b7afd841ff Import of 'util' module substituted with 'common'. 2018-05-16 17:41:19 +02:00
Junzi Sun
11b85b6959 fix temp44 and bds imports 2018-05-02 14:33:27 +02:00
Junzi Sun
8a54f927f6 fix deprecation warning 2018-04-19 09:45:56 +02:00
Junzi Sun
9bfc116516 Merge branch 'dev-2.0' of github.com:junzis/pyModeS into dev-2.0 2018-04-17 15:08:35 +02:00
Junzi Sun
be38acabfd fix type error 2018-04-17 14:48:55 +02:00
Junzi Sun
0b6d5576d3 Merge pull request #18 from hv92/patch-1
Add additional BDS 4,0 check
2018-04-13 16:39:35 +02:00
Huy Vû
dfdddb77f2 Update bds40.py 2018-04-13 16:17:31 +02:00
Huy Vû
a872cd253e Update bds40.py 2018-04-13 16:14:31 +02:00
Junzi Sun
edbfdc68de fix bug 2018-04-05 15:32:17 +02:00
Junzi Sun
f246c88dd6 add try except 2018-04-04 16:41:34 +02:00
Junzi Sun
e821b8fa77 update roll check 2018-04-04 10:41:55 +02:00
Junzi Sun
1b3dcef659 add oe_flag() back 2018-04-04 09:46:21 +02:00
Junzi Sun
7653b2459d minor update 2018-04-04 09:46:05 +02:00
Junzi Sun
362b92de7a major update; adsb restructure; rename combining util/modes to common. 2018-03-28 13:31:24 +02:00
Junzi Sun
ef8f4b3b7f Merge branch 'dev-2.0' of github.com:junzis/pyModeS into dev-2.0 2018-03-27 18:24:33 +02:00
Junzi Sun
773efff9bc added deprecation waring 2018-03-27 18:24:22 +02:00
Junzi Sun
05e34e7516 Merge pull request #17 from hv92/dev-2.0
Minor change BDS 5,0 roll angle detection
2018-03-27 13:55:19 +02:00
Huy Vu
161ea31ee1 Minor change BDS 5,0 roll, if status and sign bit are both 1, roll angle was -90 degrees. 2018-03-27 13:04:52 +02:00
Junzi Sun
12506e7c7f restructure, add BDS30 2018-03-26 21:05:54 +02:00
Junzi Sun
fcd98d6bcc Merge pull request #15 from hv92/patch-2
too hight rounding track/heading angles, change to 3 decimal points.
2018-03-16 11:57:46 +01:00
Junzi Sun
ef40acdbfd Update ehs.py
let's make them to  3 decimals even
2018-03-16 11:56:49 +01:00
Junzi Sun
8b48fabf5a add ehs BDS10 decoder, cut redundancies 2018-03-16 11:53:09 +01:00
Huy Vû
d91ad261cd Too rough rounding track/heading angles
Due to resolution of 0.175 degrees, rounding difference up to 0.05 degrees can occur.
2018-03-16 11:22:10 +01:00
Junzi Sun
4911e69171 update EHS BDS identification, add isBDS50or60() function 2018-03-13 16:55:50 +01:00
Junzi Sun
5697e5b88e Merge pull request #14 from hv92/patch-1
Probabilistic method for BDS detection
2018-03-13 12:08:32 +01:00
Junzi Sun
3073187d24 Update ehs.py 2018-03-13 12:08:10 +01:00
Huy Vû
441cd27761 Probabilistic method for BDS detection
Including probabilistic method to determine BDS 5,0 and BDS 6,0 (by using a multivariate normal distribution with reference data)
2018-03-13 11:31:34 +01:00
Junzi Sun
c4406ba276 update pip install 2018-03-12 13:45:48 +01:00
Junzi Sun
de6238f5e9 improve isBDS60 identification 2018-03-09 13:58:10 +01:00
Junzi Sun
711fd889e6 Merge branch 'master' into dev-2.0 2018-03-09 13:19:33 +01:00
Junzi Sun
d6a04865dc merge from master with BDS60 vertical rate bug fix 2018-03-08 16:41:20 +01:00
Junzi Sun
6baa218596 update crc 2018-01-16 15:37:04 +01:00
Junzi Sun
8a9045e730 arguments for pmstream 2017-12-20 17:16:31 +01:00
Junzi Sun
d13e1bd12e initial commit for v2.0 2017-12-20 17:00:02 +01:00
96 changed files with 5378 additions and 2863 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
.pytest_cache/
# C extensions
*.so

View File

@@ -1,56 +1,115 @@
The Python Mode-S Decoder
=========================
The Python ADS-B/Mode-S Decoder
===============================
Python library for Mode-S message decoding. Support Downlink Formats (DF) are:
If you find this project useful for your research, please cite our work (bibtex format):
- Automatic Dependent Surveillance - Broadcast (ADS-B) (DF17)
::
- aircraft information that contains: ICAO address, position, altitude, velocity (ground speed), callsign, etc.
- Mode-S Elementary Surveillance (ELS) (DF4 and DF5).
- DF4: Altitude
- DF5: Squawk code
- Mode-S Enhanced Surveillance (EHS) (DF20 and DF21). Additional information in response to SSR interrogation, such as: true airspeed, indicated airspeed, mach number, wind, temperature, etc.
- DF20: Altitude
- DF21: Squawk code
- BDS 2,0 Aircraft identification
- BDS 2,1 Aircraft and airline registration markings
- BDS 4,0 Selected vertical intention
- BDS 4,4 Meteorological routine air report
- BDS 5,0 Track and turn report
- BDS 5,3 Air-referenced state vector
- BDS 6,0 Heading and speed report
Detailed manual on Mode-S decoding is published by the author, at:
http://adsb-decode-guide.readthedocs.io
@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.
Message with following Downlink Formats (DF) are supported:
**DF17 / DF18: Automatic Dependent Surveillance - Broadcast (ADS-B)**
- TC=1-4 / BDS 0,8: Aircraft identification and category
- TC=5-8 / BDS 0,6: Surface position
- TC=9-18 / BDS 0,5: Airborne position
- TC=19 / BDS 0,9: Airborne velocity
- TC=28 / BDS 6,1: Airborne status [to be implemented]
- TC=29 / BDS 6,2: Target state and status information [to be implemented]
- TC=31 / BDS 6,5: Aircraft operational status [to be implemented]
**DF20 / DF21: Mode-S Comm-B replies**
- BDS 1,0: Data link capability report
- BDS 1,7: Common usage GICB capability report
- BDS 2,0: Aircraft identification
- BDS 3,0: ACAS active resolution advisory
- BDS 4,0: Selected vertical intention
- BDS 4,4: Meteorological routine air report (experimental)
- BDS 4,5: Meteorological hazard report (experimental)
- BDS 5,0: Track and turn report
- BDS 6,0: Heading and speed report
**DF4 / DF20: Altitude code**
**DF5 / DF21: Identity code (squawk code)**
Resources
-----------
Checkout and contribute to this open source project at:
Checkout and contribute to this open-source project at:
https://github.com/junzis/pyModeS
API documentation at:
Detailed manual on Mode-S decoding is published at:
https://mode-s.org/decode.
API documentation of pyModeS is at:
http://pymodes.readthedocs.io
Install
-------
The easiest installation is to use pip:
To install latest version from the GitHub:
::
pip install git+https://github.com/junzis/pyModeS
To install the stable version (2.0) from pip:
::
pip install pyModeS
To install latest devlopment version from the GitHub:
Live view traffic (modeslive)
----------------------------------------------------
Supports **Mode-S Beast** and **AVR** raw stream
::
pip install git+https://github.com/junzis/pyModeS
modeslive --server [server_address] --port [tcp_port] --rawtype [beast,avr,skysense] --latlon [lat] [lon] --dumpto [folder]
Arguments:
-h, --help show this help message and exit
--server SERVER server address or IP
--port PORT raw data port
--rawtype RAWTYPE beast, avr or skysense
--latlon LAT LON receiver position
--show-uncertainty display uncertaint values, default off
--dumpto folder to dump decoded output
If you have a RTL-SDR receiver or Mode-S Beast, use modesmixer2 (http://xdeco.org/?page_id=48) to create raw beast TCP stream:
::
$ modesmixer2 --inSeriel port[:speed[:flow_control]] --outServer beast:[tcp_port]
Example screenshot:
.. image:: https://github.com/junzis/pyModeS/raw/master/doc/modeslive-screenshot.png
:width: 700px
Use the library
@@ -61,33 +120,33 @@ Use the library
import pyModeS as pms
Common functions:
Common functions
*****************
.. code:: python
pms.df(msg) # Downlink Format
pms.icao(msg) # Infer the ICAO address from the message
pms.crc(msg, encode=False) # Perform CRC or generate parity bit
pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer
pms.gray2int(str) # Convert grey code to interger
pms.hex2bin(str) # Convert hexadecimal string to binary string
pms.bin2int(str) # Convert binary string to integer
pms.hex2int(str) # Convert hexadecimal string to integer
pms.gray2int(str) # Convert grey code to interger
Core functions for ADS-B decoding:
**********************************
Core functions for ADS-B decoding
*********************************
.. code:: python
pms.adsb.icao(msg)
pms.adsb.typecode(msg)
# typecode 1-4
# Typecode 1-4
pms.adsb.callsign(msg)
# typecode 5-8 (surface) and 9-18 (airborne)
# Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 9-18 (airborne, GNSS height)
pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None)
pms.adsb.airborne_position(msg_even, msg_odd, t_even, t_odd)
pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref)
@@ -98,9 +157,9 @@ Core functions for ADS-B decoding:
pms.adsb.altitude(msg)
# typecode: 19
pms.adsb.velocity(msg) # handles both surface & airborne messages
pms.adsb.speed_heading(msg) # handles both surface & airborne messages
# Typecode: 19
pms.adsb.velocity(msg) # Handles both surface & airborne messages
pms.adsb.speed_heading(msg) # Handles both surface & airborne messages
pms.adsb.surface_velocity(msg)
pms.adsb.airborne_velocity(msg)
@@ -111,70 +170,153 @@ use `position_with_ref()` method to decode with only one position message
messages. But the reference position shall be with in 180NM (airborne)
or 45NM (surface) of the true position.
Core functions for ELS decoding:
********************************
Decode altitude replies in DF4 / DF20
**************************************
.. code:: python
pms.common.altcode(msg) # Downlink format must be 4 or 20
Decode identity replies in DF5 / DF21
**************************************
.. code:: python
pms.common.idcode(msg) # Downlink format must be 5 or 21
Common Mode-S functions
************************
.. code:: python
pms.els.icao(msg) # ICAO address
pms.els.df4alt(msg) # Altitude from any DF4 message
pms.ehs.df5id(msg) # Squawk code from any DF5 message
pms.icao(msg) # Infer the ICAO address from the message
pms.bds.infer(msg) # Infer the Modes-S BDS register
# Check if BDS is 5,0 or 6,0, give reference speed, track, altitude (from ADS-B)
pms.bds.is50or60(msg, spd_ref, trk_ref, alt_ref)
# Check each BDS explicitly
pms.bds.bds10.is10(msg)
pms.bds.bds17.is17(msg)
pms.bds.bds20.is20(msg)
pms.bds.bds30.is30(msg)
pms.bds.bds40.is40(msg)
pms.bds.bds44.is44(msg)
pms.bds.bds50.is50(msg)
pms.bds.bds60.is60(msg)
Core functions for EHS decoding:
********************************
Mode-S Elementary Surveillance (ELS)
*************************************
.. code:: python
pms.ehs.icao(msg) # ICAO address
pms.ehs.df20alt(msg) # Altitude from any DF20 message
pms.ehs.df21id(msg) # Squawk code from any DF21 message
pms.commb.ovc10(msg) # Overlay capability, BDS 1,0
pms.commb.cap17(msg) # GICB capability, BDS 1,7
pms.commb.cs20(msg) # Callsign, BDS 2,0
pms.ehs.BDS(msg) # Comm-B Data Selector Version
# for BDS version 2,0
pms.ehs.isBDS20(msg) # Check if message is BDS 2,0
pms.ehs.callsign(msg) # Aircraft callsign
Mode-S Enhanced Surveillance (EHS)
***********************************
# for BDS version 4,0
pms.ehs.isBDS40(msg) # Check if message is BDS 4,0
pms.ehs.alt40mcp(msg) # MCP/FCU selected altitude (ft)
pms.ehs.alt40fms(msg) # FMS selected altitude (ft)
pms.ehs.p40baro(msg) # Barometric pressure (mb)
.. code:: python
# for BDS version 4,4
pms.ehs.isBDS44(msg, rev=False) # Check if message is BDS 4,4
pms.ehs.wind44(msg, rev=False) # wind speed (kt) and heading (deg)
pms.ehs.temp44(msg, rev=False) # temperature (C)
pms.ehs.p44(msg, rev=False) # pressure (hPa)
pms.ehs.hum44(msg, rev=False) # humidity (%)
# BDS 4,0
pms.commb.selalt40mcp(msg) # MCP/FCU selected altitude (ft)
pms.commb.selalt40fms(msg) # FMS selected altitude (ft)
pms.commb.p40baro(msg) # Barometric pressure (mb)
# for BDS version 5,0
pms.ehs.isBDS50(msg) # Check if message is BDS 5,0
pms.ehs.roll50(msg) # roll angle (deg)
pms.ehs.trk50(msg) # track angle (deg)
pms.ehs.gs50(msg) # ground speed (kt)
pms.ehs.rtrk50(msg) # track angle rate (deg/sec)
pms.ehs.tas50(msg) # true airspeed (kt)
# BDS 5,0
pms.commb.roll50(msg) # Roll angle (deg)
pms.commb.trk50(msg) # True track angle (deg)
pms.commb.gs50(msg) # Ground speed (kt)
pms.commb.rtrk50(msg) # Track angle rate (deg/sec)
pms.commb.tas50(msg) # True airspeed (kt)
# for BDS version 5,3
pms.ehs.isBDS53(msg) # Check if message is BDS 5,3
pms.ehs.hdg53(msg) # magnetic heading (deg)
pms.ehs.ias53(msg) # indicated airspeed (kt)
pms.ehs.mach53(msg) # MACH number
pms.ehs.tas53(msg) # true airspeed (kt)
pms.ehs.vr53(msg) # vertical rate (fpm)
# 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)
# for BDS version 6,0
pms.ehs.isBDS60(msg) # Check if message is BDS 6,0
pms.ehs.hdg60(msg) # heading (deg)
pms.ehs.ias60(msg) # indicated airspeed (kt)
pms.ehs.mach60(msg) # MACH number
pms.ehs.vr60baro(msg) # barometric altitude rate (ft/min)
pms.ehs.vr60ins(msg) # inertial vertical speed (ft/min)
Developement
------------
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 your like. You need to re-implement the ``handle_messages()`` function from
the ``BaseClient`` class to write your own logic to handle the messages.
Here is an example:
.. code:: python
from pyModeS.extra.tcpclient import BaseClient
# define your custom class by extending the BaseClient
# - implement your handle_messages() methods
class ADSBClient(BaseClient):
def __init__(self, host, port, rawtype):
super(ModesClient, 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 '1' in pms.crc(msg): # 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=30334, rawtype='beast')
client.run()
Unit test
---------
To perform unit tests. First install ``tox`` through pip, Then, run the following commands:
.. code:: bash

3
doc/.gitignore vendored
View File

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

View File

@@ -1,225 +1,21 @@
# Makefile for Sphinx documentation
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
SOURCEDIR = source
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
# Put it first so that "make" without argument is like "make help".
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: help Makefile
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyModeS.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyModeS.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyModeS"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyModeS"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
rm -f source/pyModeS*.rst source/modules.rst
sphinx-apidoc -f -e -M -o source/ ../pyModeS
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

7
doc/README.rst Normal file
View File

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

View File

@@ -1,337 +0,0 @@
# -*- coding: utf-8 -*-
#
# pyModeS documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 16 15:47:05 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinx.ext.coverage',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'pyModeS'
copyright = u'2016, Junzi Sun'
author = u'Junzi Sun'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'1.0.5'
# The full version, including alpha/beta/rc tags.
release = u'1.0.5'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'pyModeS v1.0.5'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyModeSdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pyModeS.tex', u'pyModeS Documentation',
u'Junzi Sun', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pymodes', u'pyModeS Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pyModeS', u'pyModeS Documentation',
author, 'pyModeS', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

View File

@@ -1,40 +0,0 @@
.. pyModeS documentation master file, created by
sphinx-quickstart on Tue Aug 16 15:47:05 2016.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
pyModeS APIs
=====================
This document contains all the functions within pyModeS package.
Source code and user guide: https://github.com/junzis/pyModeS
pyModeS.adsb module
-------------------
.. automodule:: pyModeS.adsb
:members:
:undoc-members:
:show-inheritance:
pyModeS.ehs module
------------------
.. automodule:: pyModeS.ehs
:members:
:undoc-members:
:show-inheritance:
pyModeS.util module
-------------------
.. automodule:: pyModeS.util
:members:
:undoc-members:
:show-inheritance:

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
pyModeS.decoder.bds package
===========================
.. automodule:: pyModeS.decoder.bds
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
pyModeS.decoder.bds.bds05
pyModeS.decoder.bds.bds06
pyModeS.decoder.bds.bds08
pyModeS.decoder.bds.bds09
pyModeS.decoder.bds.bds10
pyModeS.decoder.bds.bds17
pyModeS.decoder.bds.bds20
pyModeS.decoder.bds.bds30
pyModeS.decoder.bds.bds40
pyModeS.decoder.bds.bds44
pyModeS.decoder.bds.bds45
pyModeS.decoder.bds.bds50
pyModeS.decoder.bds.bds53
pyModeS.decoder.bds.bds60

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,17 @@
pyModeS package
===============
.. automodule:: pyModeS
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
pyModeS.decoder
pyModeS.extra
pyModeS.streamer

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,15 @@
from __future__ import absolute_import, print_function, division
from .util import *
from . import adsb
from . import ehs
from . import els
from .decoder.common import *
from .decoder import adsb
from .decoder import commb
from .decoder import common
from .decoder import bds
from .extra import aero
from .extra import tcpclient
# from .decoder import els # depricated
# from .decoder import ehs # depricated
import os
dirpath = os.path.dirname(os.path.realpath(__file__))

View File

@@ -1,696 +0,0 @@
# Copyright (C) 2015 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
A python package for decoding ABS-D messages.
"""
from __future__ import absolute_import, print_function, division
import math
from . import util
def df(msg):
"""Get the downlink format (DF) number
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: DF number
"""
return util.df(msg)
def icao(msg):
"""Get the ICAO 24 bits address, bytes 3 to 8.
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
return msg[2:8]
def data(msg):
"""Return the data frame in the message, bytes 9 to 22"""
return msg[8:22]
def typecode(msg):
"""Type code of ADS-B message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: type code number
"""
msgbin = util.hex2bin(msg)
return util.bin2int(msgbin[32:37])
# ---------------------------------------------
# Aircraft Identification
# ---------------------------------------------
def category(msg):
"""Aircraft category number
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: category number
"""
if typecode(msg) < 1 or typecode(msg) > 4:
raise RuntimeError("%s: Not a identification message" % msg)
msgbin = util.hex2bin(msg)
return util.bin2int(msgbin[5:8])
def callsign(msg):
"""Aircraft callsign
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
string: callsign
"""
if typecode(msg) < 1 or typecode(msg) > 4:
raise RuntimeError("%s: Not a identification message" % msg)
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
msgbin = util.hex2bin(msg)
csbin = msgbin[40:96]
cs = ''
cs += chars[util.bin2int(csbin[0:6])]
cs += chars[util.bin2int(csbin[6:12])]
cs += chars[util.bin2int(csbin[12:18])]
cs += chars[util.bin2int(csbin[18:24])]
cs += chars[util.bin2int(csbin[24:30])]
cs += chars[util.bin2int(csbin[30:36])]
cs += chars[util.bin2int(csbin[36:42])]
cs += chars[util.bin2int(csbin[42:48])]
# clean string, remove spaces and marks, if any.
# cs = cs.replace('_', '')
cs = cs.replace('#', '')
return cs
# ---------------------------------------------
# Positions
# ---------------------------------------------
def oe_flag(msg):
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: 0 or 1, for even or odd frame
"""
if typecode(msg) < 5 or typecode(msg) > 18:
raise RuntimeError("%s: Not a position message" % msg)
msgbin = util.hex2bin(msg)
return int(msgbin[53])
def cprlat(msg):
"""CPR encoded latitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: encoded latitude
"""
if typecode(msg) < 5 or typecode(msg) > 18:
raise RuntimeError("%s: Not a position message" % msg)
msgbin = util.hex2bin(msg)
return util.bin2int(msgbin[54:71])
def cprlon(msg):
"""CPR encoded longitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: encoded longitude
"""
if typecode(msg) < 5 or typecode(msg) > 18:
raise RuntimeError("%s: Not a position message" % msg)
msgbin = util.hex2bin(msg)
return util.bin2int(msgbin[71:88])
def position(msg0, msg1, t0, t1, lat_ref=None, lon_ref=None):
"""Decode position from a pair of even and odd position message
(works with both airborne and surface position messages)
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
if (5 <= typecode(msg0) <= 8 and 5 <= typecode(msg1) <= 8):
if (not lat_ref) or (not lon_ref):
raise RuntimeError("Surface position encountered, a reference \
position lat/lon required. Location of \
receiver can be used.")
else:
return surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref)
elif (9 <= typecode(msg0) <= 18 and 9 <= typecode(msg1) <= 18):
return airborne_position(msg0, msg1, t0, t1)
else:
raise RuntimeError("incorrect or inconsistant message types")
def airborne_position(msg0, msg1, t0, t1):
"""Decode airborn position from a pair of even and odd position message
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
msgbin0 = util.hex2bin(msg0)
msgbin1 = util.hex2bin(msg1)
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0
cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0
cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0
cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0
air_d_lat_even = 360.0 / 60
air_d_lat_odd = 360.0 / 59
# compute latitude index 'j'
j = util.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
if lat_even >= 270:
lat_even = lat_even - 360
if lat_odd >= 270:
lat_odd = lat_odd - 360
# check if both are in the same latidude zone, exit if not
if _cprNL(lat_even) != _cprNL(lat_odd):
return None
# compute ni, longitude index m, and longitude
if (t0 > t1):
lat = lat_even
nl = _cprNL(lat)
ni = max(_cprNL(lat)- 0, 1)
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = _cprNL(lat)
ni = max(_cprNL(lat) - 1, 1)
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_odd)
if lon > 180:
lon = lon - 360
return round(lat, 5), round(lon, 5)
def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message,
knowing reference nearby location, such as previously
calculated location, ground station, or airport location, etc.
Works with both airborne and surface position messages.
The reference position shall be with in 180NM (airborne) or 45NM (surface)
of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
if 5 <= typecode(msg) <= 8:
return surface_position_with_ref(msg, lat_ref, lon_ref)
elif 9 <= typecode(msg) <= 18:
return airborne_position_with_ref(msg, lat_ref, lon_ref)
else:
raise RuntimeError("incorrect or inconsistant message types")
def airborne_position_with_ref(msg, lat_ref, lon_ref):
"""Decode airborne position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be with in 180NM of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
i = oe_flag(msg)
d_lat = 360.0/59 if i else 360.0/60
msgbin = util.hex2bin(msg)
cprlat = util.bin2int(msgbin[54:71]) / 131072.0
cprlon = util.bin2int(msgbin[71:88]) / 131072.0
j = util.floor(lat_ref / d_lat) \
+ util.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
lat = d_lat * (j + cprlat)
ni = _cprNL(lat) - i
if ni > 0:
d_lon = 360.0 / ni
else:
d_lon = 360.0
m = util.floor(lon_ref / d_lon) \
+ util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5)
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
"""Decode surface position from a pair of even and odd position message,
the lat/lon of receiver must be provided to yield the correct solution.
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
lat_ref (float): latitude of the receiver
lon_ref (float): longitude of the receiver
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
msgbin0 = util.hex2bin(msg0)
msgbin1 = util.hex2bin(msg1)
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = util.bin2int(msgbin0[54:71]) / 131072.0
cprlon_even = util.bin2int(msgbin0[71:88]) / 131072.0
cprlat_odd = util.bin2int(msgbin1[54:71]) / 131072.0
cprlon_odd = util.bin2int(msgbin1[71:88]) / 131072.0
air_d_lat_even = 90.0 / 60
air_d_lat_odd = 90.0 / 59
# compute latitude index 'j'
j = util.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
# solution for north hemisphere
lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even))
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
# solution for north hemisphere
lat_even_s = lat_even_n - 90.0
lat_odd_s = lat_odd_n - 90.0
# chose which solution corrispondes to receiver location
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s
# check if both are in the same latidude zone, rare but possible
if _cprNL(lat_even) != _cprNL(lat_odd):
return None
# compute ni, longitude index m, and longitude
if (t0 > t1):
lat = lat_even
nl = _cprNL(lat_even)
ni = max(_cprNL(lat_even) - 0, 1)
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = _cprNL(lat_odd)
ni = max(_cprNL(lat_odd) - 1, 1)
m = util.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_odd)
# four possible longitude solutions
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
# the closest solution to receiver is the correct one
dls = [abs(lon_ref - l) for l in lons]
imin = min(range(4), key=dls.__getitem__)
lon = lons[imin]
return round(lat, 5), round(lon, 5)
def surface_position_with_ref(msg, lat_ref, lon_ref):
"""Decode surface position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be with in 45NM of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
i = oe_flag(msg)
d_lat = 90.0/59 if i else 90.0/60
msgbin = util.hex2bin(msg)
cprlat = util.bin2int(msgbin[54:71]) / 131072.0
cprlon = util.bin2int(msgbin[71:88]) / 131072.0
j = util.floor(lat_ref / d_lat) \
+ util.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
lat = d_lat * (j + cprlat)
ni = _cprNL(lat) - i
if ni > 0:
d_lon = 90.0 / ni
else:
d_lon = 90.0
m = util.floor(lon_ref / d_lon) \
+ util.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5)
def _cprNL(lat):
"""NL() function in CPR decoding
"""
if lat == 0:
return 59
if lat == 87 or lat == -87:
return 2
if lat > 87 or lat < -87:
return 1
nz = 15
a = 1 - math.cos(math.pi / (2 * nz))
b = math.cos(math.pi / 180.0 * abs(lat)) ** 2
nl = 2 * math.pi / (math.acos(1 - a/b))
NL = util.floor(nl)
return NL
def altitude(msg):
"""Decode aircraft altitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: altitude in feet
"""
if typecode(msg) < 5 or typecode(msg) > 18:
raise RuntimeError("%s: Not a position message" % msg)
if typecode(msg) >=5 and typecode(msg) <=8:
# surface position, altitude 0
return 0
msgbin = util.hex2bin(msg)
q = msgbin[47]
if q:
n = util.bin2int(msgbin[40:47]+msgbin[48:52])
alt = n * 25 - 1000
return alt
else:
return None
def nic(msg):
"""Calculate NIC, navigation integrity category
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: NIC number (from 0 to 11), -1 if not applicable
"""
if typecode(msg) < 9 or typecode(msg) > 18:
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
msgbin = util.hex2bin(msg)
tc = typecode(msg)
nic_sup_b = util.bin2int(msgbin[39])
if tc in [0, 18, 22]:
nic = 0
elif tc == 17:
nic = 1
elif tc == 16:
if nic_sup_b:
nic = 3
else:
nic = 2
elif tc == 15:
nic = 4
elif tc == 14:
nic = 5
elif tc == 13:
nic = 6
elif tc == 12:
nic = 7
elif tc == 11:
if nic_sup_b:
nic = 9
else:
nic = 8
elif tc in [10, 21]:
nic = 10
elif tc in [9, 20]:
nic = 11
else:
nic = -1
return nic
# ---------------------------------------------
# Velocity
# ---------------------------------------------
def velocity(msg):
"""Calculate the speed, heading, and vertical rate
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): speed (kt), ground track or heading (degree),
rate of climb/descend (ft/min), and speed type
('GS' for ground speed, 'AS' for airspeed)
"""
if 5 <= typecode(msg) <= 8:
return surface_velocity(msg)
elif typecode(msg) == 19:
return airborne_velocity(msg)
else:
raise RuntimeError("incorrect or inconsistant message types, expecting 4<TC<9 or TC=19")
def speed_heading(msg):
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), ground track or heading (degree)
"""
spd, trk_or_hdg, rocd, tag = velocity(msg)
return spd, trk_or_hdg
def airborne_velocity(msg):
"""Calculate the speed, track (or heading), and vertical rate
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): speed (kt), ground track or heading (degree),
rate of climb/descend (ft/min), and speed type
('GS' for ground speed, 'AS' for airspeed)
"""
if typecode(msg) != 19:
raise RuntimeError("%s: Not a airborne velocity message, expecting TC=19" % msg)
msgbin = util.hex2bin(msg)
subtype = util.bin2int(msgbin[37:40])
if util.bin2int(msgbin[46:56]) == 0 or util.bin2int(msgbin[57:67]) == 0:
return None
if subtype in (1, 2):
v_ew_sign = -1 if int(msgbin[45]) else 1
v_ew = util.bin2int(msgbin[46:56]) - 1 # east-west velocity
v_ns_sign = -1 if int(msgbin[56]) else 1
v_ns = util.bin2int(msgbin[57:67]) - 1 # north-south velocity
v_we = v_ew_sign * v_ew
v_sn = v_ns_sign * v_ns
spd = math.sqrt(v_sn*v_sn + v_we*v_we) # unit in kts
trk = math.atan2(v_we, v_sn)
trk = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val
tag = 'GS'
trk_or_hdg = trk
else:
hdg = util.bin2int(msgbin[46:56]) / 1024.0 * 360.0
spd = util.bin2int(msgbin[57:67])
tag = 'AS'
trk_or_hdg = hdg
vr_sign = -1 if int(msgbin[68]) else 1
vr = (util.bin2int(msgbin[69:78]) - 1) * 64 # vertical rate, fpm
rocd = vr_sign * vr
return int(spd), round(trk_or_hdg, 1), int(rocd), tag
def surface_velocity(msg):
"""Decode surface velocity from from a surface position message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float, int, string): speed (kt), ground track (degree),
rate of climb/descend (ft/min), and speed type
('GS' for ground speed, 'AS' for airspeed)
"""
if typecode(msg) < 5 or typecode(msg) > 8:
raise RuntimeError("%s: Not a surface message, expecting 5<TC<8" % msg)
msgbin = util.hex2bin(msg)
# ground track
trk_status = int(msgbin[44])
if trk_status == 1:
trk = util.bin2int(msgbin[45:52]) * 360.0 / 128.0
trk = round(trk, 1)
else:
trk = None
# ground movment / speed
mov = util.bin2int(msgbin[37:44])
if mov == 0 or mov > 124:
spd = None
elif mov == 1:
spd = 0
elif mov == 124:
spd = 175
else:
movs = [2, 9, 13, 39, 94, 109, 124]
kts = [0.125, 1, 2, 15, 70, 100, 175]
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
step = (kts[i] - kts[i-1]) * 1.0 / (movs[i]-movs[i-1])
spd = kts[i-1] + (mov-movs[i-1]) * step
spd = round(spd, 2)
return spd, trk, 0, 'GS'
def altitude_diff(msg):
"""Decode the differece between GNSS and barometric altitude
Args:
msg (string): 28 bytes hexadecimal message string, TC=19
Returns:
int: Altitude difference in ft. Negative value indicates GNSS altitude
below barometric altitude.
"""
if typecode(msg) != 19:
raise RuntimeError("incorrect message types, expecting TC=19")
msgbin = util.hex2bin(msg)
sign = -1 if int(msgbin[80]) else 1
value = util.bin2int(msgbin[81:88])
if value == 0 or value == 127:
return None
else:
return sign * (value - 1) * 25 # in ft.

View File

@@ -0,0 +1,3 @@
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import *

23
pyModeS/decoder/acas.py Normal file
View 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

523
pyModeS/decoder/adsb.py Normal file
View File

@@ -0,0 +1,523 @@
# 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 inconsistant message types")
def position_with_ref(msg, lat_ref, lon_ref):
"""Decode position with only one message,
knowing reference nearby location, such as previously
calculated location, ground station, or airport location, etc.
Works with both airborne and surface position messages.
The reference position shall be with in 180NM (airborne) or 45NM (surface)
of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
tc = typecode(msg)
if 5<=tc<=8:
return surface_position_with_ref(msg, lat_ref, lon_ref)
elif 9<=tc<=18 or 20<=tc<=22:
return airborne_position_with_ref(msg, lat_ref, lon_ref)
else:
raise RuntimeError("incorrect or inconsistant message types")
def altitude(msg):
"""Decode aircraft altitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: altitude in feet
"""
tc = typecode(msg)
if tc<5 or tc==19 or tc>22:
raise RuntimeError("%s: Not a position message" % msg)
if tc>=5 and tc<=8:
# surface position, altitude 0
return 0
msgbin = common.hex2bin(msg)
q = msgbin[47]
if q:
n = common.bin2int(msgbin[40:47]+msgbin[48:52])
alt = n * 25 - 1000
return alt
else:
return None
def velocity(msg, 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 refrence, '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 inconsistant message types, expecting 4<TC<9 or TC=19")
def speed_heading(msg):
"""Get speed and ground track (or heading) from the velocity message
(handles both airborne or surface message)
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, float): speed (kt), ground track or heading (degree)
"""
spd, trk_or_hdg, rocd, tag = velocity(msg)
return spd, trk_or_hdg
def oe_flag(msg):
"""Check the odd/even flag. Bit 54, 0 for even, 1 for odd.
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: 0 or 1, for even or odd frame
"""
msgbin = common.hex2bin(msg)
return int(msgbin[53])
def version(msg):
"""ADS-B Version
Args:
msg (string): 28 bytes hexadecimal message string, TC = 31
Returns:
int: version number
"""
tc = typecode(msg)
if tc != 31:
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg)
msgbin = common.hex2bin(msg)
version = common.bin2int(msgbin[72:75])
return version
def nuc_p(msg):
"""Calculate NUCp, Navigation Uncertainty Category - Position (ADS-B version 1)
Args:
msg (string): 28 bytes hexadecimal message string,
Returns:
int: Horizontal Protection Limit
int: 95% Containment Radius - Horizontal (meters)
int: 95% Containment Radius - Vertical (meters)
"""
tc = typecode(msg)
if typecode(msg) < 5 or typecode(msg) > 22:
raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \
or airborne position with GNSS height (20<TC<22)" % msg
)
try:
NUCp = uncertainty.TC_NUCp_lookup[tc]
HPL = uncertainty.NUCp[NUCp]['HPL']
RCu = uncertainty.NUCp[NUCp]['RCu']
RCv = uncertainty.NUCp[NUCp]['RCv']
except KeyError:
HPL, RCu, RCv = uncertainty.NA, uncertainty.NA, uncertainty.NA
if tc in [20, 21]:
RCv = uncertainty.NA
return HPL, RCu, RCv
def nuc_v(msg):
"""Calculate NUCv, Navigation Uncertainty Category - Velocity (ADS-B version 1)
Args:
msg (string): 28 bytes hexadecimal message string,
Returns:
int or string: 95% Horizontal Velocity Error
int or string: 95% Vertical Velocity Error
"""
tc = typecode(msg)
if tc != 19:
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg)
msgbin = common.hex2bin(msg)
NUCv = common.bin2int(msgbin[42:45])
try:
HVE = uncertainty.NUCv[NUCv]['HVE']
VVE = uncertainty.NUCv[NUCv]['VVE']
except KeyError:
HVE, VVE = uncertainty.NA, uncertainty.NA
return HVE, VVE
def nic_v1(msg, NICs):
"""Calculate NIC, navigation integrity category, for ADS-B version 1
Args:
msg (string): 28 bytes hexadecimal message string
NICs (int or string): NIC supplement
Returns:
int or string: Horizontal Radius of Containment
int or string: Vertical Protection Limit
"""
if typecode(msg) < 5 or typecode(msg) > 22:
raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \
or airborne position with GNSS height (20<TC<22)" % msg
)
tc = typecode(msg)
NIC = uncertainty.TC_NICv1_lookup[tc]
if isinstance(NIC, dict):
NIC = NIC[NICs]
try:
Rc = uncertainty.NICv1[NIC][NICs]['Rc']
VPL = uncertainty.NICv1[NIC][NICs]['VPL']
except KeyError:
Rc, VPL = uncertainty.NA, uncertainty.NA
return Rc, VPL
def nic_v2(msg, NICa, NICbc):
"""Calculate NIC, navigation integrity category, for ADS-B version 2
Args:
msg (string): 28 bytes hexadecimal message string
NICa (int or string): NIC supplement - A
NICbc (int or srting): NIC supplement - B or C
Returns:
int or string: Horizontal Radius of Containment
"""
if typecode(msg) < 5 or typecode(msg) > 22:
raise RuntimeError(
"%s: Not a surface position message (5<TC<8), \
airborne position message (8<TC<19), \
or airborne position with GNSS height (20<TC<22)" % msg
)
tc = typecode(msg)
NIC = uncertainty.TC_NICv2_lookup[tc]
if 20<=tc<=22:
NICs = 0
else:
NICs = NICa*2 + NICbc
try:
if isinstance(NIC, dict):
NIC = NIC[NICs]
Rc = uncertainty.NICv2[NIC][NICs]['Rc']
except KeyError:
Rc = uncertainty.NA
return Rc
def nic_s(msg):
"""Obtain NIC supplement bit, TC=31 message
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: NICs number (0 or 1)
"""
tc = typecode(msg)
if tc != 31:
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg)
msgbin = common.hex2bin(msg)
nic_s = int(msgbin[75])
return nic_s
def nic_a_c(msg):
"""Obtain NICa/c, navigation integrity category supplements a and c
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
(int, int): NICa and NICc number (0 or 1)
"""
tc = typecode(msg)
if tc != 31:
raise RuntimeError("%s: Not a status operation message, expecting TC = 31" % msg)
msgbin = common.hex2bin(msg)
nic_a = int(msgbin[75])
nic_c = int(msgbin[51])
return nic_a, nic_c
def nic_b(msg):
"""Obtain NICb, navigation integrity category supplement-b
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: NICb number (0 or 1)
"""
tc = typecode(msg)
if tc < 9 or tc > 18:
raise RuntimeError("%s: Not a airborne position message, expecting 8<TC<19" % msg)
msgbin = common.hex2bin(msg)
nic_b = int(msgbin[39])
return nic_b
def nac_p(msg):
"""Calculate NACp, Navigation Accuracy Category - Position
Args:
msg (string): 28 bytes hexadecimal message string, TC = 29 or 31
Returns:
int or string: 95% horizontal accuracy bounds, Estimated Position Uncertainty
int or string: 95% vertical accuracy bounds, Vertical Estimated Position Uncertainty
"""
tc = typecode(msg)
if tc not in [29, 31]:
raise RuntimeError("%s: Not a target state and status message, \
or operation status message, expecting TC = 29 or 31" % msg)
msgbin = common.hex2bin(msg)
if tc == 29:
NACp = common.bin2int(msgbin[71:75])
elif tc == 31:
NACp = common.bin2int(msgbin[76:80])
try:
EPU = uncertainty.NACp[NACp]['EPU']
VEPU = uncertainty.NACp[NACp]['VEPU']
except KeyError:
EPU, VEPU = uncertainty.NA, uncertainty.NA
return EPU, VEPU
def nac_v(msg):
"""Calculate NACv, Navigation Accuracy Category - Velocity
Args:
msg (string): 28 bytes hexadecimal message string, TC = 19
Returns:
int or string: 95% horizontal accuracy bounds for velocity, Horizontal Figure of Merit
int or string: 95% vertical accuracy bounds for velocity, Vertical Figure of Merit
"""
tc = typecode(msg)
if tc != 19:
raise RuntimeError("%s: Not an airborne velocity message, expecting TC = 19" % msg)
msgbin = common.hex2bin(msg)
NACv = common.bin2int(msgbin[42:45])
try:
HFOMr = uncertainty.NACv[NACv]['HFOMr']
VFOMr = uncertainty.NACv[NACv]['VFOMr']
except KeyError:
HFOMr, VFOMr = uncertainty.NA, uncertainty.NA
return HFOMr, VFOMr
def sil(msg, version):
"""Calculate SIL, Surveillance Integrity Level
Args:
msg (string): 28 bytes hexadecimal message string with TC = 29, 31
Returns:
int or string: Probability of exceeding Horizontal Radius of Containment RCu
int or string: Probability of exceeding Vertical Integrity Containment Region VPL
string: SIL supplement based on per "hour" or "sample", or 'unknown'
"""
tc = typecode(msg)
if tc not in [29, 31]:
raise RuntimeError("%s: Not a target state and status messag, \
or operation status message, expecting TC = 29 or 31" % msg)
msgbin = common.hex2bin(msg)
if tc == 29:
SIL = common.bin2int(msgbin[76:78])
elif tc == 31:
SIL = common.bin2int(msgbin[82:84])
try:
PE_RCu = uncertainty.SIL[SIL]['PE_RCu']
PE_VPL = uncertainty.SIL[SIL]['PE_VPL']
except KeyError:
PE_RCu, PE_VPL = uncertainty.NA, uncertainty.NA
base = 'unknown'
if version == 2:
if tc == 29:
SIL_SUP = common.bin2int(msgbin[39])
elif tc == 31:
SIL_SUP = common.bin2int(msgbin[86])
if SIL_SUP == 0:
base = "hour"
elif SIL_SUP == 1:
base = "sample"
return PE_RCu, PE_VPL, base

View 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

View File

@@ -0,0 +1,154 @@
# 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' # indentification and category
if 5 <= tc <= 8:
return 'BDS06' # surface movement
if 9 <= tc <= 18:
return 'BDS05' # airborne position, baro-alt
if tc == 19:
return 'BDS09' # airborne velocity
if 20 <= tc <= 22:
return 'BDS05' # airborne position, gnss-alt
if tc == 28:
return 'BDS61' # aircraft status
if tc == 29:
return 'BDS62' # target state and status
if tc == 31:
return 'BDS65' # operational status
# For Comm-B replies
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

View File

@@ -0,0 +1,161 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,5
# ADS-B TC=9-18
# Airborn position
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
def airborne_position(msg0, msg1, t0, t1):
"""Decode airborn position from a pair of even and odd position message
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
mb0 = common.hex2bin(msg0)[32:]
mb1 = common.hex2bin(msg1)[32:]
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(mb0[22:39]) / 131072.0
cprlon_even = common.bin2int(mb0[39:56]) / 131072.0
cprlat_odd = common.bin2int(mb1[22:39]) / 131072.0
cprlon_odd = common.bin2int(mb1[39:56]) / 131072.0
air_d_lat_even = 360.0 / 60
air_d_lat_odd = 360.0 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
if lat_even >= 270:
lat_even = lat_even - 360
if lat_odd >= 270:
lat_odd = lat_odd - 360
# check if both are in the same latidude zone, exit if not
if common.cprNL(lat_even) != common.cprNL(lat_odd):
return None
# compute ni, longitude index m, and longitude
if (t0 > t1):
lat = lat_even
nl = common.cprNL(lat)
ni = max(common.cprNL(lat)- 0, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat)
ni = max(common.cprNL(lat) - 1, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (360.0 / ni) * (m % ni + cprlon_odd)
if lon > 180:
lon = lon - 360
return round(lat, 5), round(lon, 5)
def airborne_position_with_ref(msg, lat_ref, lon_ref):
"""Decode airborne position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be with in 180NM of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0
i = int(mb[21])
d_lat = 360.0/59 if i else 360.0/60
j = common.floor(lat_ref / d_lat) \
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
lat = d_lat * (j + cprlat)
ni = common.cprNL(lat) - i
if ni > 0:
d_lon = 360.0 / ni
else:
d_lon = 360.0
m = common.floor(lon_ref / d_lon) \
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5)
def altitude(msg):
"""Decode aircraft altitude
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: altitude in feet
"""
tc = common.typecode(msg)
if tc<9 or tc==19 or tc>22:
raise RuntimeError("%s: Not a airborn position message" % msg)
mb = common.hex2bin(msg)[32:]
if tc < 19:
# barometric altitude
q = mb[15]
if q:
n = common.bin2int(mb[8:15]+mb[16:20])
alt = n * 25 - 1000
else:
alt = None
else:
# GNSS altitude, meters -> feet
alt = common.bin2int(mb[8:20]) * 3.28084
return alt

View File

@@ -0,0 +1,194 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,6
# ADS-B TC=5-8
# Surface position
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
import math
def surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref):
"""Decode surface position from a pair of even and odd position message,
the lat/lon of receiver must be provided to yield the correct solution.
Args:
msg0 (string): even message (28 bytes hexadecimal string)
msg1 (string): odd message (28 bytes hexadecimal string)
t0 (int): timestamps for the even message
t1 (int): timestamps for the odd message
lat_ref (float): latitude of the receiver
lon_ref (float): longitude of the receiver
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
msgbin0 = common.hex2bin(msg0)
msgbin1 = common.hex2bin(msg1)
# 131072 is 2^17, since CPR lat and lon are 17 bits each.
cprlat_even = common.bin2int(msgbin0[54:71]) / 131072.0
cprlon_even = common.bin2int(msgbin0[71:88]) / 131072.0
cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072.0
cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072.0
air_d_lat_even = 90.0 / 60
air_d_lat_odd = 90.0 / 59
# compute latitude index 'j'
j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5)
# solution for north hemisphere
lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even))
lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd))
# solution for north hemisphere
lat_even_s = lat_even_n - 90.0
lat_odd_s = lat_odd_n - 90.0
# chose which solution corrispondes to receiver location
lat_even = lat_even_n if lat_ref > 0 else lat_even_s
lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s
# check if both are in the same latidude zone, rare but possible
if common.cprNL(lat_even) != common.cprNL(lat_odd):
return None
# compute ni, longitude index m, and longitude
if (t0 > t1):
lat = lat_even
nl = common.cprNL(lat_even)
ni = max(common.cprNL(lat_even) - 0, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_even)
else:
lat = lat_odd
nl = common.cprNL(lat_odd)
ni = max(common.cprNL(lat_odd) - 1, 1)
m = common.floor(cprlon_even * (nl-1) - cprlon_odd * nl + 0.5)
lon = (90.0 / ni) * (m % ni + cprlon_odd)
# four possible longitude solutions
lons = [lon, lon + 90.0, lon + 180.0, lon + 270.0]
# the closest solution to receiver is the correct one
dls = [abs(lon_ref - l) for l in lons]
imin = min(range(4), key=dls.__getitem__)
lon = lons[imin]
return round(lat, 5), round(lon, 5)
def surface_position_with_ref(msg, lat_ref, lon_ref):
"""Decode surface position with only one message,
knowing reference nearby location, such as previously calculated location,
ground station, or airport location, etc. The reference position shall
be with in 45NM of the true position.
Args:
msg (string): even message (28 bytes hexadecimal string)
lat_ref: previous known latitude
lon_ref: previous known longitude
Returns:
(float, float): (latitude, longitude) of the aircraft
"""
mb = common.hex2bin(msg)[32:]
cprlat = common.bin2int(mb[22:39]) / 131072.0
cprlon = common.bin2int(mb[39:56]) / 131072.0
i = int(mb[21])
d_lat = 90.0/59 if i else 90.0/60
j = common.floor(lat_ref / d_lat) \
+ common.floor(0.5 + ((lat_ref % d_lat) / d_lat) - cprlat)
lat = d_lat * (j + cprlat)
ni = common.cprNL(lat) - i
if ni > 0:
d_lon = 90.0 / ni
else:
d_lon = 90.0
m = common.floor(lon_ref / d_lon) \
+ common.floor(0.5 + ((lon_ref % d_lon) / d_lon) - cprlon)
lon = d_lon * (m + cprlon)
return round(lat, 5), round(lon, 5)
def surface_velocity(msg, 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 movment / speed
mov = common.bin2int(mb[5:12])
if mov == 0 or mov > 124:
spd = None
elif mov == 1:
spd = 0
elif mov == 124:
spd = 175
else:
movs = [2, 9, 13, 39, 94, 109, 124]
kts = [0.125, 1, 2, 15, 70, 100, 175]
i = next(m[0] for m in enumerate(movs) if m[1] > mov)
step = (kts[i] - kts[i-1]) * 1.0 / (movs[i]-movs[i-1])
spd = kts[i-1] + (mov-movs[i-1]) * step
spd = round(spd, 2)
if rtn_sources:
return spd, trk, 0, 'GS', 'true_north', None
else:
return spd, trk, 0, 'GS'

View File

@@ -0,0 +1,73 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# BDS 0,8
# ADS-B TC=1-4
# Aircraft identitification and category
# ------------------------------------------
from __future__ import absolute_import, print_function, division
from pyModeS.decoder import common
def category(msg):
"""Aircraft category number
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
int: category number
"""
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
raise RuntimeError("%s: Not a identification message" % msg)
msgbin = common.hex2bin(msg)
return common.bin2int(msgbin[5:8])
def callsign(msg):
"""Aircraft callsign
Args:
msg (string): 28 bytes hexadecimal message string
Returns:
string: callsign
"""
if common.typecode(msg) < 1 or common.typecode(msg) > 4:
raise RuntimeError("%s: Not a identification message" % msg)
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
msgbin = common.hex2bin(msg)
csbin = msgbin[40:96]
cs = ''
cs += chars[common.bin2int(csbin[0:6])]
cs += chars[common.bin2int(csbin[6:12])]
cs += chars[common.bin2int(csbin[12:18])]
cs += chars[common.bin2int(csbin[18:24])]
cs += chars[common.bin2int(csbin[24:30])]
cs += chars[common.bin2int(csbin[30:36])]
cs += chars[common.bin2int(csbin[36:42])]
cs += chars[common.bin2int(csbin[42:48])]
# clean string, remove spaces and marks, if any.
# cs = cs.replace('_', '')
cs = cs.replace('#', '')
return cs

View 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 refrence, '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.

View File

@@ -0,0 +1,66 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# 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 capabilty conflict
if d[14] == '1' and bin2int(d[16:23]) < 5:
return False
if d[14] == '0' and bin2int(d[16:23]) > 4:
return False
return True
def ovc10(msg):
"""Return the overlay control capability
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: Whether the transponder is OVC capable
"""
d = hex2bin(data(msg))
return int(d[14])

View 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 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 suport BDS codes
"""
allbds = ['05', '06', '07', '08', '09', '0A', '20', '21', '40', '41',
'42', '43', '44', '45', '48', '50', '51', '52', '53', '54',
'55', '56', '5F', '60', 'NA', 'NA', 'E1', 'E2']
d = hex2bin(data(msg))
idx = [i for i, v in enumerate(d[:28]) if v=='1']
capacity = ['BDS'+allbds[i] for i in idx if allbds[i] is not 'NA']
return capacity

View File

@@ -0,0 +1,73 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# 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

View File

@@ -0,0 +1,50 @@
# Copyright (C) 2018 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------
# 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

View 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 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.simplefilter("once", DeprecationWarning)
warnings.warn(
"alt40mcp() has been renamed to selalt40mcp(). It will be removed in the future.",
DeprecationWarning,
)
return selalt40mcp(msg)
def alt40fms(msg):
warnings.simplefilter("once", DeprecationWarning)
warnings.warn(
"alt40fms() has been renamed to selalt40fms(). It will be removed in the future.",
DeprecationWarning,
)
return selalt40mcp(msg)

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

View 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

View 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

View File

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

View 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 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 messured by onbard equiments (IRS, AHRS)
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
d = hex2bin(data(msg))
if d[45] == '0':
return None
sign = int(d[46]) # 1 -> negative value, two's complement
value = bin2int(d[47:56])
if value == 0 or value == 511: # all zeros or all ones
return 0
value = value - 512 if sign else value
roc = value * 32 # feet/min
return roc

37
pyModeS/decoder/commb.py Normal file
View 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 *

365
pyModeS/decoder/common.py Normal file
View File

@@ -0,0 +1,365 @@
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."""
return hex(n)[2:].rjust(6, '0').upper()
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 vaule, bits 1 to 5."""
msgbin = hex2bin(msg)
return min(bin2int(msgbin[0:5]), 24)
def crc(msg, encode=False):
"""Mode-S Cyclic Redundancy Check.
Detect if bit error occurs in the Mode-S message. 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:
msg = msg[:-6] + "000000"
msgbin = hex2bin(msg)
msgbin_split = wrap(msgbin, 8)
mbytes = list(map(bin2int, msgbin_split))
for ibyte in range(len(mbytes)-3):
for ibit in range(8):
mask = 0x80 >> ibit
bits = mbytes[ibyte] & mask
if bits > 0:
mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit)
mbytes[ibyte+1] = mbytes[ibyte+1] ^ (0xFF & ((G[0] << 8-ibit) | (G[1] >> ibit)))
mbytes[ibyte+2] = mbytes[ibyte+2] ^ (0xFF & ((G[1] << 8-ibit) | (G[2] >> ibit)))
mbytes[ibyte+3] = mbytes[ibyte+3] ^ (0xFF & ((G[2] << 8-ibit) | (G[3] >> ibit)))
result = (mbytes[-3] << 16) | (mbytes[-2] << 8) | mbytes[-1]
return result
def crc_legacy(msg, 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
msgbin = hex2bin(msg)
return bin2int(msgbin[32:37])
def cprNL(lat):
"""NL() function in CPR decoding."""
if lat == 0:
return 59
if lat == 87 or lat == -87:
return 2
if lat > 87 or lat < -87:
return 1
nz = 15
a = 1 - np.cos(np.pi / (2 * nz))
b = np.cos(np.pi / 180.0 * abs(lat)) ** 2
nl = 2 * np.pi / (np.arccos(1 - a/b))
NL = floor(nl)
return NL
def idcode(msg):
"""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

29
pyModeS/decoder/ehs.py Normal file
View File

@@ -0,0 +1,29 @@
"""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)

22
pyModeS/decoder/els.py Normal file
View File

@@ -0,0 +1,22 @@
"""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
View 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

View File

@@ -0,0 +1,125 @@
"""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}},
}

View File

@@ -1,999 +0,0 @@
# Copyright (C) 2016 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
A python package for decoding ModeS (DF20, DF21) messages.
"""
from __future__ import absolute_import, print_function, division
from . import util, modes_common
def icao(msg):
return modes_common.icao(msg)
def data(msg):
"""Return the data frame in the message, bytes 9 to 22"""
return msg[8:22]
def isnull(msg):
"""check if the data bits are all zeros
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
d = util.hex2bin(data(msg))
if util.bin2int(d) > 0:
return False
else:
return True
def checkbits(data, sb, msb, lsb):
"""Check if the status bit and field bits are consistency. This Function
is used for checking BDS code versions.
"""
# status bit, most significant bit, least significant bit
status = int(data[sb-1])
value = util.bin2int(data[msb-1:lsb])
if not status:
if value != 0:
return False
return True
# ------------------------------------------
# Common functions
# ------------------------------------------
def df20alt(msg):
"""Computes the altitude from DF20 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if util.df(msg) != 20:
raise RuntimeError("Message must be Downlink Format 20.")
return modes_common.altcode(msg)
def df21id(msg):
"""Computes identity (squawk code) from DF21, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if util.df(msg) != 21:
raise RuntimeError("Message must be Downlink Format 21.")
return modes_common.idcode(msg)
# ------------------------------------------
# BDS 1,7
# Common usage GICB capability report
# ------------------------------------------
def isBDS17(msg):
"""Check if a message is likely to be BDS code 1,7
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
if isnull(msg):
return False
d = util.hex2bin(data(msg))
result = True
if util.bin2int(d[28:56]) != 0:
result &= False
caps = cap17(msg)
# basic BDS codes for ADS-B shall be supported
# assuming ADS-B out is installed (2017EU/2020US mandate)
if not set(['BDS05', 'BDS06', 'BDS09', 'BDS20']).issubset(caps):
result &= False
return result
def cap17(msg):
"""Extract capacities from BDS 1,7 message
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
list: list of suport BDS codes
"""
allbds = ['05', '06', '07', '08', '09', '0A', '20', '21', '40', '41',
'42', '43', '44', '45', '48', '50', '51', '52', '53', '54',
'55', '56', '5F', '60', 'NA', 'NA', 'E1', 'E2']
d = util.hex2bin(data(msg))
idx = [i for i, v in enumerate(d[:28]) if v=='1']
capacity = ['BDS'+allbds[i] for i in idx if allbds[i] is not 'NA']
return capacity
# ------------------------------------------
# BDS 2,0
# Aircraft identification
# ------------------------------------------
def isBDS20(msg):
"""Check if a message is likely to be BDS code 2,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 14, and 27
d = util.hex2bin(data(msg))
result = True
if util.bin2int(d[0:4]) != 2 or util.bin2int(d[4:8]) != 0:
result &= False
cs = callsign(msg)
if '#' in cs:
result &= False
return result
def callsign(msg):
"""Aircraft callsign
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
string: callsign, max. 8 chars
"""
chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######'
d = util.hex2bin(data(msg))
cs = ''
cs += chars[util.bin2int(d[8:14])]
cs += chars[util.bin2int(d[14:20])]
cs += chars[util.bin2int(d[20:26])]
cs += chars[util.bin2int(d[26:32])]
cs += chars[util.bin2int(d[32:38])]
cs += chars[util.bin2int(d[38:44])]
cs += chars[util.bin2int(d[44:50])]
cs += chars[util.bin2int(d[50:56])]
return cs
# ------------------------------------------
# BDS 4,0
# Selected vertical intention
# ------------------------------------------
def isBDS40(msg):
"""Check if a message is likely to be BDS code 4,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 14, and 27
d = util.hex2bin(data(msg))
result = True
result = result & checkbits(d, 1, 2, 13) \
& checkbits(d, 14, 15, 26) & checkbits(d, 27, 28, 39)
# bits 40-47 and 52-53 shall all be zero
if util.bin2int(d[39:47]) != 0:
result &= False
if util.bin2int(d[51:53]) != 0:
result &= False
return result
def alt40mcp(msg):
"""Selected altitude, MCP/FCU
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
int: altitude in feet
"""
d = util.hex2bin(data(msg))
if d[0] == '0':
return None
alt = util.bin2int(d[1:13]) * 16 # ft
return alt
def alt40fms(msg):
"""Selected altitude, FMS
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
int: altitude in feet
"""
d = util.hex2bin(data(msg))
if d[13] == '0':
return None
alt = util.bin2int(d[14:26]) * 16 # ft
return alt
def p40baro(msg):
"""Barometric pressure setting
Args:
msg (String): 28 bytes hexadecimal message (BDS40) string
Returns:
float: pressure in millibar
"""
d = util.hex2bin(data(msg))
if d[26] == '0':
return None
p = util.bin2int(d[27:39]) * 0.1 + 800 # millibar
return p
# ------------------------------------------
# BDS 4,4
# Meteorological routine air report
# ------------------------------------------
def isBDS44(msg, rev=False):
"""Check if a message is likely to be BDS code 4,4
Meteorological routine air report
Args:
msg (String): 28 bytes hexadecimal message string
rev (bool): using revised version
Returns:
bool: True or False
"""
if isnull(msg):
return False
d = util.hex2bin(data(msg))
result = True
if not rev:
# status bit 5, 35, 47, 50
result = result & checkbits(d, 5, 6, 23) \
& checkbits(d, 35, 36, 46) & checkbits(d, 47, 48, 49) \
& checkbits(d, 50, 51, 56)
# Bits 1-4 indicate source, values > 4 reserved and should not occur
if util.bin2int(d[0:4]) > 4:
result &= False
else:
# status bit 5, 15, 24, 36, 49
result = result & checkbits(d, 5, 6, 14) \
& checkbits(d, 15, 16, 23) & checkbits(d, 24, 25, 35) \
& checkbits(d, 36, 37, 47) & checkbits(d, 49, 50, 56)
# Bits 1-4 are reserved and should be zero
if util.bin2int(d[0:4]) != 0:
result &= False
if not result:
return False
vw = wind44(msg, rev=rev)
if vw is not None and vw[0] > 250:
result &= False
if temp44(msg):
if temp44(msg) > 60 or temp44(msg) < -80:
result &= False
elif temp44(msg) == 0:
result &= False
return result
def wind44(msg, rev=False):
"""reported wind speed and direction
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
rev (bool): using revised version
Returns:
(int, float): speed (kt), direction (degree)
"""
d = util.hex2bin(data(msg))
if not rev:
status = int(d[4])
if not status:
return None
speed = util.bin2int(d[5:14]) # knots
direction = util.bin2int(d[14:23]) * 180.0 / 256.0 # degree
else:
spd_status = int(d[4])
dir_status = int(d[14])
if (not spd_status) or (not dir_status):
return None
speed = util.bin2int(d[5:14]) # knots
direction = util.bin2int(d[15:23]) * 180.0 / 128.0 # degree
return round(speed, 0), round(direction, 1)
def temp44(msg, rev=False):
"""reported air temperature
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
rev (bool): using revised version
Returns:
float: tmeperature in Celsius degree
"""
d = util.hex2bin(data(msg))
if not rev:
# if d[22] == '0':
# return None
sign = int(d[23])
value = util.bin2int(d[24:34])
if sign:
value = value - 1024
temp = value * 0.125 # celsius
temp = round(temp, 1)
else:
# if d[23] == '0':
# return None
sign = int(d[24])
value = util.bin2int(d[25:35])
if sign:
value = value - 1024
temp = value * 0.125 # celsius
temp = round(temp, 1)
return -1*temp if sign else temp
def p44(msg, rev=False):
"""reported average static pressure
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
rev (bool): using revised version
Returns:
int: static pressure in hPa
"""
d = util.hex2bin(data(msg))
if not rev:
if d[34] == '0':
return None
p = util.bin2int(d[35:46]) # hPa
else:
if d[35] == '0':
return None
p = util.bin2int(d[36:47]) # hPa
return p
def hum44(msg, rev=False):
"""reported humidity
Args:
msg (String): 28 bytes hexadecimal message (BDS44) string
rev (bool): using revised version
Returns:
float: percentage of humidity, [0 - 100] %
"""
d = util.hex2bin(data(msg))
if not rev:
if d[49] == '0':
return None
hm = util.bin2int(d[50:56]) * 100.0 / 64 # %
else:
if d[48] == '0':
return None
hm = util.bin2int(d[49:56]) # %
return round(hm, 1)
# ------------------------------------------
# BDS 5,0
# Track and turn report
# ------------------------------------------
def isBDS50(msg):
"""Check if a message is likely to be BDS code 5,0
(Track and turn report)
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 12, 24, 35, 46
d = util.hex2bin(data(msg))
result = True
result = result & checkbits(d, 1, 3, 11) & checkbits(d, 12, 13, 23) \
& checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \
& checkbits(d, 46, 47, 56)
if not result:
return False
if d[2:11] == "000000000":
result &= True
else:
roll = abs(roll50(msg))
if roll and roll > 60:
result &= False
gs = gs50(msg)
if gs is not None and gs > 600:
result &= False
tas = tas50(msg)
if tas is not None and tas > 500:
result &= False
if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200):
result &= False
return result
def roll50(msg):
"""Roll angle, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
float: angle in degrees,
negative->left wing down, positive->right wing down
"""
d = util.hex2bin(data(msg))
if d[0] == '0':
return None
sign = int(d[1]) # 1 -> left wing down
value = util.bin2int(d[2:11])
if sign:
value = value - 512
angle = value * 45.0 / 256.0 # degree
return round(angle, 1)
def trk50(msg):
"""True track angle, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
float: angle in degrees to true north (from 0 to 360)
"""
d = util.hex2bin(data(msg))
if d[11] == '0':
return None
sign = int(d[12]) # 1 -> west
value = util.bin2int(d[13:23])
if sign:
value = value - 1024
trk = value * 90.0 / 512.0
# convert from [-180, 180] to [0, 360]
if trk < 0:
trk = 360 + trk
return round(trk, 1)
def gs50(msg):
"""Ground speed, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
int: ground speed in knots
"""
d = util.hex2bin(data(msg))
if d[23] == '0':
return None
spd = util.bin2int(d[24:34]) * 2 # kts
return spd
def rtrk50(msg):
"""Track angle rate, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
float: angle rate in degrees/second
"""
d = util.hex2bin(data(msg))
if d[34] == '0':
return None
if d[36:45] == "111111111":
return None
sign = int(d[35]) # 1 -> negative value, two's complement
value = util.bin2int(d[36:45])
if sign:
value = value - 512
angle = value * 8.0 / 256.0 # degree / sec
return round(angle, 3)
def tas50(msg):
"""Aircraft true airspeed, BDS 5,0 message
Args:
msg (String): 28 bytes hexadecimal message (BDS50) string
Returns:
int: true airspeed in knots
"""
d = util.hex2bin(data(msg))
if d[45] == '0':
return None
tas = util.bin2int(d[46:56]) * 2 # kts
return tas
# ------------------------------------------
# BDS 5,3
# Air-referenced state vector
# ------------------------------------------
def isBDS53(msg):
"""Check if a message is likely to be BDS code 5,3
(Air-referenced state vector)
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 13, 24, 34, 47
d = util.hex2bin(data(msg))
result = True
result = result & checkbits(d, 1, 3, 12) & checkbits(d, 13, 14, 23) \
& checkbits(d, 24, 25, 33) & checkbits(d, 34, 35, 46) \
& checkbits(d, 47, 49, 56)
if not result:
return False
ias = ias53(msg)
if ias is not None and ias > 500:
result &= False
mach = mach53(msg)
if mach is not None and mach > 1:
result &= False
tas = tas53(msg)
if tas is not None and tas > 500:
result &= False
vr = vr53(msg)
if vr is not None and abs(vr) > 8000:
result &= False
return result
def hdg53(msg):
"""Magnetic heading, BDS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message (BDS53) string
Returns:
float: angle in degrees to true north (from 0 to 360)
"""
d = util.hex2bin(data(msg))
if d[0] == '0':
return None
sign = int(d[1]) # 1 -> west
value = util.bin2int(d[2:12])
if sign:
value = value - 1024
hdg = value * 90.0 / 512.0 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
hdg = 360 + hdg
return round(hdg, 1)
def ias53(msg):
"""Indicated airspeed, DBS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message
Returns:
int: indicated arispeed in knots
"""
d = util.hex2bin(data(msg))
if d[12] == '0':
return None
ias = util.bin2int(d[13:23]) # knots
return ias
def mach53(msg):
"""MACH number, DBS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message
Returns:
float: MACH number
"""
d = util.hex2bin(data(msg))
if d[23] == '0':
return None
mach = util.bin2int(d[24:33]) * 0.008
return round(mach, 3)
def tas53(msg):
"""Aircraft true airspeed, BDS 5,3 message
Args:
msg (String): 28 bytes hexadecimal message
Returns:
float: true airspeed in knots
"""
d = util.hex2bin(data(msg))
if d[33] == '0':
return None
tas = util.bin2int(d[34:46]) * 0.5 # kts
return round(tas, 1)
def vr53(msg):
"""Vertical rate
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
d = util.hex2bin(data(msg))
if d[46] == '0':
return None
sign = int(d[47]) # 1 -> negative value, two's complement
value = util.bin2int(d[48:56])
if value == 0 or value == 255: # all zeros or all ones
return 0
value = value - 256 if sign else value
roc = value * 64 # feet/min
return roc
# ------------------------------------------
# BDS 6,0
# ------------------------------------------
def isBDS60(msg):
"""Check if a message is likely to be BDS code 6,0
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
bool: True or False
"""
if isnull(msg):
return False
# status bit 1, 13, 24, 35, 46
d = util.hex2bin(data(msg))
result = True
result = result & checkbits(d, 1, 2, 12) & checkbits(d, 13, 14, 23) \
& checkbits(d, 24, 25, 34) & checkbits(d, 35, 36, 45) \
& checkbits(d, 46, 47, 56)
if not result:
return False
ias = ias60(msg)
if ias is not None and ias > 500:
result &= False
mach = mach60(msg)
if mach is not None and mach > 1:
result &= False
# leave out the check from vertical rates,
# due to very noisy measurement
return result
def hdg60(msg):
"""Megnetic heading of aircraft
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
float: heading in degrees to megnetic north (from 0 to 360)
"""
d = util.hex2bin(data(msg))
if d[0] == '0':
return None
sign = int(d[1]) # 1 -> west
value = util.bin2int(d[2:12])
if sign:
value = value - 1024
hdg = value * 90 / 512.0 # degree
# convert from [-180, 180] to [0, 360]
if hdg < 0:
hdg = 360 + hdg
return round(hdg, 1)
def ias60(msg):
"""Indicated airspeed
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: indicated airspeed in knots
"""
d = util.hex2bin(data(msg))
if d[12] == '0':
return None
ias = util.bin2int(d[13:23]) # kts
return ias
def mach60(msg):
"""Aircraft MACH number
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
float: MACH number
"""
d = util.hex2bin(data(msg))
if d[23] == '0':
return None
mach = util.bin2int(d[24:34]) * 2.048 / 512.0
return round(mach, 3)
def vr60baro(msg):
"""Vertical rate from barometric measurement, this value may be very noisy.
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
d = util.hex2bin(data(msg))
if d[34] == '0':
return None
sign = int(d[35]) # 1 -> negative value, two's complement
value = util.bin2int(d[36:45])
if value == 0 or value == 511: # all zeros or all ones
return 0
value = value - 512 if sign else value
roc = value * 32 # feet/min
return roc
def vr60ins(msg):
"""Vertical rate messured by onbard equiments (IRS, AHRS)
Args:
msg (String): 28 bytes hexadecimal message (BDS60) string
Returns:
int: vertical rate in feet/minutes
"""
d = util.hex2bin(data(msg))
if d[45] == '0':
return None
sign = int(d[46]) # 1 -> negative value, two's complement
value = util.bin2int(d[47:56])
if value == 0 or value == 511: # all zeros or all ones
return 0
value = value - 512 if sign else value
roc = value * 32 # feet/min
return roc
def BDS(msg):
"""Estimate the most likely BDS code of an message
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String or None: Version: "BDS20", "BDS40", "BDS50", or "BDS60". Or None, if nothing matched
"""
if isnull(msg):
return None
is17 = isBDS17(msg)
is20 = isBDS20(msg)
is40 = isBDS40(msg)
is44 = isBDS44(msg)
is44rev = isBDS44(msg, rev=True)
is50 = isBDS50(msg)
is53 = isBDS53(msg)
is60 = isBDS60(msg)
BDS = ["BDS17", "BDS20", "BDS40", "BDS44", "BDS44REV", "BDS50", "BDS53", "BDS60"]
isBDS = [is17, is20, is40, is44, is44rev, is50, is53, is60]
if sum(isBDS) == 0:
return None
elif sum(isBDS) == 1:
return BDS[isBDS.index(True)]
else:
return [bds for (bds, i) in zip(BDS, isBDS) if i]

View File

@@ -1,37 +0,0 @@
from __future__ import absolute_import, print_function, division
from . import util, modes_common
def icao(msg):
return modes_common.icao(msg)
def df4alt(msg):
"""Computes the altitude from DF4 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if util.df(msg) != 4:
raise RuntimeError("Message must be Downlink Format 4.")
return modes_common.altcode(msg)
def df5id(msg):
"""Computes identity (squawk code) from DF5 message, bit 20-32
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if util.df(msg) != 5:
raise RuntimeError("Message must be Downlink Format 5.")
return modes_common.idcode(msg)

View File

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

184
pyModeS/extra/aero.py Normal file
View File

@@ -0,0 +1,184 @@
"""
Functions for aeronautics in this module
- physical quantities always in SI units
- lat,lon,course and heading in degrees
International Standard Atmosphere
::
p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m]
a = vsound(H) # speed of sound [m/s] as function of H[m]
p = pressure(H) # calls atmos but retruns only pressure [Pa]
T = temperature(H) # calculates temperature [K]
rho = density(H) # calls atmos but retruns only pressure [Pa]
Speed conversion at altitude H[m] in ISA
::
Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion
Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion
Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m]
Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m]
Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m]
Vcas = tas2cas(Vtas,H) # Vtas to Vcas conversion both m/s, H in [m]
Vcas = mach2cas(Mach,H) # Mach to Vcas conversion Vcas in m/s, H in [m]
Mach = cas2mach(Vcas,H) # Vcas to mach copnversion Vcas in m/s, H in [m]
"""
import numpy as np
"""Aero and geo Constants """
kts = 0.514444 # knot -> m/s
ft = 0.3048 # ft -> m
fpm = 0.00508 # ft/min -> m/s
inch = 0.0254 # inch -> m
sqft = 0.09290304 # 1 square foot
nm = 1852. # nautical mile -> m
lbs = 0.453592 # pound -> kg
g0 = 9.80665 # m/s2, Sea level gravity constant
R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA
p0 = 101325. # Pa, air pressure, sea level ISA
rho0 = 1.225 # kg/m3, air density, sea level ISA
T0 = 288.15 # K, temperature, sea level ISA
gamma = 1.40 # cp/cv for air
gamma1 = 0.2 # (gamma-1)/2 for air
gamma2 = 3.5 # gamma/(gamma-1) for air
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
r_earth = 6371000. # m, average earth radius
a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0)
def atmos(H):
# H in metres
T = np.maximum(288.15 - 0.0065 * H, 216.65)
rhotrop = 1.225 * (T / 288.15)**4.256848030018761
dhstrat = np.maximum(0., H - 11000.0)
rho = rhotrop * np.exp(-dhstrat / 6341.552161)
p = rho * R * T
return p, rho, T
def temperature(H):
p, r, T = atmos(H)
return T
def pressure(H):
p, r, T = atmos(H)
return p
def density(H):
p, r, T = atmos(H)
return r
def vsound(H):
"""Speed of sound"""
T = temperature(H)
a = np.sqrt(gamma * R * T)
return a
def distance(lat1, lon1, lat2, lon2, H=0):
"""
Compute spherical distance from spherical coordinates.
For two locations in spherical coordinates
(1, theta, phi) and (1, theta', phi')
cosine( arc length ) =
sin phi sin phi' cos(theta-theta') + cos phi cos phi'
distance = rho * arc length
"""
# phi = 90 - latitude
phi1 = np.radians(90.0 - lat1)
phi2 = np.radians(90.0 - lat2)
# theta = longitude
theta1 = np.radians(lon1)
theta2 = np.radians(lon2)
cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos(phi2)
cos = np.where(cos>1, 1, cos)
arc = np.arccos(cos)
dist = arc * (r_earth + H) # meters, radius of earth
return dist
def bearing(lat1, lon1, lat2, lon2):
lat1 = np.radians(lat1)
lon1 = np.radians(lon1)
lat2 = np.radians(lat2)
lon2 = np.radians(lon2)
x = np.sin(lon2-lon1) * np.cos(lat2)
y = np.cos(lat1) * np.sin(lat2) \
- np.sin(lat1) * np.cos(lat2) * np.cos(lon2-lon1)
initial_bearing = np.arctan2(x, y)
initial_bearing = np.degrees(initial_bearing)
bearing = (initial_bearing + 360) % 360
return bearing
# -----------------------------------------------------
# Speed conversions, altitude H all in meters
# -----------------------------------------------------
def tas2mach(Vtas, H):
"""True Airspeed to Mach number"""
a = vsound(H)
Mach = Vtas/a
return Mach
def mach2tas(Mach, H):
"""Mach number to True Airspeed"""
a = vsound(H)
Vtas = Mach*a
return Vtas
def eas2tas(Veas, H):
"""Equivalent Airspeed to True Airspeed"""
rho = density(H)
Vtas = Veas * np.sqrt(rho0/rho)
return Vtas
def tas2eas(Vtas, H):
"""True Airspeed to Equivalent Airspeed"""
rho = density(H)
Veas = Vtas * np.sqrt(rho/rho0)
return Veas
def cas2tas(Vcas, H):
"""Calibrated Airspeed to True Airspeed"""
p, rho, T = atmos(H)
qdyn = p0*((1.+rho0*Vcas*Vcas/(7.*p0))**3.5-1.)
Vtas = np.sqrt(7.*p/rho*((1.+qdyn/p)**(2./7.)-1.))
return Vtas
def tas2cas(Vtas, H):
"""True Airspeed to Calibrated Airspeed"""
p, rho, T = atmos(H)
qdyn = p*((1.+rho*Vtas*Vtas/(7.*p))**3.5-1.)
Vcas = np.sqrt(7.*p0/rho0*((qdyn/p0+1.)**(2./7.)-1.))
return Vcas
def mach2cas(Mach, H):
"""Mach number to Calibrated Airspeed"""
Vtas = mach2tas(Mach, H)
Vcas = tas2cas(Vtas, H)
return Vcas
def cas2mach(Vcas, H):
"""Calibrated Airspeed to Mach number"""
Vtas = cas2tas(Vcas, H)
Mach = tas2mach(Vtas, H)
return Mach

302
pyModeS/extra/tcpclient.py Normal file
View File

@@ -0,0 +1,302 @@
'''
Stream beast raw data from a TCP server, convert to mode-s messages
'''
from __future__ import print_function, division
import os
import sys
import socket
import time
import pyModeS as pms
from threading import Thread
import traceback
if (sys.version_info > (3, 0)):
PY_VERSION = 3
else:
PY_VERSION = 2
class BaseClient(Thread):
def __init__(self, host, port, rawtype):
Thread.__init__(self)
self.host = host
self.port = port
self.buffer = []
self.rawtype = rawtype
if self.rawtype not in ['avr', 'beast', 'skysense']:
print("rawtype must be either avr, beast or skysense")
os._exit(1)
def connect(self):
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10) # 10 second timeout
s.connect((self.host, self.port))
print("Server connected - %s:%s" % (self.host, self.port))
print("collecting ADS-B messages...")
return s
except socket.error as err:
print("Socket connection error: %s. reconnecting..." % err)
time.sleep(3)
def read_avr_buffer(self):
# -- testing --
# for b in self.buffer:
# print(chr(b), b)
# Append message with 0-9,A-F,a-f, until stop sign
messages = []
msg_stop = False
for b in self.buffer:
if b == 59:
msg_stop = True
ts = time.time()
messages.append([self.current_msg, ts])
if b == 42:
msg_stop = False
self.current_msg = ''
if (not msg_stop) and (48<=b<=57 or 65<=b<=70 or 97<=b<=102):
self.current_msg = self.current_msg + chr(b)
self.buffer = []
return messages
def read_beast_buffer(self):
'''
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
2 byte Mode-AC
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
7 byte Mode-S short frame
<esc> "3" : 6 byte MLAT timestamp, 1 byte signal level,
14 byte Mode-S long frame
<esc> "4" : 6 byte MLAT timestamp, status data, DIP switch
configuration settings (not on Mode-S Beast classic)
<esc><esc>: true 0x1a
<esc> is 0x1a, and "1", "2" and "3" are 0x31, 0x32 and 0x33
timestamp:
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
'''
messages_mlat = []
msg = []
i = 0
# process the buffer until the last divider <esc> 0x1a
# then, reset the self.buffer with the remainder
while i < len(self.buffer):
if (self.buffer[i:i+2] == [0x1a, 0x1a]):
msg.append(0x1a)
i += 1
elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1a):
# special case where the last bit is 0x1a
msg.append(0x1a)
elif self.buffer[i] == 0x1a:
if i == len(self.buffer) - 1:
# special case where the last bit is 0x1a
msg.append(0x1a)
elif len(msg) > 0:
messages_mlat.append(msg)
msg = []
else:
msg.append(self.buffer[i])
i += 1
# save the reminder for next reading cycle, if not empty
if len(msg) > 0:
reminder = []
for i, m in enumerate(msg):
if (m == 0x1a) and (i < len(msg)-1):
# rewind 0x1a, except when it is at the last bit
reminder.extend([m, m])
else:
reminder.append(m)
self.buffer = [0x1a] + msg
else:
self.buffer = []
# extract messages
messages = []
for mm in messages_mlat:
ts = time.time()
msgtype = mm[0]
# print(''.join('%02X' % i for i in mm))
if msgtype == 0x32:
# Mode-S Short Message, 7 byte, 14-len hexstr
msg = ''.join('%02X' % i for i in mm[8:15])
elif msgtype == 0x33:
# Mode-S Long Message, 14 byte, 28-len hexstr
msg = ''.join('%02X' % i for i in mm[8:22])
else:
# Other message tupe
continue
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
Postion 1 through 14:
14 bytes = 112 bits
Mode-S payload
In case of DF types that only carry 7 bytes of information
position 8 through 14 are set to 0x00.
TS field - Time stamp
Position 15 through 20:
6 bytes = 48 bits
Time stamp with fields as:
Lock Status - Status of internal time keeping mechanism
Equal to 1 if operating normally
Bit 47 - 1 bit
Time of day in UTC seconds, between 0 and 86399
Bits 46 through 30 - 17 bits
Nanoseconds into current second, between 0 and 999999999
Bits 29 through 0 - 30 bits
RS field - Signal Level
Position 21 through 23:
3 bytes = 24 bits
RSSI (received signal strength indication) and relative
noise level with fields
RNL, Q12.4 unsigned fixed point binary with 4 fractional
bits and 8 integer bits.
This is and indication of the noise level of the message.
Roughly 40 counts per 10dBm.
Bits 23 through 12 - 12 bits
RSSI, Q12.4 unsigned fixed point binary with 4 fractional
bits and 8 integer bits.
This is an indication of the signal level of the received
message in ADC counts. Roughly 40 counts per 10dBm.
Bits 11 through 0 - 12 bits
"""
SS_MSGLENGTH = 24
SS_STARTCHAR = 0x24
if len(self.buffer) <= SS_MSGLENGTH:
return None
messages = []
while len(self.buffer) > SS_MSGLENGTH:
i = 0
if self.buffer[i] == SS_STARTCHAR and self.buffer[i+SS_MSGLENGTH] == SS_STARTCHAR:
i += 1
if (self.buffer[i]>>7):
#Long message
payload = self.buffer[i:i+14]
else:
#Short message
payload = self.buffer[i:i+7]
msg = ''.join('%02X' % j for j in payload)
i += 14 #Both message types use 14 bytes
tsbin = self.buffer[i:i+6]
sec = ( (tsbin[0] & 0x7f) << 10) | (tsbin[1] << 2 ) | (tsbin[2] >> 6)
nano = ( (tsbin[2] & 0x3f) << 24) | (tsbin[3] << 16) | (tsbin[4] << 8) | tsbin[5]
ts = sec + nano*1.0e-9
i += 6
#Signal and noise level - Don't care for now
i += 3
self.buffer = self.buffer[SS_MSGLENGTH:]
messages.append( [msg,ts] )
else:
self.buffer = self.buffer[1:]
return messages
def handle_messages(self, messages):
"""re-implement this method to handle the messages"""
for msg, t in messages:
print("%15.9f %s" % (t, msg))
def run(self):
sock = self.connect()
while True:
try:
received = sock.recv(1024)
if PY_VERSION == 2:
received = [ord(i) for i in received]
self.buffer.extend(received)
# print(''.join(x.encode('hex') for x in self.buffer))
# process self.buffer when it is longer enough
# if len(self.buffer) < 2048:
# continue
# -- Removed!! Cause delay in low data rate scenario --
if self.rawtype == 'beast':
messages = self.read_beast_buffer()
elif self.rawtype == 'avr':
messages = self.read_avr_buffer()
elif self.rawtype == 'skysense':
messages = self.read_skysense_buffer()
if not messages:
continue
else:
self.handle_messages(messages)
time.sleep(0.001)
except Exception as e:
# 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()
except Exception as e:
print("Unexpected Error:", e)
if __name__ == '__main__':
# for testing purpose only
host = sys.argv[1]
port = int(sys.argv[2])
rawtype = sys.argv[3]
client = BaseClient(host=host, port=port, rawtype=rawtype)
client.daemon = True
client.run()

View File

@@ -1,131 +0,0 @@
from __future__ import absolute_import, print_function, division
from . import util
def icao(msg):
"""Calculate the ICAO address from an Mode-S message
with DF4, DF5, DF20, DF21
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
String: ICAO address in 6 bytes hexadecimal string
"""
if util.df(msg) not in (4, 5, 20, 21):
# raise RuntimeError("Message DF must be in (4, 5, 20, 21)")
return None
c0 = util.bin2int(util.crc(msg, encode=True))
c1 = util.hex2int(msg[-6:])
addr = '%06X' % (c0 ^ c1)
return addr
def idcode(msg):
"""Computes identity (squawk code) from DF5 or DF21 message, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
string: squawk code
"""
if util.df(msg) not in [5, 21]:
raise RuntimeError("Message must be Downlink Format 5 or 21.")
mbin = util.hex2bin(msg)
C1 = mbin[19]
A1 = mbin[20]
C2 = mbin[21]
A2 = mbin[22]
C4 = mbin[23]
A4 = mbin[24]
# _ = mbin[25]
B1 = mbin[26]
D1 = mbin[27]
B2 = mbin[28]
D2 = mbin[29]
B4 = mbin[30]
D4 = mbin[31]
byte1 = int(A4+A2+A1, 2)
byte2 = int(B4+B2+B1, 2)
byte3 = int(C4+C2+C1, 2)
byte4 = int(D4+D2+D1, 2)
return str(byte1) + str(byte2) + str(byte3) + str(byte4)
def altcode(msg):
"""Computes the altitude from DF4 or DF20 message, bit 20-32.
credit: @fbyrkjeland
Args:
msg (String): 28 bytes hexadecimal message string
Returns:
int: altitude in ft
"""
if util.df(msg) not in [4, 20]:
raise RuntimeError("Message must be Downlink Format 4 or 20.")
# Altitude code, bit 20-32
mbin = util.hex2bin(msg)
mbit = mbin[25] # M bit: 26
qbit = mbin[27] # Q bit: 28
if mbit == '0': # unit in ft
if qbit == '1': # 25ft interval
vbin = mbin[19:25] + mbin[26] + mbin[28:32]
alt = util.bin2int(vbin) * 25 - 1000
if qbit == '0': # 100ft interval, above 50175ft
C1 = mbin[19]
A1 = mbin[20]
C2 = mbin[21]
A2 = mbin[22]
C4 = mbin[23]
A4 = mbin[24]
# _ = mbin[25]
B1 = mbin[26]
# D1 = mbin[27] # always zero
B2 = mbin[28]
D2 = mbin[29]
B4 = mbin[30]
D4 = mbin[31]
graystr = D2 + D4 + A1 + A2 + A4 + B1 + B2 + B4 + C1 + C2 + C4
alt = gray2alt(graystr)
if mbit == '1': # unit in meter
vbin = mbin[19:25] + mbin[26:31]
alt = int(util.bin2int(vbin) * 3.28084) # convert to ft
return alt
def gray2alt(codestr):
gc500 = codestr[:8]
n500 = util.gray2int(gc500)
# in 100-ft step must be converted first
gc100 = codestr[8:]
n100 = util.gray2int(gc100)
if n100 in [0, 5, 6]:
return None
if n100 == 7:
n100 = 5
if n500%2:
n100 = 6 - n100
alt = (n500*500 + n100*100) - 1300
return alt

View File

120
pyModeS/streamer/modeslive Executable file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python
from __future__ import print_function, division
import os
import sys
import time
import argparse
import curses
from threading import Lock
import pyModeS as pms
from pyModeS.extra.tcpclient import BaseClient
from pyModeS.streamer.stream import Stream
from pyModeS.streamer.screen import Screen
LOCK = Lock()
ADSB_MSG = []
ADSB_TS = []
COMMB_MSG = []
COMMB_TS = []
parser = argparse.ArgumentParser()
parser.add_argument('--server', help='server address or IP', required=True)
parser.add_argument('--port', help='raw data port', required=True)
parser.add_argument('--rawtype', help='beast, avr or skysense', required=True)
parser.add_argument('--latlon', help='receiver position', nargs=2, metavar=('LAT', 'LON'), required=True)
parser.add_argument('--show-uncertainty', dest='uncertainty', help='display uncertaint values, default off', action='store_true', required=False, default=False)
parser.add_argument('--dumpto', help='folder to dump decoded output', required=False, default=None)
args = parser.parse_args()
SERVER = args.server
PORT = int(args.port)
RAWTYPE = args.rawtype
LAT0 = float(args.latlon[0])
LON0 = float(args.latlon[1])
UNCERTAINTY = args.uncertainty
DUMPTO = args.dumpto
if DUMPTO is not None:
# append to current folder except root is given
if DUMPTO[0] != '/':
DUMPTO = os.getcwd() + '/' + DUMPTO
if not os.path.isdir(DUMPTO):
print('Error: dump folder (%s) does not exist' % DUMPTO)
sys.exit(1)
class ModesClient(BaseClient):
def __init__(self, host, port, rawtype):
super(ModesClient, self).__init__(host, port, rawtype)
def handle_messages(self, messages):
local_buffer_adsb_msg = []
local_buffer_adsb_ts = []
local_buffer_ehs_msg = []
local_buffer_ehs_ts = []
for msg, t in messages:
if len(msg) < 28: # only process long messages
continue
df = pms.df(msg)
if df == 17 or df == 18:
local_buffer_adsb_msg.append(msg)
local_buffer_adsb_ts.append(t)
elif df == 20 or df == 21:
local_buffer_ehs_msg.append(msg)
local_buffer_ehs_ts.append(t)
else:
continue
LOCK.acquire()
ADSB_MSG.extend(local_buffer_adsb_msg)
ADSB_TS.extend(local_buffer_adsb_ts)
COMMB_MSG.extend(local_buffer_ehs_msg)
COMMB_TS.extend(local_buffer_ehs_ts)
LOCK.release()
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, 'w')
client = ModesClient(host=SERVER, port=PORT, rawtype=RAWTYPE)
client.daemon = True
client.start()
stream = Stream(lat0=LAT0, lon0=LON0, dumpto=DUMPTO)
try:
screen = Screen(uncertainty=UNCERTAINTY)
screen.daemon = True
screen.start()
while True:
if len(ADSB_MSG) > 200:
LOCK.acquire()
stream.process_raw(ADSB_TS, ADSB_MSG, COMMB_TS, COMMB_MSG)
ADSB_MSG = []
ADSB_TS = []
COMMB_MSG = []
COMMB_TS = []
LOCK.release()
acs = stream.get_aircraft()
try:
screen.update_data(acs)
screen.update()
time.sleep(0.02)
except KeyboardInterrupt:
raise
except:
continue
except KeyboardInterrupt:
sys.exit(0)
finally:
curses.endwin()

183
pyModeS/streamer/screen.py Normal file
View File

@@ -0,0 +1,183 @@
from __future__ import print_function, division
import os
import curses
import numpy as np
import time
from threading import Thread
COLUMNS = [
('call', 10),
('lat', 10),
('lon', 10),
('alt', 7),
('gs', 5),
('tas', 5),
('ias', 5),
('mach', 7),
('roc', 7),
('trk', 10),
('hdg', 10),
('live', 6),
]
UNCERTAINTY_COLUMNS = [
('|', 5),
('ver', 4),
('HPL', 5),
('RCu', 5),
('RCv', 5),
('HVE', 5),
('VVE', 5),
('Rc', 4),
('VPL', 5),
('EPU', 5),
('VEPU', 6),
('HFOMr', 7),
('VFOMr', 7),
('PE_RCu', 8),
('PE_VPL', 8),
]
class Screen(Thread):
def __init__(self, uncertainty=False):
Thread.__init__(self)
self.screen = curses.initscr()
curses.noecho()
curses.mousemask(1)
self.screen.keypad(True)
self.y = 3
self.x = 1
self.offset = 0
self.acs = {}
self.lock_icao = None
self.columns = COLUMNS
if uncertainty:
self.columns.extend(UNCERTAINTY_COLUMNS)
def reset_cursor_pos(self):
self.screen.move(self.y, self.x)
def update_data(self, acs):
self.acs = acs
def draw_frame(self):
self.screen.border(0)
self.screen.addstr(0, 2, "Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)" % len(self.acs))
def update(self):
if len(self.acs) == 0:
return
resized = curses.is_term_resized(self.scr_h, self.scr_w)
if resized is True:
self.scr_h, self.scr_w = self.screen.getmaxyx()
self.screen.clear()
curses.resizeterm(self.scr_h, self.scr_w)
self.screen.refresh()
self.draw_frame()
row = 1
header = ' icao'
for c, cw in self.columns:
header += (cw-len(c))*' ' + c
# fill end with spaces
header += (self.scr_w - 2 - len(header)) * ' '
if len(header) > self.scr_w - 2:
header = header[:self.scr_w-3] + '>'
self.screen.addstr(row, 1, header)
row +=1
self.screen.addstr(row, 1, '-'*(self.scr_w-2))
icaos = np.array(list(self.acs.keys()))
icaos = np.sort(icaos)
for row in range(3, self.scr_h - 3):
icao = None
idx = row + self.offset - 3
if idx > len(icaos) - 1:
line = ' '*(self.scr_w-2)
else:
line = ''
icao = icaos[idx]
ac = self.acs[icao]
line += icao
for c, cw in self.columns:
if c=='|':
val = '|'
elif c=='live':
val = str(int(time.time() - ac[c]))+'s'
elif ac[c] is None:
val = ''
else:
val = ac[c]
val_str = str(val)
line += (cw-len(val_str))*' ' + val_str
# fill end with spaces
line += (self.scr_w - 2 - len(line)) * ' '
if len(line) > self.scr_w - 2:
line = line[:self.scr_w-3] + '>'
if (icao is not None) and (self.lock_icao == icao):
self.screen.addstr(row, 1, line, curses.A_STANDOUT)
elif row == self.y:
self.screen.addstr(row, 1, line, curses.A_BOLD)
else:
self.screen.addstr(row, 1, line)
self.screen.addstr(self.scr_h-3, 1, '-'*(self.scr_w-2))
total_page = len(icaos) // (self.scr_h - 4) + 1
current_page = self.offset // (self.scr_h - 4) + 1
self.screen.addstr(self.scr_h-2, 1, '(%d / %d)' % (current_page, total_page))
self.reset_cursor_pos()
def run(self):
self.draw_frame()
self.scr_h, self.scr_w = self.screen.getmaxyx()
while True:
c = self.screen.getch()
if c == curses.KEY_HOME:
self.x = 1
self.y = 1
elif c == curses.KEY_NPAGE:
offset_intent = self.offset + (self.scr_h - 4)
if offset_intent < len(self.acs) - 5:
self.offset = offset_intent
elif c == curses.KEY_PPAGE:
offset_intent = self.offset - (self.scr_h - 4)
if offset_intent > 0:
self.offset = offset_intent
else:
self.offset = 0
elif c == curses.KEY_DOWN :
y_intent = self.y + 1
if y_intent < self.scr_h - 3:
self.y = y_intent
elif c == curses.KEY_UP:
y_intent = self.y - 1
if y_intent > 2:
self.y = y_intent
elif c == curses.KEY_ENTER or c == 10 or c == 13:
self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode()
elif c == curses.KEY_F5:
self.screen.refresh()
self.draw_frame()

255
pyModeS/streamer/stream.py Normal file
View File

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

View File

@@ -1,97 +0,0 @@
# Copyright (C) 2015 Junzi Sun (TU Delft)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Common functions for ADS-B and Mode-S EHS decoder
"""
import math
# the polynominal generattor code for CRC
GENERATOR = "1111111111111010000001001"
def hex2bin(hexstr):
"""Convert a hexdecimal string to binary string, with zero fillings. """
scale = 16
num_of_bits = len(hexstr) * math.log(scale, 2)
binstr = bin(int(hexstr, scale))[2:].zfill(int(num_of_bits))
return binstr
def bin2int(binstr):
"""Convert a binary string to integer. """
return int(binstr, 2)
def hex2int(hexstr):
"""Convert a hexdecimal string to integer. """
return int(hexstr, 16)
def df(msg):
"""Decode Downlink Format vaule, bits 1 to 5."""
msgbin = hex2bin(msg)
return bin2int(msgbin[0:5])
def crc(msg, encode=False):
"""Mode-S Cyclic Redundancy Check
Detect if bit error occurs in the Mode-S message
Args:
msg (string): 28 bytes hexadecimal message string
encode (bool): True to encode the date only and return the checksum
Returns:
string: message checksum, or partity bits (encoder)
"""
msgbin = list(hex2bin(msg))
if encode:
msgbin[-24:] = ['0'] * 24
# loop all bits, except last 24 piraty bits
for i in range(len(msgbin)-24):
# if 1, perform modulo 2 multiplication,
if msgbin[i] == '1':
for j in range(len(GENERATOR)):
# modulo 2 multiplication = XOR
msgbin[i+j] = str((int(msgbin[i+j]) ^ int(GENERATOR[j])))
# last 24 bits
reminder = ''.join(msgbin[-24:])
return reminder
def floor(x):
""" Mode-S floor function
Defined as the greatest integer value k, such that k <= x
eg.: floor(3.6) = 3, while floor(-3.6) = -4
"""
return int(math.floor(x))
def gray2int(graystr):
"""Convert greycode to binary (DF4, 20 altitude coding)"""
num = bin2int(graystr)
num ^= (num >> 8)
num ^= (num >> 4)
num ^= (num >> 2)
num ^= (num >> 1)
return num

View File

@@ -14,6 +14,7 @@ Steps for deploying a new verison:
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# To use a consistent encoding
from codecs import open
from os import path
@@ -21,73 +22,59 @@ 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.2.2',
description='Python Mode-S Decoder',
version="2.2",
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 :: Build Tools",
# 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 :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
],
# What does your project relate to?
keywords='Mode-S ADS-B EHS decoding',
keywords="Mode-S ADS-B EHS decoding",
# You can just specify the packages manually here if your project is
# simple. Or you can use find_packages().
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
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"],
# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,
# for example:
@@ -96,20 +83,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.
@@ -118,4 +102,5 @@ setup(
# 'sample=sample:main',
# ],
# },
scripts=["pyModeS/streamer/modeslive"],
)

View File

@@ -1,5 +1,5 @@
from __future__ import print_function
from pyModeS import adsb, ehs, util
from pyModeS import adsb, ehs
# === Decode sample data file ===

75
tests/sample_run_commb.py Normal file
View File

@@ -0,0 +1,75 @@
from __future__ import print_function
from pyModeS import commb, common, bds
# === Decode sample data file ===
def bds_info(BDS, m):
if BDS == "BDS10":
info = [commb.ovc10(m)]
elif BDS == "BDS17":
info = ([i[-2:] for i in commb.cap17(m)])
elif BDS == "BDS20":
info = [commb.cs20(m)]
elif BDS == "BDS40":
info = (commb.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)

View File

@@ -1,75 +0,0 @@
from __future__ import print_function
from pyModeS import adsb, ehs, util
# === Decode sample data file ===
def bds_info(BDS, m):
if BDS == "BDS17":
info = ([i[-2:] for i in ehs.cap17(m)])
elif BDS == "BDS20":
info = ehs.callsign(m)
elif BDS == "BDS40":
info = (ehs.alt40mcp(m), ehs.alt40fms(m), ehs.p40baro(m))
elif BDS == "BDS44":
info = (ehs.wind44(m), ehs.temp44(m), ehs.p44(m), ehs.hum44(m))
elif BDS == "BDS44REV":
info = (ehs.wind44(m, rev=True), ehs.temp44(m, rev=True), ehs.p44(m, rev=True), ehs.hum44(m, rev=True))
elif BDS == "BDS50":
info = (ehs.roll50(m), ehs.trk50(m), ehs.gs50(m), ehs.rtrk50(m), ehs.tas50(m))
elif BDS == "BDS53":
info = (ehs.hdg53(m), ehs.ias53(m), ehs.mach53(m), ehs.tas53(m), ehs.vr53(m))
elif BDS == "BDS60":
info = (ehs.hdg60(m), ehs.ias60(m), ehs.mach60(m), ehs.vr60baro(m), ehs.vr60ins(m))
else:
info = None
return info
def ehs_decode_all(df, n=None):
import csv
print("===== Decode EHS sample data (DF=%s)=====" % df)
f = open('tests/data/sample_data_ehs_df%s.csv' % df, 'rt')
for i, r in enumerate(csv.reader(f)):
if n and i > n:
break
ts = r[0]
m = r[2]
df = util.df(m)
icao = ehs.icao(m)
BDS = ehs.BDS(m)
code = ehs.df20alt(m) if df==20 else ehs.df21id(m)
if not BDS:
print(ts, m, icao, df, '%5s'%code, 'UNKNOWN')
continue
if isinstance(BDS, list):
print(ts, m, icao, df, '%5s'%code, end=' ')
for i, bds in enumerate(BDS):
if i == 0:
print(bds, *bds_info(bds, m))
else:
print(' '*55, bds, *bds_info(bds, m))
else:
print(ts, m, icao, df, '%5s'%code, BDS, *bds_info(BDS, m))
if __name__ == '__main__':
ehs_decode_all(df=20, n=100)
ehs_decode_all(df=21, n=100)

View File

@@ -58,22 +58,22 @@ def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, 182.9, -832, 'GS')
assert vas == (376, 244.0, -2304, 'AS')
assert vgs == (159, 182.88, -832, 'GS')
assert vas == (375, 243.98, -2304, 'TAS')
assert vgs_surface == (19.0, 42.2, 0 , 'GS')
assert adsb.altitude_diff('8D485020994409940838175B284F') == 550
def test_nic():
assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11
# def test_nic():
# assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0
# assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1
# assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2
# assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3
# assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4
# assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5
# assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6
# assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7
# assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8
# assert adsb.nic('8F48511C598D04F12CCF82451642') == 9
# assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10
# assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11

View File

@@ -0,0 +1,20 @@
from pyModeS import bds
def test_bds_infer():
assert bds.infer("8D406B902015A678D4D220AA4BDA") == 'BDS08'
assert bds.infer("8FC8200A3AB8F5F893096B000000") == 'BDS06'
assert bds.infer("8D40058B58C901375147EFD09357") == 'BDS05'
assert bds.infer("8D485020994409940838175B284F") == 'BDS09'
assert bds.infer("A800178D10010080F50000D5893C") == 'BDS10'
assert bds.infer("A0000638FA81C10000000081A92F") == 'BDS17'
assert bds.infer("A0001838201584F23468207CDFA5") == 'BDS20'
assert bds.infer("A0001839CA3800315800007448D9") == 'BDS40'
assert bds.infer("A000139381951536E024D4CCF6B5") == 'BDS50'
assert bds.infer("A00004128F39F91A7E27C46ADC21") == 'BDS60'
def test_bds_is50or60():
assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None
assert bds.is50or60("A0000000FFDA9517000464000000", 182, 237, 1250) == 'BDS50'
assert bds.is50or60("A0000000919A5927E23444000000", 413, 54, 18700) == 'BDS60'

49
tests/test_commb.py Normal file
View File

@@ -0,0 +1,49 @@
from pyModeS import bds, commb
# from pyModeS import ehs, els # deprecated
def test_bds20_callsign():
assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
assert commb.cs20("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert commb.cs20("A0001993202422F2E37CE038738E") == 'IBK2873_'
def test_bds40_functions():
assert bds.bds40.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

58
tests/test_common.py Normal file
View File

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

View File

@@ -1,62 +0,0 @@
from pyModeS import ehs
from pyModeS import modes_common
def test_ehs_icao():
assert ehs.icao("A0001839CA3800315800007448D9") == '400940'
assert ehs.icao("A000139381951536E024D4CCF6B5") == '3C4DD2'
assert ehs.icao("A000029CFFBAA11E2004727281F1") == '4243D0'
def test_df20alt():
assert ehs.df20alt("A02014B400000000000000F9D514") == 32300
def test_ehs_BDS():
assert ehs.BDS("A0001838201584F23468207CDFA5") == 'BDS20'
assert ehs.BDS("A0001839CA3800315800007448D9") == 'BDS40'
# assert ehs.BDS("A000031DBAA9DD18622C441330E9") == 'BDS44'
assert ehs.BDS("A000139381951536E024D4CCF6B5") == 'BDS50'
assert ehs.BDS("A00004128F39F91A7E27C46ADC21") == 'BDS60'
def test_ehs_BDS20_callsign():
assert ehs.callsign("A000083E202CC371C31DE0AA1CCF") == 'KLM1017_'
assert ehs.callsign("A0001993202422F2E37CE038738E") == 'IBK2873_'
def test_ehs_BDS40_functions():
assert ehs.alt40mcp("A000029C85E42F313000007047D3") == 3008
assert ehs.alt40fms("A000029C85E42F313000007047D3") == 3008
assert ehs.p40baro("A000029C85E42F313000007047D3") == 1020.0
def test_ehs_BDS50_functions():
assert ehs.roll50("A000139381951536E024D4CCF6B5") == 2.1
assert ehs.trk50("A000139381951536E024D4CCF6B5") == 114.3
assert ehs.gs50("A000139381951536E024D4CCF6B5") == 438
assert ehs.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
assert ehs.tas50("A000139381951536E024D4CCF6B5") == 424
# signed values
assert ehs.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4
def test_ehs_BDS60_functions():
assert ehs.hdg60("A00004128F39F91A7E27C46ADC21") == 42.7
assert ehs.ias60("A00004128F39F91A7E27C46ADC21") == 252
assert ehs.mach60("A00004128F39F91A7E27C46ADC21") == 0.42
assert ehs.vr60baro("A00004128F39F91A7E27C46ADC21") == -1920
assert ehs.vr60ins("A00004128F39F91A7E27C46ADC21") == -1920
def test_graycode_to_altitude():
assert modes_common.gray2alt('00000000010') == -1000
assert modes_common.gray2alt('00000001010') == -500
assert modes_common.gray2alt('00000011011') == -100
assert modes_common.gray2alt('00000011010') == 0
assert modes_common.gray2alt('00000011110') == 100
assert modes_common.gray2alt('00000010011') == 600
assert modes_common.gray2alt('00000110010') == 1000
assert modes_common.gray2alt('00001001001') == 5800
assert modes_common.gray2alt('00011100100') == 10300
assert modes_common.gray2alt('01100011010') == 32000
assert modes_common.gray2alt('01110000100') == 46300
assert modes_common.gray2alt('01010101100') == 50200
assert modes_common.gray2alt('11011110100') == 73200
assert modes_common.gray2alt('10000000011') == 126600
assert modes_common.gray2alt('10000000001') == 126700

View File

@@ -1,15 +0,0 @@
from pyModeS import util
def test_hex2bin():
assert util.hex2bin('6E406B') == "011011100100000001101011"
def test_crc_decode():
checksum = util.crc("8D406B902015A678D4D220AA4BDA")
assert checksum == "000000000000000000000000"
def test_crc_encode():
parity = util.crc("8D406B902015A678D4D220AA4BDA", encode=True)
assert util.hex2bin("AA4BDA") == parity

View File

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