├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ └── build-publish.yml ├── .gitignore ├── LICENSE ├── README.rst ├── doc ├── Makefile ├── README.rst ├── modeslive-screenshot.png ├── source │ ├── _templates │ │ └── layout.html │ ├── conf.py │ ├── index.rst │ ├── modules.rst │ ├── pyModeS.c_common.rst │ ├── pyModeS.common.rst │ ├── pyModeS.decoder.acas.rst │ ├── pyModeS.decoder.adsb.rst │ ├── pyModeS.decoder.allcall.rst │ ├── pyModeS.decoder.bds.bds05.rst │ ├── pyModeS.decoder.bds.bds06.rst │ ├── pyModeS.decoder.bds.bds08.rst │ ├── pyModeS.decoder.bds.bds09.rst │ ├── pyModeS.decoder.bds.bds10.rst │ ├── pyModeS.decoder.bds.bds17.rst │ ├── pyModeS.decoder.bds.bds20.rst │ ├── pyModeS.decoder.bds.bds30.rst │ ├── pyModeS.decoder.bds.bds40.rst │ ├── pyModeS.decoder.bds.bds44.rst │ ├── pyModeS.decoder.bds.bds45.rst │ ├── pyModeS.decoder.bds.bds50.rst │ ├── pyModeS.decoder.bds.bds53.rst │ ├── pyModeS.decoder.bds.bds60.rst │ ├── pyModeS.decoder.bds.rst │ ├── pyModeS.decoder.commb.rst │ ├── pyModeS.decoder.rst │ ├── pyModeS.decoder.surv.rst │ ├── pyModeS.decoder.uncertainty.rst │ ├── pyModeS.decoder.uplink.rst │ └── pyModeS.rst └── warnings ├── hatch_build.py ├── pyproject.toml ├── src └── pyModeS │ ├── .gitignore │ ├── __init__.py │ ├── c_common.pxd │ ├── c_common.pyi │ ├── c_common.pyx │ ├── common.pyi │ ├── decoder │ ├── __init__.py │ ├── acas.py │ ├── adsb.py │ ├── allcall.py │ ├── bds │ │ ├── __init__.py │ │ ├── bds05.py │ │ ├── bds06.py │ │ ├── bds08.py │ │ ├── bds09.py │ │ ├── bds10.py │ │ ├── bds17.py │ │ ├── bds20.py │ │ ├── bds30.py │ │ ├── bds40.py │ │ ├── bds44.py │ │ ├── bds45.py │ │ ├── bds50.py │ │ ├── bds53.py │ │ ├── bds60.py │ │ ├── bds61.py │ │ └── bds62.py │ ├── commb.py │ ├── ehs.py │ ├── els.py │ ├── flarm │ │ ├── __init__.py │ │ ├── core.c │ │ ├── core.h │ │ ├── core.pxd │ │ ├── decode.pyi │ │ └── decode.pyx │ ├── surv.py │ ├── uncertainty.py │ └── uplink.py │ ├── extra │ ├── __init__.py │ ├── aero.py │ ├── rtlreader.py │ └── tcpclient.py │ ├── py.typed │ ├── py_common.py │ └── streamer │ ├── __init__.py │ ├── decode.py │ ├── modeslive.py │ ├── screen.py │ └── source.py ├── tests ├── benchmark.py ├── data │ ├── sample_data_adsb.csv │ ├── sample_data_commb_df20.csv │ └── sample_data_commb_df21.csv ├── sample_run_adsb.py ├── sample_run_commb.py ├── test_adsb.py ├── test_allcall.py ├── test_bds_inference.py ├── test_c_common.py ├── test_commb.py ├── test_py_common.py ├── test_surv.py └── test_tell.py └── uv.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = */pyModeS/* 4 | omit = *tests* 5 | 6 | [report] 7 | exclude_lines = 8 | coverage: ignore 9 | raise NotImplementedError 10 | if TYPE_CHECKING: 11 | 12 | ignore_errors = True -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish pyModeS 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | env: 9 | CIBW_BUILD: cp310* cp311* cp312* cp313* 10 | CIBW_ARCHS_WINDOWS: auto64 11 | CIBW_ARCHS_LINUX: auto64 aarch64 12 | CIBW_ARCHS_MACOS: universal2 13 | # CIBW_ARCHS_MACOS: auto universal2 14 | CIBW_TEST_SKIP: "*universal2:arm64" 15 | 16 | jobs: 17 | build_wheels: 18 | name: Build wheels on ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | # macos-13 is an intel runner, macos-14 is apple silicon 23 | os: [ubuntu-latest, windows-latest, macos-14] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Set up QEMU 29 | if: runner.os == 'Linux' && runner.arch == 'X64' 30 | uses: docker/setup-qemu-action@v3 31 | with: 32 | platforms: all 33 | 34 | - name: Build wheels 35 | uses: pypa/cibuildwheel@v2.22.0 36 | 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 40 | path: ./wheelhouse/*.whl 41 | 42 | build_sdist: 43 | name: Build source distribution 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Build sdist 49 | run: pipx run build --sdist 50 | 51 | - uses: actions/upload-artifact@v4 52 | with: 53 | name: cibw-sdist 54 | path: dist/*.tar.gz 55 | 56 | upload_pypi: 57 | needs: [build_wheels, build_sdist] 58 | runs-on: ubuntu-latest 59 | environment: pypi 60 | permissions: 61 | id-token: write 62 | if: github.event_name == 'release' && github.event.action == 'published' 63 | steps: 64 | - uses: actions/download-artifact@v4 65 | with: 66 | # unpacks all CIBW artifacts into dist/ 67 | pattern: cibw-* 68 | path: dist 69 | merge-multiple: true 70 | 71 | - uses: pypa/gh-action-pypi-publish@release/v1 72 | with: 73 | user: __token__ 74 | password: ${{ secrets.PYPI_API_TOKEN_PYMODES }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | .pytest_cache/ 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # PyCharm 60 | .idea/ 61 | 62 | # Environments 63 | .env 64 | .venv 65 | env/ 66 | venv/ 67 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | The Python ADS-B/Mode-S Decoder 2 | =============================== 3 | 4 | PyModeS is a Python library designed to decode Mode-S (including ADS-B) messages. It can be imported to your python project or used as a standalone tool to view and save live traffic data. 5 | 6 | This is a project created by Junzi Sun, who works at `TU Delft `_, `Aerospace Engineering Faculty `_, `CNS/ATM research group `_. It is supported by many `contributors `_ from different institutions. 7 | 8 | Introduction 9 | ------------ 10 | 11 | pyModeS supports the decoding of following types of messages: 12 | 13 | - DF4 / DF20: Altitude code 14 | - DF5 / DF21: Identity code (squawk code) 15 | 16 | - DF17 / DF18: Automatic Dependent Surveillance-Broadcast (ADS-B) 17 | 18 | - TC=1-4 / BDS 0,8: Aircraft identification and category 19 | - TC=5-8 / BDS 0,6: Surface position 20 | - TC=9-18 / BDS 0,5: Airborne position 21 | - TC=19 / BDS 0,9: Airborne velocity 22 | - TC=28 / BDS 6,1: Airborne status [to be implemented] 23 | - TC=29 / BDS 6,2: Target state and status information [to be implemented] 24 | - TC=31 / BDS 6,5: Aircraft operational status [to be implemented] 25 | 26 | - DF20 / DF21: Mode-S Comm-B messages 27 | 28 | - BDS 1,0: Data link capability report 29 | - BDS 1,7: Common usage GICB capability report 30 | - BDS 2,0: Aircraft identification 31 | - BDS 3,0: ACAS active resolution advisory 32 | - BDS 4,0: Selected vertical intention 33 | - BDS 4,4: Meteorological routine air report (experimental) 34 | - BDS 4,5: Meteorological hazard report (experimental) 35 | - BDS 5,0: Track and turn report 36 | - BDS 6,0: Heading and speed report 37 | 38 | 39 | 40 | If you find this project useful for your research, please considering cite this tool as:: 41 | 42 | @article{sun2019pymodes, 43 | author={J. {Sun} and H. {V\^u} and J. {Ellerbroek} and J. M. {Hoekstra}}, 44 | journal={IEEE Transactions on Intelligent Transportation Systems}, 45 | title={pyModeS: Decoding Mode-S Surveillance Data for Open Air Transportation Research}, 46 | year={2019}, 47 | doi={10.1109/TITS.2019.2914770}, 48 | ISSN={1524-9050}, 49 | } 50 | 51 | 52 | 53 | 54 | Resources 55 | ----------- 56 | Check out and contribute to this open-source project at: 57 | https://github.com/junzis/pyModeS 58 | 59 | Detailed manual on Mode-S decoding is published at: 60 | https://mode-s.org/decode 61 | 62 | The API documentation of pyModeS is at: 63 | https://mode-s.org/api 64 | 65 | 66 | 67 | Basic installation 68 | ------------------- 69 | 70 | Installation examples:: 71 | 72 | # stable version 73 | pip install pyModeS 74 | 75 | # conda (compiled) version 76 | conda install -c conda-forge pymodes 77 | 78 | # development version 79 | pip install git+https://github.com/junzis/pyModeS 80 | 81 | 82 | Dependencies ``numpy``, and ``pyzmq`` are installed automatically during previous installations processes. 83 | 84 | If you need to connect pyModeS to a RTL-SDR receiver, ``pyrtlsdr`` need to be installed manually:: 85 | 86 | pip install pyrtlsdr 87 | 88 | 89 | Advanced installation (using c modules) 90 | ------------------------------------------ 91 | 92 | If you want to make use of the (faster) c module, install ``pyModeS`` as follows:: 93 | 94 | # conda (compiled) version 95 | conda install -c conda-forge pymodes 96 | 97 | # stable version 98 | pip install pyModeS 99 | 100 | # development version 101 | git clone https://github.com/junzis/pyModeS 102 | cd pyModeS 103 | poetry install -E rtlsdr 104 | 105 | 106 | View live traffic (modeslive) 107 | ---------------------------------------------------- 108 | 109 | General usage:: 110 | 111 | $ modeslive [-h] --source SOURCE [--connect SERVER PORT DATAYPE] 112 | [--latlon LAT LON] [--show-uncertainty] [--dumpto DUMPTO] 113 | 114 | arguments: 115 | -h, --help show this help message and exit 116 | --source SOURCE Choose data source, "rtlsdr" or "net" 117 | --connect SERVER PORT DATATYPE 118 | Define server, port and data type. Supported data 119 | types are: ['raw', 'beast', 'skysense'] 120 | --latlon LAT LON Receiver latitude and longitude, needed for the surface 121 | position, default none 122 | --show-uncertainty Display uncertainty values, default off 123 | --dumpto DUMPTO Folder to dump decoded output, default none 124 | 125 | 126 | Live with RTL-SDR 127 | ******************* 128 | 129 | If you have an RTL-SDR receiver connected to your computer, you can use the ``rtlsdr`` source switch (require ``pyrtlsdr`` package), with command:: 130 | 131 | $ modeslive --source rtlsdr 132 | 133 | 134 | Live with network data 135 | *************************** 136 | 137 | If you want to connect to a TCP server that broadcast raw data. use can use ``net`` source switch, for example:: 138 | 139 | $ modeslive --source net --connect localhost 30002 raw 140 | $ modeslive --source net --connect 127.0.0.1 30005 beast 141 | 142 | 143 | 144 | Example screenshot: 145 | 146 | .. image:: https://github.com/junzis/pyModeS/raw/master/doc/modeslive-screenshot.png 147 | :width: 700px 148 | 149 | 150 | Use the library 151 | --------------- 152 | 153 | .. code:: python 154 | 155 | import pyModeS as pms 156 | 157 | 158 | Common functions 159 | ***************** 160 | 161 | .. code:: python 162 | 163 | pms.df(msg) # Downlink Format 164 | pms.icao(msg) # Infer the ICAO address from the message 165 | pms.crc(msg, encode=False) # Perform CRC or generate parity bit 166 | 167 | pms.hex2bin(str) # Convert hexadecimal string to binary string 168 | pms.bin2int(str) # Convert binary string to integer 169 | pms.hex2int(str) # Convert hexadecimal string to integer 170 | pms.gray2int(str) # Convert grey code to integer 171 | 172 | 173 | Core functions for ADS-B decoding 174 | ********************************* 175 | 176 | .. code:: python 177 | 178 | pms.adsb.icao(msg) 179 | pms.adsb.typecode(msg) 180 | 181 | # Typecode 1-4 182 | pms.adsb.callsign(msg) 183 | 184 | # Typecode 5-8 (surface), 9-18 (airborne, barometric height), and 20-22 (airborne, GNSS height) 185 | pms.adsb.position(msg_even, msg_odd, t_even, t_odd, lat_ref=None, lon_ref=None) 186 | pms.adsb.airborne_position(msg_even, msg_odd, t_even, t_odd) 187 | pms.adsb.surface_position(msg_even, msg_odd, t_even, t_odd, lat_ref, lon_ref) 188 | pms.adsb.surface_velocity(msg) 189 | 190 | pms.adsb.position_with_ref(msg, lat_ref, lon_ref) 191 | pms.adsb.airborne_position_with_ref(msg, lat_ref, lon_ref) 192 | pms.adsb.surface_position_with_ref(msg, lat_ref, lon_ref) 193 | 194 | pms.adsb.altitude(msg) 195 | 196 | # Typecode: 19 197 | pms.adsb.velocity(msg) # Handles both surface & airborne messages 198 | pms.adsb.speed_heading(msg) # Handles both surface & airborne messages 199 | pms.adsb.airborne_velocity(msg) 200 | 201 | 202 | Note: When you have a fix position of the aircraft, it is convenient to use `position_with_ref()` method to decode with only one position message (either odd or even). This works with both airborne and surface position messages. But the reference position shall be within 180NM (airborne) or 45NM (surface) of the true position. 203 | 204 | 205 | Decode altitude replies in DF4 / DF20 206 | ************************************** 207 | .. code:: python 208 | 209 | pms.common.altcode(msg) # Downlink format must be 4 or 20 210 | 211 | 212 | Decode identity replies in DF5 / DF21 213 | ************************************** 214 | .. code:: python 215 | 216 | pms.common.idcode(msg) # Downlink format must be 5 or 21 217 | 218 | 219 | 220 | Common Mode-S functions 221 | ************************ 222 | 223 | .. code:: python 224 | 225 | pms.icao(msg) # Infer the ICAO address from the message 226 | pms.bds.infer(msg) # Infer the Modes-S BDS register 227 | 228 | # Check if BDS is 5,0 or 6,0, give reference speed, track, altitude (from ADS-B) 229 | pms.bds.is50or60(msg, spd_ref, trk_ref, alt_ref) 230 | 231 | # Check each BDS explicitly 232 | pms.bds.bds10.is10(msg) 233 | pms.bds.bds17.is17(msg) 234 | pms.bds.bds20.is20(msg) 235 | pms.bds.bds30.is30(msg) 236 | pms.bds.bds40.is40(msg) 237 | pms.bds.bds44.is44(msg) 238 | pms.bds.bds50.is50(msg) 239 | pms.bds.bds60.is60(msg) 240 | 241 | 242 | 243 | Mode-S Elementary Surveillance (ELS) 244 | ************************************* 245 | 246 | .. code:: python 247 | 248 | pms.commb.ovc10(msg) # Overlay capability, BDS 1,0 249 | pms.commb.cap17(msg) # GICB capability, BDS 1,7 250 | pms.commb.cs20(msg) # Callsign, BDS 2,0 251 | 252 | 253 | Mode-S Enhanced Surveillance (EHS) 254 | *********************************** 255 | 256 | .. code:: python 257 | 258 | # BDS 4,0 259 | pms.commb.selalt40mcp(msg) # MCP/FCU selected altitude (ft) 260 | pms.commb.selalt40fms(msg) # FMS selected altitude (ft) 261 | pms.commb.p40baro(msg) # Barometric pressure (mb) 262 | 263 | # BDS 5,0 264 | pms.commb.roll50(msg) # Roll angle (deg) 265 | pms.commb.trk50(msg) # True track angle (deg) 266 | pms.commb.gs50(msg) # Ground speed (kt) 267 | pms.commb.rtrk50(msg) # Track angle rate (deg/sec) 268 | pms.commb.tas50(msg) # True airspeed (kt) 269 | 270 | # BDS 6,0 271 | pms.commb.hdg60(msg) # Magnetic heading (deg) 272 | pms.commb.ias60(msg) # Indicated airspeed (kt) 273 | pms.commb.mach60(msg) # Mach number (-) 274 | pms.commb.vr60baro(msg) # Barometric altitude rate (ft/min) 275 | pms.commb.vr60ins(msg) # Inertial vertical speed (ft/min) 276 | 277 | 278 | Meteorological reports [Experimental] 279 | ************************************** 280 | 281 | To identify BDS 4,4 and 4,5 codes, you must set ``mrar`` argument to ``True`` in the ``infer()`` function: 282 | 283 | .. code:: python 284 | 285 | pms.bds.infer(msg. mrar=True) 286 | 287 | Once the correct MRAR and MHR messages are identified, decode them as follows: 288 | 289 | 290 | Meteorological routine air report (MRAR) 291 | +++++++++++++++++++++++++++++++++++++++++ 292 | 293 | .. code:: python 294 | 295 | # BDS 4,4 296 | pms.commb.wind44(msg) # Wind speed (kt) and direction (true) (deg) 297 | pms.commb.temp44(msg) # Static air temperature (C) 298 | pms.commb.p44(msg) # Average static pressure (hPa) 299 | pms.commb.hum44(msg) # Humidity (%) 300 | 301 | 302 | Meteorological hazard air report (MHR) 303 | +++++++++++++++++++++++++++++++++++++++++ 304 | 305 | .. code:: python 306 | 307 | # BDS 4,5 308 | pms.commb.turb45(msg) # Turbulence level (0-3) 309 | pms.commb.ws45(msg) # Wind shear level (0-3) 310 | pms.commb.mb45(msg) # Microburst level (0-3) 311 | pms.commb.ic45(msg) # Icing level (0-3) 312 | pms.commb.wv45(msg) # Wake vortex level (0-3) 313 | pms.commb.temp45(msg) # Static air temperature (C) 314 | pms.commb.p45(msg) # Average static pressure (hPa) 315 | pms.commb.rh45(msg) # Radio height (ft) 316 | 317 | 318 | 319 | Customize the streaming module 320 | ****************************** 321 | The TCP client module from pyModeS can be re-used to stream and process Mode-S data as you like. You need to re-implement the ``handle_messages()`` function from the ``TcpClient`` class to write your own logic to handle the messages. 322 | 323 | Here is an example: 324 | 325 | .. code:: python 326 | 327 | import pyModeS as pms 328 | from pyModeS.extra.tcpclient import TcpClient 329 | 330 | # define your custom class by extending the TcpClient 331 | # - implement your handle_messages() methods 332 | class ADSBClient(TcpClient): 333 | def __init__(self, host, port, rawtype): 334 | super(ADSBClient, self).__init__(host, port, rawtype) 335 | 336 | def handle_messages(self, messages): 337 | for msg, ts in messages: 338 | if len(msg) != 28: # wrong data length 339 | continue 340 | 341 | df = pms.df(msg) 342 | 343 | if df != 17: # not ADSB 344 | continue 345 | 346 | if pms.crc(msg) !=0: # CRC fail 347 | continue 348 | 349 | icao = pms.adsb.icao(msg) 350 | tc = pms.adsb.typecode(msg) 351 | 352 | # TODO: write you magic code here 353 | print(ts, icao, tc, msg) 354 | 355 | # run new client, change the host, port, and rawtype if needed 356 | client = ADSBClient(host='127.0.0.1', port=30005, rawtype='beast') 357 | client.run() 358 | 359 | 360 | Unit test 361 | --------- 362 | 363 | .. code:: bash 364 | 365 | uv sync --dev --all-extras 366 | uv run pytest 367 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | rm -f source/pyModeS*.rst source/modules.rst 20 | sphinx-apidoc -f -e -M -o source/ ../pyModeS ../pyModeS/decoder/ehs.py ../pyModeS/decoder/els.py ../pyModeS/streamer ../pyModeS/extra 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /doc/README.rst: -------------------------------------------------------------------------------- 1 | How to generate the apidoc 2 | ==================================== 3 | 4 | :: 5 | 6 | cd doc 7 | make html 8 | -------------------------------------------------------------------------------- /doc/modeslive-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junzis/pyModeS/6401a3963c1f7a11884b64e23fa2f7b0c72f2b2e/doc/modeslive-screenshot.png -------------------------------------------------------------------------------- /doc/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | 6 | 7 | 8 | 15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath("../..")) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "pyModeS" 24 | copyright = "2019, Junzi Sun" 25 | author = "Junzi Sun" 26 | 27 | # The short X.Y version 28 | version = "" 29 | # The full version, including alpha/beta/rc tags 30 | release = "" 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | "sphinx.ext.autodoc", 44 | "sphinx.ext.mathjax", 45 | "sphinx.ext.viewcode", 46 | "sphinx.ext.githubpages", 47 | "sphinx.ext.napoleon", 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = ".rst" 58 | 59 | # The master toctree document. 60 | master_doc = "index" 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This pattern also affects html_static_path and html_extra_path. 72 | exclude_patterns = [] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = None 76 | 77 | 78 | # -- Options for HTML output ------------------------------------------------- 79 | 80 | # The theme to use for HTML and HTML Help pages. See the documentation for 81 | # a list of builtin themes. 82 | # 83 | # html_theme = 'alabaster' 84 | html_theme = "neo_rtd_theme" 85 | import sphinx_theme 86 | 87 | html_theme_path = [sphinx_theme.get_html_theme_path()] 88 | 89 | # Theme options are theme-specific and customize the look and feel of a theme 90 | # further. For a list of options available for each theme, see the 91 | # documentation. 92 | # 93 | # html_theme_options = {} 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | # html_static_path = [''] 99 | 100 | # Custom sidebar templates, must be a dictionary that maps document names 101 | # to template names. 102 | # 103 | # The default sidebars (for documents that don't match any pattern) are 104 | # defined by theme itself. Builtin themes are using these templates by 105 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 106 | # 'searchbox.html']``. 107 | # 108 | # html_sidebars = {} 109 | 110 | 111 | # -- Options for HTMLHelp output --------------------------------------------- 112 | 113 | # Output file base name for HTML help builder. 114 | htmlhelp_basename = "pyModeSdoc" 115 | 116 | 117 | # -- Options for LaTeX output ------------------------------------------------ 118 | 119 | latex_elements = { 120 | # The paper size ('letterpaper' or 'a4paper'). 121 | # 122 | # 'papersize': 'letterpaper', 123 | # The font size ('10pt', '11pt' or '12pt'). 124 | # 125 | # 'pointsize': '10pt', 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | # Latex figure (float) alignment 130 | # 131 | # 'figure_align': 'htbp', 132 | } 133 | 134 | # Grouping the document tree into LaTeX files. List of tuples 135 | # (source start file, target name, title, 136 | # author, documentclass [howto, manual, or own class]). 137 | latex_documents = [ 138 | (master_doc, "pyModeS.tex", "pyModeS Documentation", "Junzi Sun", "manual") 139 | ] 140 | 141 | 142 | # -- Options for manual page output ------------------------------------------ 143 | 144 | # One entry per manual page. List of tuples 145 | # (source start file, name, description, authors, manual section). 146 | man_pages = [(master_doc, "pymodes", "pyModeS Documentation", [author], 1)] 147 | 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | ( 156 | master_doc, 157 | "pyModeS", 158 | "pyModeS Documentation", 159 | author, 160 | "pyModeS", 161 | "One line description of project.", 162 | "Miscellaneous", 163 | ) 164 | ] 165 | 166 | 167 | # -- Options for Epub output ------------------------------------------------- 168 | 169 | # Bibliographic Dublin Core info. 170 | epub_title = project 171 | 172 | # The unique identifier of the text. This can be a ISBN number 173 | # or the project homepage. 174 | # 175 | # epub_identifier = '' 176 | 177 | # A unique identification for the text. 178 | # 179 | # epub_uid = '' 180 | 181 | # A list of files that should not be packed into the epub file. 182 | epub_exclude_files = ["search.html"] 183 | 184 | 185 | # -- Extension configuration ------------------------------------------------- 186 | 187 | # -- Options for todo extension ---------------------------------------------- 188 | 189 | # If true, `todo` and `todoList` produce output, else they produce nothing. 190 | todo_include_todos = True 191 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyModeS documentation master file, created by 2 | sphinx-quickstart on Mon Apr 1 13:13:10 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyModeS documentation! 7 | =================================== 8 | 9 | The source code can be found at: https://github.com/junzis/pyModeS 10 | 11 | .. toctree:: 12 | :caption: Core modules 13 | :maxdepth: 2 14 | 15 | pyModeS.decoder.adsb 16 | pyModeS.decoder.commb 17 | 18 | 19 | .. toctree:: 20 | :caption: ADS-B messages 21 | :maxdepth: 2 22 | 23 | pyModeS.decoder.bds.bds05 24 | pyModeS.decoder.bds.bds06 25 | pyModeS.decoder.bds.bds08 26 | pyModeS.decoder.bds.bds09 27 | 28 | 29 | .. toctree:: 30 | :caption: ELS - elementary surveillance 31 | :maxdepth: 2 32 | 33 | pyModeS.decoder.bds.bds10 34 | pyModeS.decoder.bds.bds17 35 | pyModeS.decoder.bds.bds20 36 | pyModeS.decoder.bds.bds30 37 | 38 | 39 | .. toctree:: 40 | :caption: EHS - enhanced surveillance 41 | :maxdepth: 2 42 | 43 | pyModeS.decoder.bds.bds40 44 | pyModeS.decoder.bds.bds50 45 | pyModeS.decoder.bds.bds60 46 | 47 | 48 | .. toctree:: 49 | :caption: MRAR / MHR 50 | :maxdepth: 2 51 | 52 | pyModeS.decoder.bds.bds44 53 | pyModeS.decoder.bds.bds45 54 | 55 | 56 | 57 | ---- 58 | 59 | .. include:: ../../README.rst 60 | 61 | ---- 62 | 63 | Indices and tables 64 | ********************** 65 | 66 | * :ref:`genindex` 67 | * :ref:`modindex` 68 | * :ref:`search` 69 | -------------------------------------------------------------------------------- /doc/source/modules.rst: -------------------------------------------------------------------------------- 1 | pyModeS 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyModeS 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.c_common.rst: -------------------------------------------------------------------------------- 1 | pyModeS.c\_common module 2 | ======================== 3 | 4 | .. automodule:: pyModeS.c_common 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.common.rst: -------------------------------------------------------------------------------- 1 | pyModeS.common module 2 | ===================== 3 | 4 | .. automodule:: pyModeS.common 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.acas.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.acas module 2 | =========================== 3 | 4 | .. automodule:: pyModeS.decoder.acas 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.adsb.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.adsb module 2 | =========================== 3 | 4 | .. automodule:: pyModeS.decoder.adsb 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.allcall.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.allcall module 2 | ============================== 3 | 4 | .. automodule:: pyModeS.decoder.allcall 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds05.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds05 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds05 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds06.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds06 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds06 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds08.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds08 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds08 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds09.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds09 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds09 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds10.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds10 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds10 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds17.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds17 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds17 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds20.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds20 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds20 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds30.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds30 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds30 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds40.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds40 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds40 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds44.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds44 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds44 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds45.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds45 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds45 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds50.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds50 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds50 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds53.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds53 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds53 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.bds60.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds.bds60 module 2 | ================================ 3 | 4 | .. automodule:: pyModeS.decoder.bds.bds60 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.bds.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.bds package 2 | =========================== 3 | 4 | .. automodule:: pyModeS.decoder.bds 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | pyModeS.decoder.bds.bds05 16 | pyModeS.decoder.bds.bds06 17 | pyModeS.decoder.bds.bds08 18 | pyModeS.decoder.bds.bds09 19 | pyModeS.decoder.bds.bds10 20 | pyModeS.decoder.bds.bds17 21 | pyModeS.decoder.bds.bds20 22 | pyModeS.decoder.bds.bds30 23 | pyModeS.decoder.bds.bds40 24 | pyModeS.decoder.bds.bds44 25 | pyModeS.decoder.bds.bds45 26 | pyModeS.decoder.bds.bds50 27 | pyModeS.decoder.bds.bds53 28 | pyModeS.decoder.bds.bds60 29 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.commb.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.commb module 2 | ============================ 3 | 4 | .. automodule:: pyModeS.decoder.commb 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder package 2 | ======================= 3 | 4 | .. automodule:: pyModeS.decoder 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | pyModeS.decoder.bds 16 | 17 | Submodules 18 | ---------- 19 | 20 | .. toctree:: 21 | :maxdepth: 4 22 | 23 | pyModeS.decoder.acas 24 | pyModeS.decoder.adsb 25 | pyModeS.decoder.allcall 26 | pyModeS.decoder.commb 27 | pyModeS.decoder.surv 28 | pyModeS.decoder.uncertainty 29 | pyModeS.decoder.uplink 30 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.surv.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.surv module 2 | =========================== 3 | 4 | .. automodule:: pyModeS.decoder.surv 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.uncertainty.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.uncertainty module 2 | ================================== 3 | 4 | .. automodule:: pyModeS.decoder.uncertainty 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.decoder.uplink.rst: -------------------------------------------------------------------------------- 1 | pyModeS.decoder.uplink module 2 | ============================= 3 | 4 | .. automodule:: pyModeS.decoder.uplink 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/source/pyModeS.rst: -------------------------------------------------------------------------------- 1 | pyModeS package 2 | =============== 3 | 4 | .. automodule:: pyModeS 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | pyModeS.decoder 16 | 17 | Submodules 18 | ---------- 19 | 20 | .. toctree:: 21 | :maxdepth: 4 22 | 23 | pyModeS.c_common 24 | pyModeS.common 25 | -------------------------------------------------------------------------------- /doc/warnings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junzis/pyModeS/6401a3963c1f7a11884b64e23fa2f7b0c72f2b2e/doc/warnings -------------------------------------------------------------------------------- /hatch_build.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from Cython.Build import cythonize 5 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 6 | from setuptools import Distribution, Extension 7 | from setuptools.command import build_ext 8 | 9 | 10 | class CustomBuildHook(BuildHookInterface): 11 | def initialize(self, version, build_data): 12 | """Initialize the build hook.""" 13 | compile_args = [] 14 | 15 | if sys.platform == "linux": 16 | compile_args += ["-Wno-pointer-sign", "-Wno-unused-variable"] 17 | 18 | extensions = [ 19 | Extension( 20 | "pyModeS.c_common", 21 | sources=["src/pyModeS/c_common.pyx"], 22 | include_dirs=["src"], 23 | extra_compile_args=compile_args, 24 | ), 25 | Extension( 26 | "pyModeS.decoder.flarm.decode", 27 | [ 28 | "src/pyModeS/decoder/flarm/decode.pyx", 29 | "src/pyModeS/decoder/flarm/core.c", 30 | ], 31 | extra_compile_args=compile_args, 32 | include_dirs=["src/pyModeS/decoder/flarm"], 33 | ), 34 | # Extension( 35 | # "pyModeS.extra.demod2400.core", 36 | # [ 37 | # "src/pyModeS/extra/demod2400/core.pyx", 38 | # "src/pyModeS/extra/demod2400/demod2400.c", 39 | # ], 40 | # extra_compile_args=compile_args, 41 | # include_dirs=["src/pyModeS/extra/demod2400"], 42 | # libraries=["m"], 43 | # ), 44 | ] 45 | 46 | ext_modules = cythonize( 47 | extensions, 48 | compiler_directives={"binding": True, "language_level": 3}, 49 | ) 50 | 51 | # Create a dummy distribution object 52 | dist = Distribution(dict(name="pyModeS", ext_modules=ext_modules)) 53 | dist.package_dir = "pyModeS" 54 | 55 | # Create and run the build_ext command 56 | cmd = build_ext.build_ext(dist) 57 | cmd.verbose = True 58 | cmd.ensure_finalized() 59 | cmd.run() 60 | 61 | buildpath = Path(cmd.build_lib) 62 | 63 | # Provide locations of compiled modules 64 | force_include = { 65 | ( 66 | buildpath / cmd.get_ext_filename("pyModeS.c_common") 67 | ).as_posix(): cmd.get_ext_filename("pyModeS.c_common"), 68 | ( 69 | buildpath / cmd.get_ext_filename("pyModeS.decoder.flarm.decode") 70 | ).as_posix(): cmd.get_ext_filename("pyModeS.decoder.flarm.decode"), 71 | } 72 | 73 | build_data["pure_python"] = False 74 | build_data["infer_tag"] = True 75 | build_data["force_include"].update(force_include) 76 | 77 | return super().initialize(version, build_data) 78 | 79 | def finalize(self, version, build_data, artifact_path): 80 | """Hook called after the build.""" 81 | return super().finalize(version, build_data, artifact_path) 82 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pyModeS" 3 | version = "2.20" 4 | description = "Python Mode-S and ADS-B Decoder" 5 | authors = [{ name = "Junzi Sun", email = "git@junzis.com" }] 6 | license = { text = "GNU GPL v3" } 7 | readme = "README.rst" 8 | classifiers = [ 9 | "Development Status :: 4 - Beta", 10 | "Intended Audience :: Developers", 11 | "Topic :: Software Development :: Libraries", 12 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 13 | "Programming Language :: Python :: 3", 14 | "Typing :: Typed", 15 | ] 16 | requires-python = ">=3.9" 17 | dependencies = ["numpy>=1.26", "pyzmq>=24.0"] 18 | 19 | [project.optional-dependencies] 20 | rtlsdr = ["pyrtlsdr>=0.2.93"] 21 | 22 | [project.scripts] 23 | modeslive = "pyModeS.streamer.modeslive:main" 24 | 25 | [project.urls] 26 | homepage = "https://mode-s.org" 27 | repository = "https://github.com/junzis/pyModeS" 28 | issues = "https://github.com/junzis/pyModeS/issues" 29 | 30 | [tool.uv] 31 | dev-dependencies = [ 32 | "mypy>=0.991", 33 | "flake8>=5.0.0", 34 | "black>=22.12.0", 35 | "isort>=5.11.4", 36 | "pytest>=7.2.0", 37 | "pytest-cov>=4.0.0", 38 | "codecov>=2.1.12", 39 | ] 40 | 41 | [tool.ruff] 42 | target-version = "py311" 43 | 44 | [tool.ruff.lint] 45 | select = [ 46 | "E", 47 | "W", # pycodestyle 48 | "F", # pyflakes 49 | "I", # isort 50 | "NPY", # numpy 51 | "NPY201", # numpy 52 | # "PD", # pandas 53 | "DTZ", # flake8-datetimez 54 | "RUF", 55 | ] 56 | 57 | [build-system] 58 | requires = ["hatchling", "Cython", "setuptools"] 59 | build-backend = "hatchling.build" 60 | 61 | [tool.hatch.build.targets.wheel.hooks.custom] 62 | dependencies = ["setuptools"] 63 | -------------------------------------------------------------------------------- /src/pyModeS/.gitignore: -------------------------------------------------------------------------------- 1 | decoder/flarm/decode.c 2 | extra/demod2400/core.c 3 | c_common.c 4 | -------------------------------------------------------------------------------- /src/pyModeS/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | try: 5 | from . import c_common as common 6 | from .c_common import * 7 | except Exception: 8 | from . import py_common as common # type: ignore 9 | from .py_common import * # type: ignore 10 | 11 | from .decoder import tell 12 | from .decoder import adsb 13 | from .decoder import commb 14 | from .decoder import allcall 15 | from .decoder import surv 16 | from .decoder import bds 17 | from .extra import aero 18 | from .extra import tcpclient 19 | 20 | __all__ = [ 21 | "common", 22 | "tell", 23 | "adsb", 24 | "commb", 25 | "allcall", 26 | "surv", 27 | "bds", 28 | "aero", 29 | "tcpclient", 30 | ] 31 | 32 | 33 | warnings.simplefilter("once", DeprecationWarning) 34 | 35 | dirpath = os.path.dirname(os.path.realpath(__file__)) 36 | -------------------------------------------------------------------------------- /src/pyModeS/c_common.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | 3 | cdef int char_to_int(unsigned char binstr) 4 | cdef unsigned char int_to_char(unsigned char i) 5 | 6 | cpdef str hex2bin(str hexstr) 7 | cpdef long bin2int(str binstr) 8 | cpdef long hex2int(str hexstr) 9 | cpdef str bin2hex(str binstr) 10 | 11 | cpdef unsigned char df(str msg) 12 | cpdef long crc(str msg, bint encode=*) 13 | 14 | cpdef long floor(double x) 15 | cpdef str icao(str msg) 16 | cpdef bint is_icao_assigned(str icao) 17 | 18 | cpdef int typecode(str msg) 19 | cpdef int cprNL(double lat) 20 | 21 | cpdef str idcode(str msg) 22 | cpdef str squawk(str binstr) 23 | 24 | cpdef int altcode(str msg) 25 | cpdef int altitude(str binstr) 26 | 27 | cpdef str data(str msg) 28 | cpdef bint allzeros(str msg) 29 | -------------------------------------------------------------------------------- /src/pyModeS/c_common.pyi: -------------------------------------------------------------------------------- 1 | def hex2bin(hexstr: str) -> str: ... 2 | def bin2int(binstr: str) -> int: ... 3 | def hex2int(hexstr: str) -> int: ... 4 | def bin2hex(binstr: str) -> str: ... 5 | def df(msg: str) -> int: ... 6 | def crc(msg: str, encode: bool = False) -> int: ... 7 | def floor(x: float) -> float: ... 8 | def icao(msg: str) -> str: ... 9 | def is_icao_assigned(icao: str) -> bool: ... 10 | def typecode(msg: str) -> int: ... 11 | def cprNL(lat: float) -> int: ... 12 | def idcode(msg: str) -> str: ... 13 | def squawk(binstr: str) -> str: ... 14 | def altcode(msg: str) -> int: ... 15 | def altitude(binstr: str) -> int: ... 16 | def data(msg: str) -> str: ... 17 | def allzeros(msg: str) -> bool: ... 18 | def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ... 19 | -------------------------------------------------------------------------------- /src/pyModeS/c_common.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | 3 | cimport cython 4 | from cpython cimport array 5 | from cpython.bytes cimport PyBytes_GET_SIZE 6 | from cpython.bytearray cimport PyByteArray_GET_SIZE 7 | 8 | from libc.math cimport abs, cos, acos, fabs, M_PI as pi, floor as c_floor 9 | 10 | 11 | cdef int char_to_int(unsigned char binstr): 12 | if 48 <= binstr <= 57: # 0 to 9 13 | return binstr - 48 14 | if 97 <= binstr <= 102: # a to f 15 | return binstr - 97 + 10 16 | if 65 <= binstr <= 70: # A to F 17 | return binstr - 65 + 10 18 | return 0 19 | 20 | cdef unsigned char int_to_char(unsigned char i): 21 | if i < 10: 22 | return 48 + i # "0" + i 23 | return 97 - 10 + i # "a" - 10 + i 24 | 25 | @cython.boundscheck(False) 26 | @cython.overflowcheck(False) 27 | cpdef str hex2bin(str hexstr): 28 | """Convert a hexadecimal string to binary string, with zero fillings.""" 29 | # num_of_bits = len(hexstr) * 4 30 | cdef hexbytes = bytes(hexstr.encode()) 31 | cdef Py_ssize_t len_hexstr = PyBytes_GET_SIZE(hexbytes) 32 | # binstr = bin(int(hexbytes, 16))[2:].zfill(int(num_of_bits)) 33 | cdef bytearray _binstr = bytearray(4 * len_hexstr) 34 | cdef unsigned char[:] binstr = _binstr 35 | cdef unsigned char int_ 36 | cdef Py_ssize_t i 37 | for i in range(len_hexstr): 38 | int_ = char_to_int(hexbytes[i]) 39 | binstr[4*i] = int_to_char((int_ >> 3) & 1) 40 | binstr[4*i+1] = int_to_char((int_ >> 2) & 1) 41 | binstr[4*i+2] = int_to_char((int_ >> 1) & 1) 42 | binstr[4*i+3] = int_to_char((int_) & 1) 43 | return _binstr.decode() 44 | 45 | @cython.boundscheck(False) 46 | cpdef long bin2int(str binstr): 47 | """Convert a binary string to integer.""" 48 | # return int(binstr, 2) 49 | cdef bytearray binbytes = bytearray(binstr.encode()) 50 | cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes) 51 | cdef long cumul = 0 52 | cdef unsigned char[:] v_binstr = binbytes 53 | for i in range(len_): 54 | cumul = 2*cumul + char_to_int(v_binstr[i]) 55 | return cumul 56 | 57 | @cython.boundscheck(False) 58 | cpdef long hex2int(str hexstr): 59 | """Convert a binary string to integer.""" 60 | # return int(hexstr, 2) 61 | cdef bytearray binbytes = bytearray(hexstr.encode()) 62 | cdef Py_ssize_t len_ = PyByteArray_GET_SIZE(binbytes) 63 | cdef long cumul = 0 64 | cdef unsigned char[:] v_hexstr = binbytes 65 | for i in range(len_): 66 | cumul = 16*cumul + char_to_int(v_hexstr[i]) 67 | return cumul 68 | 69 | @cython.boundscheck(False) 70 | cpdef str bin2hex(str binstr): 71 | return "{0:X}".format(int(binstr, 2)) 72 | 73 | 74 | @cython.boundscheck(False) 75 | cpdef unsigned char df(str msg): 76 | """Decode Downlink Format value, bits 1 to 5.""" 77 | cdef str dfbin = hex2bin(msg[:2]) 78 | # return min(bin2int(dfbin[0:5]), 24) 79 | cdef long df = bin2int(dfbin[0:5]) 80 | if df > 24: 81 | return 24 82 | return df 83 | 84 | # the CRC generator 85 | # G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)] 86 | cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000]) 87 | 88 | @cython.boundscheck(False) 89 | @cython.wraparound(False) 90 | cpdef long crc(str msg, bint encode=False): 91 | """Mode-S Cyclic Redundancy Check. 92 | 93 | Detect if bit error occurs in the Mode-S message. When encode option is on, 94 | the checksum is generated. 95 | 96 | Args: 97 | msg (string): 28 bytes hexadecimal message string 98 | encode (bool): True to encode the date only and return the checksum 99 | Returns: 100 | int: message checksum, or partity bits (encoder) 101 | 102 | """ 103 | # the CRC generator 104 | # G = [int("11111111", 2), int("11111010", 2), int("00000100", 2), int("10000000", 2)] 105 | # cdef array.array _G = array.array('l', [0b11111111, 0b11111010, 0b00000100, 0b10000000]) 106 | cdef long[4] G = _G 107 | 108 | # msgbin_split = wrap(msgbin, 8) 109 | # mbytes = list(map(bin2int, msgbin_split)) 110 | cdef bytearray _msgbin = bytearray(hex2bin(msg).encode()) 111 | cdef unsigned char[:] msgbin = _msgbin 112 | 113 | cdef Py_ssize_t len_msgbin = PyByteArray_GET_SIZE(_msgbin) 114 | cdef Py_ssize_t len_mbytes = len_msgbin // 8 115 | cdef Py_ssize_t i 116 | 117 | if encode: 118 | for i in range(len_msgbin - 24, len_msgbin): 119 | msgbin[i] = 0 120 | 121 | cdef array.array _mbytes = array.array( 122 | 'l', [bin2int(_msgbin[8*i:8*i+8].decode()) for i in range(len_mbytes)] 123 | ) 124 | 125 | cdef long[:] mbytes = _mbytes 126 | 127 | cdef long bits, mask 128 | cdef Py_ssize_t ibyte, ibit 129 | 130 | for ibyte in range(len_mbytes - 3): 131 | for ibit in range(8): 132 | mask = 0x80 >> ibit 133 | bits = mbytes[ibyte] & mask 134 | 135 | if bits > 0: 136 | mbytes[ibyte] = mbytes[ibyte] ^ (G[0] >> ibit) 137 | mbytes[ibyte + 1] = mbytes[ibyte + 1] ^ ( 138 | 0xFF & ((G[0] << 8 - ibit) | (G[1] >> ibit)) 139 | ) 140 | mbytes[ibyte + 2] = mbytes[ibyte + 2] ^ ( 141 | 0xFF & ((G[1] << 8 - ibit) | (G[2] >> ibit)) 142 | ) 143 | mbytes[ibyte + 3] = mbytes[ibyte + 3] ^ ( 144 | 0xFF & ((G[2] << 8 - ibit) | (G[3] >> ibit)) 145 | ) 146 | 147 | cdef long result = (mbytes[len_mbytes-3] << 16) | (mbytes[len_mbytes-2] << 8) | mbytes[len_mbytes-1] 148 | 149 | return result 150 | 151 | 152 | 153 | cpdef long floor(double x): 154 | """Mode-S floor function. 155 | 156 | Defined as the greatest integer value k, such that k <= x 157 | For example: floor(3.6) = 3 and floor(-3.6) = -4 158 | 159 | """ 160 | return c_floor(x) 161 | 162 | cpdef str icao(str msg): 163 | """Calculate the ICAO address from an Mode-S message.""" 164 | cdef unsigned char DF = df(msg) 165 | cdef long c0, c1 166 | 167 | if DF in (11, 17, 18): 168 | addr = msg[2:8] 169 | elif DF in (0, 4, 5, 16, 20, 21): 170 | c0 = crc(msg, encode=True) 171 | c1 = hex2int(msg[-6:]) 172 | addr = "%06X" % (c0 ^ c1) 173 | else: 174 | addr = None 175 | 176 | return addr 177 | 178 | 179 | cpdef bint is_icao_assigned(str icao): 180 | """Check whether the ICAO address is assigned (Annex 10, Vol 3).""" 181 | if (icao is None) or (not isinstance(icao, str)) or (len(icao) != 6): 182 | return False 183 | 184 | cdef long icaoint = hex2int(icao) 185 | 186 | if 0x200000 < icaoint < 0x27FFFF: 187 | return False # AFI 188 | if 0x280000 < icaoint < 0x28FFFF: 189 | return False # SAM 190 | if 0x500000 < icaoint < 0x5FFFFF: 191 | return False # EUR, NAT 192 | if 0x600000 < icaoint < 0x67FFFF: 193 | return False # MID 194 | if 0x680000 < icaoint < 0x6F0000: 195 | return False # ASIA 196 | if 0x900000 < icaoint < 0x9FFFFF: 197 | return False # NAM, PAC 198 | if 0xB00000 < icaoint < 0xBFFFFF: 199 | return False # CAR 200 | if 0xD00000 < icaoint < 0xDFFFFF: 201 | return False # future 202 | if 0xF00000 < icaoint < 0xFFFFFF: 203 | return False # future 204 | 205 | return True 206 | 207 | @cython.boundscheck(False) 208 | @cython.wraparound(False) 209 | cpdef int typecode(str msg): 210 | """Type code of ADS-B message""" 211 | if df(msg) not in (17, 18): 212 | return -1 213 | # return None 214 | 215 | cdef str tcbin = hex2bin(msg[8:10]) 216 | return bin2int(tcbin[0:5]) 217 | 218 | @cython.cdivision(True) 219 | cpdef int cprNL(double lat): 220 | """NL() function in CPR decoding.""" 221 | 222 | if abs(lat) <= 1e-08: 223 | return 59 224 | elif abs(abs(lat) - 87) <= 1e-08 + 1e-05 * 87: 225 | return 2 226 | elif lat > 87 or lat < -87: 227 | return 1 228 | 229 | cdef int nz = 15 230 | cdef double a = 1 - cos(pi / (2 * nz)) 231 | cdef double b = cos(pi / 180 * fabs(lat)) ** 2 232 | cdef double nl = 2 * pi / (acos(1 - a / b)) 233 | NL = floor(nl) 234 | return NL 235 | 236 | @cython.boundscheck(False) 237 | @cython.wraparound(False) 238 | cpdef str idcode(str msg): 239 | """Compute identity (squawk code).""" 240 | if df(msg) not in [5, 21]: 241 | raise RuntimeError("Message must be Downlink Format 5 or 21.") 242 | 243 | squawk_code = squawk(hex2bin(msg)[19:32]) 244 | return squawk_code 245 | 246 | 247 | @cython.boundscheck(False) 248 | @cython.wraparound(False) 249 | cpdef str squawk(str binstr): 250 | """Compute identity (squawk code).""" 251 | 252 | if len(binstr) != 13 or set(binstr) != set('01'): 253 | raise RuntimeError("Input must be 13 bits binary string") 254 | 255 | cdef bytearray _mbin = bytearray(binstr.encode()) 256 | cdef unsigned char[:] mbin = _mbin 257 | 258 | cdef bytearray _idcode = bytearray(4) 259 | cdef unsigned char[:] idcode = _idcode 260 | 261 | cdef unsigned char C1 = mbin[0] 262 | cdef unsigned char A1 = mbin[1] 263 | cdef unsigned char C2 = mbin[2] 264 | cdef unsigned char A2 = mbin[3] 265 | cdef unsigned char C4 = mbin[4] 266 | cdef unsigned char A4 = mbin[5] 267 | # X = mbin[6] 268 | cdef unsigned char B1 = mbin[7] 269 | cdef unsigned char D1 = mbin[8] 270 | cdef unsigned char B2 = mbin[9] 271 | cdef unsigned char D2 = mbin[10] 272 | cdef unsigned char B4 = mbin[11] 273 | cdef unsigned char D4 = mbin[12] 274 | 275 | idcode[0] = int_to_char((char_to_int(A4)*2 + char_to_int(A2))*2 + char_to_int(A1)) 276 | idcode[1] = int_to_char((char_to_int(B4)*2 + char_to_int(B2))*2 + char_to_int(B1)) 277 | idcode[2] = int_to_char((char_to_int(C4)*2 + char_to_int(C2))*2 + char_to_int(C1)) 278 | idcode[3] = int_to_char((char_to_int(D4)*2 + char_to_int(D2))*2 + char_to_int(D1)) 279 | 280 | return _idcode.decode() 281 | 282 | 283 | @cython.boundscheck(False) 284 | @cython.wraparound(False) 285 | cpdef int altcode(str msg): 286 | """Compute the altitude.""" 287 | if df(msg) not in [0, 4, 16, 20]: 288 | raise RuntimeError("Message must be Downlink Format 0, 4, 16, or 20.") 289 | 290 | alt = altitude(hex2bin(msg)[19:32]) 291 | return alt 292 | 293 | 294 | @cython.boundscheck(False) 295 | @cython.wraparound(False) 296 | cpdef int altitude(str binstr): 297 | 298 | if len(binstr) != 13 or not set(binstr).issubset(set("01")): 299 | raise RuntimeError("Input must be 13 bits binary string") 300 | 301 | cdef bytearray _mbin = bytearray(binstr.encode()) 302 | cdef unsigned char[:] mbin = _mbin 303 | 304 | cdef char Mbit = binstr[6] 305 | cdef char Qbit = binstr[8] 306 | 307 | cdef int alt = 0 308 | cdef bytearray vbin 309 | cdef bytearray _graybytes = bytearray(11) 310 | cdef unsigned char[:] graybytes = _graybytes 311 | 312 | if bin2int(binstr) == 0: 313 | # altitude unknown or invalid 314 | alt = -999999 315 | 316 | elif Mbit == 48: # unit in ft, "0" -> 48 317 | if Qbit == 49: # 25ft interval, "1" -> 49 318 | vbin = _mbin[:6] + _mbin[7:8] + _mbin[9:] 319 | alt = bin2int(vbin.decode()) * 25 - 1000 320 | if Qbit == 48: # 100ft interval, above 50175ft, "0" -> 48 321 | graybytes[8] = mbin[0] 322 | graybytes[2] = mbin[1] 323 | graybytes[9] = mbin[2] 324 | graybytes[3] = mbin[3] 325 | graybytes[10] = mbin[4] 326 | graybytes[4] = mbin[5] 327 | # M = mbin[6] 328 | graybytes[5] = mbin[7] 329 | # Q = mbin[8] 330 | graybytes[6] = mbin[9] 331 | graybytes[0] = mbin[10] 332 | graybytes[7] = mbin[11] 333 | graybytes[1] = mbin[12] 334 | 335 | alt = gray2alt(_graybytes.decode()) 336 | 337 | elif Mbit == 49: # unit in meter, "1" -> 49 338 | vbin = _mbin[:6] + _mbin[7:] 339 | alt = int(bin2int(vbin.decode()) * 3.28084) # convert to ft 340 | 341 | return alt 342 | 343 | 344 | cpdef int gray2alt(str codestr): 345 | cdef str gc500 = codestr[:8] 346 | cdef int n500 = gray2int(gc500) 347 | 348 | # in 100-ft step must be converted first 349 | cdef str gc100 = codestr[8:] 350 | cdef int n100 = gray2int(gc100) 351 | 352 | if n100 in [0, 5, 6]: 353 | return -1 354 | #return None 355 | 356 | if n100 == 7: 357 | n100 = 5 358 | 359 | if n500 % 2: 360 | n100 = 6 - n100 361 | 362 | alt = (n500 * 500 + n100 * 100) - 1300 363 | return alt 364 | 365 | 366 | cdef int gray2int(str graystr): 367 | """Convert greycode to binary.""" 368 | cdef int num = bin2int(graystr) 369 | num ^= num >> 8 370 | num ^= num >> 4 371 | num ^= num >> 2 372 | num ^= num >> 1 373 | return num 374 | 375 | 376 | cpdef str data(str msg): 377 | """Return the data frame in the message, bytes 9 to 22.""" 378 | return msg[8:-6] 379 | 380 | 381 | cpdef bint allzeros(str msg): 382 | """Check if the data bits are all zeros.""" 383 | d = hex2bin(data(msg)) 384 | 385 | if bin2int(d) > 0: 386 | return False 387 | else: 388 | return True 389 | 390 | 391 | def wrongstatus(data, sb, msb, lsb): 392 | """Check if the status bit and field bits are consistency. 393 | 394 | This Function is used for checking BDS code versions. 395 | 396 | """ 397 | # status bit, most significant bit, least significant bit 398 | status = int(data[sb - 1]) 399 | value = bin2int(data[msb - 1 : lsb]) 400 | 401 | if not status: 402 | if value != 0: 403 | return True 404 | 405 | return False 406 | -------------------------------------------------------------------------------- /src/pyModeS/common.pyi: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | def hex2bin(hexstr: str) -> str: ... 4 | def bin2int(binstr: str) -> int: ... 5 | def hex2int(hexstr: str) -> int: ... 6 | def bin2hex(binstr: str) -> str: ... 7 | def df(msg: str) -> int: ... 8 | def crc(msg: str, encode: bool = False) -> int: ... 9 | def floor(x: float) -> float: ... 10 | def icao(msg: str) -> Optional[str]: ... 11 | def is_icao_assigned(icao: str) -> bool: ... 12 | def typecode(msg: str) -> Optional[int]: ... 13 | def cprNL(lat: float) -> int: ... 14 | def idcode(msg: str) -> str: ... 15 | def squawk(binstr: str) -> str: ... 16 | def altcode(msg: str) -> Optional[int]: ... 17 | def altitude(binstr: str) -> Optional[int]: ... 18 | def gray2alt(binstr: str) -> Optional[int]: ... 19 | def gray2int(binstr: str) -> int: ... 20 | def data(msg: str) -> str: ... 21 | def allzeros(msg: str) -> bool: ... 22 | def wrongstatus(data: str, sb: int, msb: int, lsb: int) -> bool: ... 23 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/__init__.py: -------------------------------------------------------------------------------- 1 | def tell(msg: str) -> None: 2 | from .. import common, adsb, commb, bds 3 | 4 | def _print(label, value, unit=None): 5 | print("%28s: " % label, end="") 6 | print("%s " % value, end="") 7 | if unit: 8 | print(unit) 9 | else: 10 | print() 11 | 12 | df = common.df(msg) 13 | icao = common.icao(msg) 14 | 15 | _print("Message", msg) 16 | _print("ICAO address", icao) 17 | _print("Downlink Format", df) 18 | 19 | if df == 17: 20 | _print("Protocol", "Mode-S Extended Squitter (ADS-B)") 21 | 22 | tc = common.typecode(msg) 23 | 24 | if tc is None: 25 | _print("ERROR", "Unknown typecode") 26 | return 27 | 28 | if 1 <= tc <= 4: # callsign 29 | callsign = adsb.callsign(msg) 30 | _print("Type", "Identification and category") 31 | _print("Callsign:", callsign) 32 | 33 | if 5 <= tc <= 8: # surface position 34 | _print("Type", "Surface position") 35 | oe = adsb.oe_flag(msg) 36 | msgbin = common.hex2bin(msg) 37 | cprlat = common.bin2int(msgbin[54:71]) / 131072.0 38 | cprlon = common.bin2int(msgbin[71:88]) / 131072.0 39 | v = adsb.surface_velocity(msg) 40 | _print("CPR format", "Odd" if oe else "Even") 41 | _print("CPR Latitude", cprlat) 42 | _print("CPR Longitude", cprlon) 43 | _print("Speed", v[0], "knots") 44 | _print("Track", v[1], "degrees") 45 | 46 | if 9 <= tc <= 18: # airborne position 47 | _print("Type", "Airborne position (with barometric altitude)") 48 | alt = adsb.altitude(msg) 49 | oe = adsb.oe_flag(msg) 50 | msgbin = common.hex2bin(msg) 51 | cprlat = common.bin2int(msgbin[54:71]) / 131072.0 52 | cprlon = common.bin2int(msgbin[71:88]) / 131072.0 53 | _print("CPR format", "Odd" if oe else "Even") 54 | _print("CPR Latitude", cprlat) 55 | _print("CPR Longitude", cprlon) 56 | _print("Altitude", alt, "feet") 57 | 58 | if tc == 19: 59 | _print("Type", "Airborne velocity") 60 | velocity = adsb.velocity(msg) 61 | if velocity is not None: 62 | spd, trk, vr, t = velocity 63 | types = {"GS": "Ground speed", "TAS": "True airspeed"} 64 | _print("Speed", spd, "knots") 65 | _print("Track", trk, "degrees") 66 | _print("Vertical rate", vr, "feet/minute") 67 | _print("Type", types[t]) 68 | 69 | if 20 <= tc <= 22: # airborne position 70 | _print("Type", "Airborne position (with GNSS altitude)") 71 | alt = adsb.altitude(msg) 72 | oe = adsb.oe_flag(msg) 73 | msgbin = common.hex2bin(msg) 74 | cprlat = common.bin2int(msgbin[54:71]) / 131072.0 75 | cprlon = common.bin2int(msgbin[71:88]) / 131072.0 76 | _print("CPR format", "Odd" if oe else "Even") 77 | _print("CPR Latitude", cprlat) 78 | _print("CPR Longitude", cprlon) 79 | _print("Altitude", alt, "feet") 80 | 81 | if tc == 29: # target state and status 82 | _print("Type", "Target State and Status") 83 | subtype = common.bin2int((common.hex2bin(msg)[32:])[5:7]) 84 | _print("Subtype", subtype) 85 | tcas_operational = adsb.tcas_operational(msg) 86 | types_29 = {0: "Not Engaged", 1: "Engaged"} 87 | tcas_operational_types = {0: "Not Operational", 1: "Operational"} 88 | if subtype == 0: 89 | emergency_types = { 90 | 0: "No emergency", 91 | 1: "General emergency", 92 | 2: "Lifeguard/medical emergency", 93 | 3: "Minimum fuel", 94 | 4: "No communications", 95 | 5: "Unlawful interference", 96 | 6: "Downed aircraft", 97 | 7: "Reserved", 98 | } 99 | vertical_horizontal_types = { 100 | 1: "Acquiring mode", 101 | 2: "Capturing/Maintaining mode", 102 | } 103 | tcas_ra_types = {0: "Not active", 1: "Active"} 104 | alt, alt_source, alt_ref = adsb.target_altitude(msg) 105 | angle, angle_type, angle_source = adsb.target_angle(msg) 106 | vertical_mode = adsb.vertical_mode(msg) 107 | horizontal_mode = adsb.horizontal_mode(msg) 108 | tcas_ra = adsb.tcas_ra(msg) 109 | emergency_status = adsb.emergency_status(msg) 110 | _print("Target altitude", alt, "feet") 111 | _print("Altitude source", alt_source) 112 | _print("Altitude reference", alt_ref) 113 | _print("Angle", angle, "°") 114 | _print("Angle Type", angle_type) 115 | _print("Angle Source", angle_source) 116 | if vertical_mode is not None: 117 | _print( 118 | "Vertical mode", 119 | vertical_horizontal_types[vertical_mode], 120 | ) 121 | if horizontal_mode is not None: 122 | _print( 123 | "Horizontal mode", 124 | vertical_horizontal_types[horizontal_mode], 125 | ) 126 | _print( 127 | "TCAS/ACAS", 128 | tcas_operational_types[tcas_operational] 129 | if tcas_operational 130 | else None, 131 | ) 132 | _print("TCAS/ACAS RA", tcas_ra_types[tcas_ra]) 133 | _print("Emergency status", emergency_types[emergency_status]) 134 | else: 135 | alt, alt_source = adsb.selected_altitude(msg) # type: ignore 136 | baro = adsb.baro_pressure_setting(msg) 137 | hdg = adsb.selected_heading(msg) 138 | autopilot = adsb.autopilot(msg) 139 | vnav = adsb.vnav_mode(msg) 140 | alt_hold = adsb.altitude_hold_mode(msg) 141 | app = adsb.approach_mode(msg) 142 | lnav = adsb.lnav_mode(msg) 143 | _print("Selected altitude", alt, "feet") 144 | _print("Altitude source", alt_source) 145 | _print( 146 | "Barometric pressure setting", 147 | baro, 148 | "" if baro is None else "millibars", 149 | ) 150 | _print("Selected Heading", hdg, "°") 151 | if not (common.bin2int((common.hex2bin(msg)[32:])[46]) == 0): 152 | _print( 153 | "Autopilot", types_29[autopilot] if autopilot else None 154 | ) 155 | _print("VNAV mode", types_29[vnav] if vnav else None) 156 | _print( 157 | "Altitude hold mode", 158 | types_29[alt_hold] if alt_hold else None, 159 | ) 160 | _print("Approach mode", types_29[app] if app else None) 161 | _print( 162 | "TCAS/ACAS", 163 | tcas_operational_types[tcas_operational] 164 | if tcas_operational 165 | else None, 166 | ) 167 | _print("LNAV mode", types_29[lnav] if lnav else None) 168 | 169 | if df == 20: 170 | _print("Protocol", "Mode-S Comm-B altitude reply") 171 | _print("Altitude", common.altcode(msg), "feet") 172 | 173 | if df == 21: 174 | _print("Protocol", "Mode-S Comm-B identity reply") 175 | _print("Squawk code", common.idcode(msg)) 176 | 177 | if df == 20 or df == 21: 178 | labels = { 179 | "BDS10": "Data link capability", 180 | "BDS17": "GICB capability", 181 | "BDS20": "Aircraft identification", 182 | "BDS30": "ACAS resolution", 183 | "BDS40": "Vertical intention report", 184 | "BDS50": "Track and turn report", 185 | "BDS60": "Heading and speed report", 186 | "BDS44": "Meteorological routine air report", 187 | "BDS45": "Meteorological hazard report", 188 | "EMPTY": "[No information available]", 189 | } 190 | 191 | BDS = bds.infer(msg, mrar=True) 192 | if BDS is not None and BDS in labels.keys(): 193 | _print("BDS", "%s (%s)" % (BDS, labels[BDS])) 194 | else: 195 | _print("BDS", BDS) 196 | 197 | if BDS == "BDS20": 198 | callsign = commb.cs20(msg) 199 | _print("Callsign", callsign) 200 | 201 | if BDS == "BDS40": 202 | _print("MCP target alt", commb.selalt40mcp(msg), "feet") 203 | _print("FMS Target alt", commb.selalt40fms(msg), "feet") 204 | _print("Pressure", commb.p40baro(msg), "millibar") 205 | 206 | if BDS == "BDS50": 207 | _print("Roll angle", commb.roll50(msg), "degrees") 208 | _print("Track angle", commb.trk50(msg), "degrees") 209 | _print("Track rate", commb.rtrk50(msg), "degree/second") 210 | _print("Ground speed", commb.gs50(msg), "knots") 211 | _print("True airspeed", commb.tas50(msg), "knots") 212 | 213 | if BDS == "BDS60": 214 | _print("Magnetic Heading", commb.hdg60(msg), "degrees") 215 | _print("Indicated airspeed", commb.ias60(msg), "knots") 216 | _print("Mach number", commb.mach60(msg)) 217 | _print("Vertical rate (Baro)", commb.vr60baro(msg), "feet/minute") 218 | _print("Vertical rate (INS)", commb.vr60ins(msg), "feet/minute") 219 | 220 | if BDS == "BDS44": 221 | _print("Wind speed", commb.wind44(msg)[0], "knots") 222 | _print("Wind direction", commb.wind44(msg)[1], "degrees") 223 | _print("Temperature 1", commb.temp44(msg)[0], "Celsius") 224 | _print("Temperature 2", commb.temp44(msg)[1], "Celsius") 225 | _print("Pressure", commb.p44(msg), "hPa") 226 | _print("Humidity", commb.hum44(msg), "%") 227 | _print("Turbulence", commb.turb44(msg)) 228 | 229 | if BDS == "BDS45": 230 | _print("Turbulence", commb.turb45(msg)) 231 | _print("Wind shear", commb.ws45(msg)) 232 | _print("Microbust", commb.mb45(msg)) 233 | _print("Icing", commb.ic45(msg)) 234 | _print("Wake vortex", commb.wv45(msg)) 235 | _print("Temperature", commb.temp45(msg), "Celsius") 236 | _print("Pressure", commb.p45(msg), "hPa") 237 | _print("Radio height", commb.rh45(msg), "feet") 238 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/acas.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decoding Air-Air Surveillance (ACAS) DF=0/16 3 | 4 | [To be implemented] 5 | """ 6 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/allcall.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decode all-call reply messages, with downlink format 11 3 | """ 4 | 5 | 6 | from __future__ import annotations 7 | from typing import Callable, TypeVar 8 | 9 | from .. import common 10 | 11 | T = TypeVar("T") 12 | F = Callable[[str], T] 13 | 14 | 15 | def _checkdf(func: F[T]) -> F[T]: 16 | 17 | """Ensure downlink format is 11.""" 18 | 19 | def wrapper(msg: str) -> T: 20 | df = common.df(msg) 21 | if df != 11: 22 | raise RuntimeError( 23 | "Incorrect downlink format, expect 11, got {}".format(df) 24 | ) 25 | return func(msg) 26 | 27 | return wrapper 28 | 29 | 30 | @_checkdf 31 | def icao(msg: str) -> None | str: 32 | """Decode transponder code (ICAO address). 33 | 34 | Args: 35 | msg (str): 14 hexdigits string 36 | Returns: 37 | string: ICAO address 38 | 39 | """ 40 | return common.icao(msg) 41 | 42 | 43 | @_checkdf 44 | def interrogator(msg: str) -> str: 45 | """Decode interrogator identifier code. 46 | 47 | Args: 48 | msg (str): 14 hexdigits string 49 | Returns: 50 | int: interrogator identifier code 51 | 52 | """ 53 | # the CRC remainder contains the CL and IC field. 54 | # the top three bits are CL field and last four bits are IC field. 55 | remainder = common.crc(msg) 56 | if remainder > 79: 57 | IC = "corrupt IC" 58 | elif remainder < 16: 59 | IC = "II" + str(remainder) 60 | else: 61 | IC = "SI" + str(remainder - 16) 62 | return IC 63 | 64 | 65 | @_checkdf 66 | def capability(msg: str) -> tuple[int, None | str]: 67 | """Decode transponder capability. 68 | 69 | Args: 70 | msg (str): 14 hexdigits string 71 | Returns: 72 | int, str: transponder capability, description 73 | 74 | """ 75 | msgbin = common.hex2bin(msg) 76 | ca = common.bin2int(msgbin[5:8]) 77 | 78 | if ca == 0: 79 | text = "level 1 transponder" 80 | elif ca == 4: 81 | text = "level 2 transponder, ability to set CA to 7, on ground" 82 | elif ca == 5: 83 | text = "level 2 transponder, ability to set CA to 7, airborne" 84 | elif ca == 6: 85 | text = ( 86 | "evel 2 transponder, ability to set CA to 7, " 87 | "either airborne or ground" 88 | ) 89 | elif ca == 7: 90 | text = ( 91 | "Downlink Request value is not 0, " 92 | "or the Flight Status is 2, 3, 4 or 5, " 93 | "and either airborne or on the ground" 94 | ) 95 | else: 96 | text = None 97 | 98 | return ca, text 99 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Junzi Sun (TU Delft) 2 | 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | """ 18 | Common functions for Mode-S decoding 19 | """ 20 | 21 | from typing import Optional 22 | 23 | import numpy as np 24 | 25 | from ... import common 26 | from ...extra import aero 27 | from . import ( # noqa: F401 28 | bds10, 29 | bds17, 30 | bds20, 31 | bds30, 32 | bds40, 33 | bds44, 34 | bds45, 35 | bds50, 36 | bds60, 37 | bds61, 38 | bds62, 39 | ) 40 | 41 | 42 | def is50or60( 43 | msg: str, spd_ref: float, trk_ref: float, alt_ref: float 44 | ) -> Optional[str]: 45 | """Use reference ground speed and trk to determine BDS50 and DBS60. 46 | 47 | Args: 48 | msg (str): 28 hexdigits string 49 | spd_ref (float): reference speed (ADS-B ground speed), kts 50 | trk_ref (float): reference track (ADS-B track angle), deg 51 | alt_ref (float): reference altitude (ADS-B altitude), ft 52 | 53 | Returns: 54 | String or None: BDS version, or possible versions, 55 | or None if nothing matches. 56 | 57 | """ 58 | 59 | def vxy(v, angle): 60 | vx = v * np.sin(np.radians(angle)) 61 | vy = v * np.cos(np.radians(angle)) 62 | return vx, vy 63 | 64 | # message must be both BDS 50 and 60 before processing 65 | if not (bds50.is50(msg) and bds60.is60(msg)): 66 | return None 67 | 68 | # --- assuming BDS60 --- 69 | h60 = bds60.hdg60(msg) 70 | m60 = bds60.mach60(msg) 71 | i60 = bds60.ias60(msg) 72 | 73 | # additional check now knowing the altitude 74 | if (m60 is not None) and (i60 is not None): 75 | ias_ = aero.mach2cas(m60, alt_ref * aero.ft) / aero.kts 76 | if abs(i60 - ias_) > 20: 77 | return "BDS50" 78 | 79 | if h60 is None or (m60 is None and i60 is None): 80 | return "BDS50,BDS60" 81 | 82 | m60 = np.nan if m60 is None else m60 83 | i60 = np.nan if i60 is None else i60 84 | 85 | # --- assuming BDS50 --- 86 | h50 = bds50.trk50(msg) 87 | v50 = bds50.gs50(msg) 88 | 89 | if h50 is None or v50 is None: 90 | return "BDS50,BDS60" 91 | 92 | XY5 = vxy(v50 * aero.kts, h50) 93 | XY6m = vxy(aero.mach2tas(m60, alt_ref * aero.ft), h60) 94 | XY6i = vxy(aero.cas2tas(i60 * aero.kts, alt_ref * aero.ft), h60) 95 | 96 | allbds = ["BDS50", "BDS60", "BDS60"] 97 | 98 | X = np.array([XY5, XY6m, XY6i]) 99 | Mu = np.array(vxy(spd_ref * aero.kts, trk_ref)) 100 | 101 | # compute Mahalanobis distance matrix 102 | # Cov = [[20**2, 0], [0, 20**2]] 103 | # mmatrix = np.sqrt(np.dot(np.dot(X-Mu, np.linalg.inv(Cov)), (X-Mu).T)) 104 | # dist = np.diag(mmatrix) 105 | 106 | # since the covariance matrix is identity matrix, 107 | # M-dist is same as eculidian distance 108 | try: 109 | dist = np.linalg.norm(X - Mu, axis=1) 110 | BDS = allbds[np.nanargmin(dist)] 111 | except ValueError: 112 | return "BDS50,BDS60" 113 | 114 | return BDS 115 | 116 | 117 | def infer(msg: str, mrar: bool = False) -> Optional[str]: 118 | """Estimate the most likely BDS code of an message. 119 | 120 | Args: 121 | msg (str): 28 hexdigits string 122 | mrar (bool): Also infer MRAR (BDS 44) and MHR (BDS 45). 123 | Defaults to False. 124 | 125 | Returns: 126 | String or None: BDS version, or possible versions, 127 | or None if nothing matches. 128 | 129 | """ 130 | df = common.df(msg) 131 | 132 | if common.allzeros(msg): 133 | return "EMPTY" 134 | 135 | # For ADS-B / Mode-S extended squitter 136 | if df == 17: 137 | tc = common.typecode(msg) 138 | if tc is None: 139 | return None 140 | 141 | if 1 <= tc <= 4: 142 | return "BDS08" # identification and category 143 | if 5 <= tc <= 8: 144 | return "BDS06" # surface movement 145 | if 9 <= tc <= 18: 146 | return "BDS05" # airborne position, baro-alt 147 | if tc == 19: 148 | return "BDS09" # airborne velocity 149 | if 20 <= tc <= 22: 150 | return "BDS05" # airborne position, gnss-alt 151 | if tc == 28: 152 | return "BDS61" # aircraft status 153 | if tc == 29: 154 | return "BDS62" # target state and status 155 | if tc == 31: 156 | return "BDS65" # operational status 157 | 158 | # For Comm-B replies 159 | IS10 = bds10.is10(msg) 160 | IS17 = bds17.is17(msg) 161 | IS20 = bds20.is20(msg) 162 | IS30 = bds30.is30(msg) 163 | IS40 = bds40.is40(msg) 164 | IS50 = bds50.is50(msg) 165 | IS60 = bds60.is60(msg) 166 | IS44 = bds44.is44(msg) 167 | IS45 = bds45.is45(msg) 168 | 169 | if mrar: 170 | allbds = np.array( 171 | [ 172 | "BDS10", 173 | "BDS17", 174 | "BDS20", 175 | "BDS30", 176 | "BDS40", 177 | "BDS44", 178 | "BDS45", 179 | "BDS50", 180 | "BDS60", 181 | ] 182 | ) 183 | mask = [IS10, IS17, IS20, IS30, IS40, IS44, IS45, IS50, IS60] 184 | else: 185 | allbds = np.array( 186 | ["BDS10", "BDS17", "BDS20", "BDS30", "BDS40", "BDS50", "BDS60"] 187 | ) 188 | mask = [IS10, IS17, IS20, IS30, IS40, IS50, IS60] 189 | 190 | bds = ",".join(sorted(allbds[mask])) 191 | 192 | if len(bds) == 0: 193 | return None 194 | else: 195 | return bds 196 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds05.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 0,5 3 | # ADS-B TC=9-18 4 | # Airborne position 5 | # ------------------------------------------ 6 | 7 | from __future__ import annotations 8 | 9 | from datetime import datetime 10 | 11 | from ... import common 12 | 13 | 14 | def airborne_position( 15 | msg0: str, msg1: str, t0: int | datetime, t1: int | datetime 16 | ) -> None | tuple[float, float]: 17 | """Decode airborne position from a pair of even and odd position message 18 | 19 | Args: 20 | msg0 (string): even message (28 hexdigits) 21 | msg1 (string): odd message (28 hexdigits) 22 | t0 (int): timestamps for the even message 23 | t1 (int): timestamps for the odd message 24 | 25 | Returns: 26 | (float, float): (latitude, longitude) of the aircraft 27 | """ 28 | 29 | mb0 = common.hex2bin(msg0)[32:] 30 | mb1 = common.hex2bin(msg1)[32:] 31 | 32 | oe0 = int(mb0[21]) 33 | oe1 = int(mb1[21]) 34 | if oe0 == 0 and oe1 == 1: 35 | pass 36 | elif oe0 == 1 and oe1 == 0: 37 | mb0, mb1 = mb1, mb0 38 | t0, t1 = t1, t0 39 | else: 40 | raise RuntimeError("Both even and odd CPR frames are required.") 41 | 42 | # 131072 is 2^17, since CPR lat and lon are 17 bits each. 43 | cprlat_even = common.bin2int(mb0[22:39]) / 131072 44 | cprlon_even = common.bin2int(mb0[39:56]) / 131072 45 | cprlat_odd = common.bin2int(mb1[22:39]) / 131072 46 | cprlon_odd = common.bin2int(mb1[39:56]) / 131072 47 | 48 | air_d_lat_even = 360 / 60 49 | air_d_lat_odd = 360 / 59 50 | 51 | # compute latitude index 'j' 52 | j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5) 53 | 54 | lat_even = float(air_d_lat_even * (j % 60 + cprlat_even)) 55 | lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd)) 56 | 57 | if lat_even >= 270: 58 | lat_even = lat_even - 360 59 | 60 | if lat_odd >= 270: 61 | lat_odd = lat_odd - 360 62 | 63 | # check if both are in the same latidude zone, exit if not 64 | if common.cprNL(lat_even) != common.cprNL(lat_odd): 65 | return None 66 | 67 | # compute ni, longitude index m, and longitude 68 | # (people pass int+int or datetime+datetime) 69 | if t0 > t1: # type: ignore 70 | lat = lat_even 71 | nl = common.cprNL(lat) 72 | ni = max(common.cprNL(lat) - 0, 1) 73 | m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5) 74 | lon = (360 / ni) * (m % ni + cprlon_even) 75 | else: 76 | lat = lat_odd 77 | nl = common.cprNL(lat) 78 | ni = max(common.cprNL(lat) - 1, 1) 79 | m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5) 80 | lon = (360 / ni) * (m % ni + cprlon_odd) 81 | 82 | if lon > 180: 83 | lon = lon - 360 84 | 85 | return lat, lon 86 | 87 | 88 | def airborne_position_with_ref( 89 | msg: str, lat_ref: float, lon_ref: float 90 | ) -> tuple[float, float]: 91 | """Decode airborne position with only one message, 92 | knowing reference nearby location, such as previously calculated location, 93 | ground station, or airport location, etc. The reference position shall 94 | be within 180NM of the true position. 95 | 96 | Args: 97 | msg (str): even message (28 hexdigits) 98 | lat_ref: previous known latitude 99 | lon_ref: previous known longitude 100 | 101 | Returns: 102 | (float, float): (latitude, longitude) of the aircraft 103 | """ 104 | 105 | mb = common.hex2bin(msg)[32:] 106 | 107 | cprlat = common.bin2int(mb[22:39]) / 131072 108 | cprlon = common.bin2int(mb[39:56]) / 131072 109 | 110 | i = int(mb[21]) 111 | d_lat = 360 / 59 if i else 360 / 60 112 | 113 | # From 1090 MOPS, Vol.1 DO-260C, A.1.7.5 114 | j = common.floor(0.5 + lat_ref / d_lat - cprlat) 115 | 116 | lat = d_lat * (j + cprlat) 117 | 118 | ni = common.cprNL(lat) - i 119 | 120 | if ni > 0: 121 | d_lon = 360 / ni 122 | else: 123 | d_lon = 360 124 | 125 | m = common.floor(0.5 + lon_ref / d_lon - cprlon) 126 | 127 | lon = d_lon * (m + cprlon) 128 | 129 | return lat, lon 130 | 131 | 132 | def altitude(msg: str) -> None | int: 133 | """Decode aircraft altitude 134 | 135 | Args: 136 | msg (str): 28 hexdigits string 137 | 138 | Returns: 139 | int: altitude in feet 140 | """ 141 | 142 | tc = common.typecode(msg) 143 | 144 | if tc is None or tc < 9 or tc == 19 or tc > 22: 145 | raise RuntimeError("%s: Not an airborne position message" % msg) 146 | 147 | mb = common.hex2bin(msg)[32:] 148 | altbin = mb[8:20] 149 | 150 | if tc < 19: 151 | altcode = altbin[0:6] + "0" + altbin[6:] 152 | alt = common.altitude(altcode) 153 | if alt != -999999: 154 | return alt 155 | else: 156 | # return None if altitude is invalid 157 | return None 158 | else: 159 | return common.bin2int(altbin) * 3.28084 # type: ignore 160 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds06.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 0,6 3 | # ADS-B TC=5-8 4 | # Surface movement 5 | # ------------------------------------------ 6 | 7 | from __future__ import annotations 8 | 9 | from datetime import datetime 10 | 11 | from ... import common 12 | 13 | 14 | def surface_position( 15 | msg0: str, 16 | msg1: str, 17 | t0: int | datetime, 18 | t1: int | datetime, 19 | lat_ref: float, 20 | lon_ref: float, 21 | ) -> None | tuple[float, float]: 22 | """Decode surface position from a pair of even and odd position message, 23 | the lat/lon of receiver must be provided to yield the correct solution. 24 | 25 | Args: 26 | msg0 (string): even message (28 hexdigits) 27 | msg1 (string): odd message (28 hexdigits) 28 | t0 (int): timestamps for the even message 29 | t1 (int): timestamps for the odd message 30 | lat_ref (float): latitude of the receiver 31 | lon_ref (float): longitude of the receiver 32 | 33 | Returns: 34 | (float, float): (latitude, longitude) of the aircraft 35 | """ 36 | 37 | msgbin0 = common.hex2bin(msg0) 38 | msgbin1 = common.hex2bin(msg1) 39 | 40 | # 131072 is 2^17, since CPR lat and lon are 17 bits each. 41 | cprlat_even = common.bin2int(msgbin0[54:71]) / 131072 42 | cprlon_even = common.bin2int(msgbin0[71:88]) / 131072 43 | cprlat_odd = common.bin2int(msgbin1[54:71]) / 131072 44 | cprlon_odd = common.bin2int(msgbin1[71:88]) / 131072 45 | 46 | air_d_lat_even = 90 / 60 47 | air_d_lat_odd = 90 / 59 48 | 49 | # compute latitude index 'j' 50 | j = common.floor(59 * cprlat_even - 60 * cprlat_odd + 0.5) 51 | 52 | # solution for north hemisphere 53 | lat_even_n = float(air_d_lat_even * (j % 60 + cprlat_even)) 54 | lat_odd_n = float(air_d_lat_odd * (j % 59 + cprlat_odd)) 55 | 56 | # solution for north hemisphere 57 | lat_even_s = lat_even_n - 90 58 | lat_odd_s = lat_odd_n - 90 59 | 60 | # chose which solution corrispondes to receiver location 61 | lat_even = lat_even_n if lat_ref > 0 else lat_even_s 62 | lat_odd = lat_odd_n if lat_ref > 0 else lat_odd_s 63 | 64 | # check if both are in the same latidude zone, rare but possible 65 | if common.cprNL(lat_even) != common.cprNL(lat_odd): 66 | return None 67 | 68 | # compute ni, longitude index m, and longitude 69 | # (people pass int+int or datetime+datetime) 70 | if t0 > t1: # type: ignore 71 | lat = lat_even 72 | nl = common.cprNL(lat_even) 73 | ni = max(common.cprNL(lat_even) - 0, 1) 74 | m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5) 75 | lon = (90 / ni) * (m % ni + cprlon_even) 76 | else: 77 | lat = lat_odd 78 | nl = common.cprNL(lat_odd) 79 | ni = max(common.cprNL(lat_odd) - 1, 1) 80 | m = common.floor(cprlon_even * (nl - 1) - cprlon_odd * nl + 0.5) 81 | lon = (90 / ni) * (m % ni + cprlon_odd) 82 | 83 | # four possible longitude solutions 84 | lons = [lon, lon + 90, lon + 180, lon + 270] 85 | 86 | # make sure lons are between -180 and 180 87 | lons = [(lon + 180) % 360 - 180 for lon in lons] 88 | 89 | # the closest solution to receiver is the correct one 90 | dls = [abs(lon_ref - lon) for lon in lons] 91 | imin = min(range(4), key=dls.__getitem__) 92 | lon = lons[imin] 93 | 94 | return lat, lon 95 | 96 | 97 | def surface_position_with_ref( 98 | msg: str, lat_ref: float, lon_ref: float 99 | ) -> tuple[float, float]: 100 | """Decode surface position with only one message, 101 | knowing reference nearby location, such as previously calculated location, 102 | ground station, or airport location, etc. The reference position shall 103 | be within 45NM of the true position. 104 | 105 | Args: 106 | msg (str): even message (28 hexdigits) 107 | lat_ref: previous known latitude 108 | lon_ref: previous known longitude 109 | 110 | Returns: 111 | (float, float): (latitude, longitude) of the aircraft 112 | """ 113 | 114 | mb = common.hex2bin(msg)[32:] 115 | 116 | cprlat = common.bin2int(mb[22:39]) / 131072 117 | cprlon = common.bin2int(mb[39:56]) / 131072 118 | 119 | i = int(mb[21]) 120 | d_lat = 90 / 59 if i else 90 / 60 121 | 122 | # From 1090 MOPS, Vol.1 DO-260C, A.1.7.6 123 | j = common.floor(0.5 + lat_ref / d_lat - cprlat) 124 | 125 | lat = d_lat * (j + cprlat) 126 | 127 | ni = common.cprNL(lat) - i 128 | 129 | if ni > 0: 130 | d_lon = 90 / ni 131 | else: 132 | d_lon = 90 133 | 134 | m = common.floor(0.5 + lon_ref / d_lon - cprlon) 135 | 136 | lon = d_lon * (m + cprlon) 137 | 138 | return lat, lon 139 | 140 | 141 | def surface_velocity( 142 | msg: str, source: bool = False 143 | ) -> tuple[None | float, None | float, int, str]: 144 | """Decode surface velocity from a surface position message 145 | 146 | Args: 147 | msg (str): 28 hexdigits string 148 | source (boolean): Include direction and vertical rate sources in return. 149 | Default to False. 150 | If set to True, the function will return six value instead of four. 151 | 152 | Returns: 153 | int, float, int, string, [string], [string]: 154 | - Speed (kt) 155 | - Angle (degree), ground track 156 | - Vertical rate, always 0 157 | - Speed type ('GS' for ground speed, 'AS' for airspeed) 158 | - [Optional] Direction source ('TRUE_NORTH') 159 | - [Optional] Vertical rate source (None) 160 | 161 | """ 162 | tc = common.typecode(msg) 163 | if tc is None or tc < 5 or tc > 8: 164 | raise RuntimeError("%s: Not a surface message, expecting 5 124: 179 | spd = None 180 | elif mov == 1: 181 | spd = 0.0 182 | elif mov == 124: 183 | spd = 175.0 184 | else: 185 | mov_lb = [2, 9, 13, 39, 94, 109, 124] 186 | kts_lb: list[float] = [0.125, 1, 2, 15, 70, 100, 175] 187 | step: list[float] = [0.125, 0.25, 0.5, 1, 2, 5] 188 | i = next(m[0] for m in enumerate(mov_lb) if m[1] > mov) 189 | spd = kts_lb[i - 1] + (mov - mov_lb[i - 1]) * step[i - 1] 190 | 191 | if source: 192 | return spd, trk, 0, "GS", "TRUE_NORTH", None # type: ignore 193 | else: 194 | return spd, trk, 0, "GS" 195 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds08.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 0,8 3 | # ADS-B TC=1-4 4 | # Aircraft identification and category 5 | # ------------------------------------------ 6 | 7 | from ... import common 8 | 9 | 10 | def category(msg: str) -> int: 11 | """Aircraft category number 12 | 13 | Args: 14 | msg (str): 28 hexdigits string 15 | 16 | Returns: 17 | int: category number 18 | """ 19 | 20 | tc = common.typecode(msg) 21 | if tc is None or tc < 1 or tc > 4: 22 | raise RuntimeError("%s: Not a identification message" % msg) 23 | 24 | msgbin = common.hex2bin(msg) 25 | mebin = msgbin[32:87] 26 | return common.bin2int(mebin[5:8]) 27 | 28 | 29 | def callsign(msg: str) -> str: 30 | """Aircraft callsign 31 | 32 | Args: 33 | msg (str): 28 hexdigits string 34 | 35 | Returns: 36 | string: callsign 37 | """ 38 | tc = common.typecode(msg) 39 | 40 | if tc is None or tc < 1 or tc > 4: 41 | raise RuntimeError("%s: Not a identification message" % msg) 42 | 43 | chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######" 44 | msgbin = common.hex2bin(msg) 45 | csbin = msgbin[40:96] 46 | 47 | cs = "" 48 | cs += chars[common.bin2int(csbin[0:6])] 49 | cs += chars[common.bin2int(csbin[6:12])] 50 | cs += chars[common.bin2int(csbin[12:18])] 51 | cs += chars[common.bin2int(csbin[18:24])] 52 | cs += chars[common.bin2int(csbin[24:30])] 53 | cs += chars[common.bin2int(csbin[30:36])] 54 | cs += chars[common.bin2int(csbin[36:42])] 55 | cs += chars[common.bin2int(csbin[42:48])] 56 | 57 | # clean string, remove spaces and marks, if any. 58 | # cs = cs.replace('_', '') 59 | cs = cs.replace("#", "") 60 | return cs 61 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds09.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 0,9 3 | # ADS-B TC=19 4 | # Aircraft Airborne velocity 5 | # ------------------------------------------ 6 | 7 | from __future__ import annotations 8 | 9 | import math 10 | 11 | from ... import common 12 | 13 | 14 | def airborne_velocity( 15 | msg: str, source: bool = False 16 | ) -> None | tuple[None | int, None | float, None | int, str]: 17 | """Decode airborne velocity. 18 | 19 | Args: 20 | msg (str): 28 hexdigits string 21 | source (boolean): Include direction and vertical rate sources in return. 22 | Default to False. 23 | If set to True, the function will return six value instead of four. 24 | 25 | Returns: 26 | int, float, int, string, [string], [string]: 27 | - Speed (kt) 28 | - Angle (degree), either ground track or heading 29 | - Vertical rate (ft/min) 30 | - Speed type ('GS' for ground speed, 'AS' for airspeed) 31 | - [Optional] Direction source ('TRUE_NORTH' or 'MAGNETIC_NORTH') 32 | - [Optional] Vertical rate source ('BARO' or 'GNSS') 33 | 34 | """ 35 | if common.typecode(msg) != 19: 36 | raise RuntimeError( 37 | "%s: Not a airborne velocity message, expecting TC=19" % msg 38 | ) 39 | 40 | mb = common.hex2bin(msg)[32:] 41 | 42 | subtype = common.bin2int(mb[5:8]) 43 | 44 | if common.bin2int(mb[14:24]) == 0 or common.bin2int(mb[25:35]) == 0: 45 | return None 46 | 47 | trk_or_hdg: None | float 48 | spd: None | float 49 | 50 | if subtype in (1, 2): 51 | v_ew = common.bin2int(mb[14:24]) 52 | v_ns = common.bin2int(mb[25:35]) 53 | 54 | if v_ew == 0 or v_ns == 0: 55 | spd = None 56 | trk_or_hdg = None 57 | vs = None 58 | else: 59 | v_ew_sign = -1 if mb[13] == "1" else 1 60 | v_ew = v_ew - 1 # east-west velocity 61 | if subtype == 2: # Supersonic 62 | v_ew *= 4 63 | 64 | v_ns_sign = -1 if mb[24] == "1" else 1 65 | v_ns = v_ns - 1 # north-south velocity 66 | if subtype == 2: # Supersonic 67 | v_ns *= 4 68 | 69 | v_we = v_ew_sign * v_ew 70 | v_sn = v_ns_sign * v_ns 71 | 72 | spd = math.sqrt(v_sn * v_sn + v_we * v_we) # unit in kts 73 | spd = int(spd) 74 | 75 | trk = math.atan2(v_we, v_sn) 76 | trk = math.degrees(trk) # convert to degrees 77 | trk = trk if trk >= 0 else trk + 360 # no negative val 78 | 79 | trk_or_hdg = trk 80 | 81 | spd_type = "GS" 82 | dir_type = "TRUE_NORTH" 83 | 84 | else: 85 | if mb[13] == "0": 86 | hdg = None 87 | else: 88 | hdg = common.bin2int(mb[14:24]) / 1024 * 360.0 89 | 90 | trk_or_hdg = hdg 91 | 92 | spd = common.bin2int(mb[25:35]) 93 | spd = None if spd == 0 else spd - 1 94 | if subtype == 4 and spd is not None: # Supersonic 95 | spd *= 4 96 | 97 | if mb[24] == "0": 98 | spd_type = "IAS" 99 | else: 100 | spd_type = "TAS" 101 | 102 | dir_type = "MAGNETIC_NORTH" 103 | 104 | vr_source = "GNSS" if mb[35] == "0" else "BARO" 105 | vr_sign = -1 if mb[36] == "1" else 1 106 | vr = common.bin2int(mb[37:46]) 107 | vs = None if vr == 0 else int(vr_sign * (vr - 1) * 64) 108 | 109 | if source: 110 | return ( # type: ignore 111 | spd, 112 | trk_or_hdg, 113 | vs, 114 | spd_type, 115 | dir_type, 116 | vr_source, 117 | ) 118 | else: 119 | return spd, trk_or_hdg, vs, spd_type 120 | 121 | 122 | def altitude_diff(msg: str) -> None | float: 123 | """Decode the differece between GNSS and barometric altitude. 124 | 125 | Args: 126 | msg (str): 28 hexdigits string, TC=19 127 | 128 | Returns: 129 | int: Altitude difference in feet. Negative value indicates GNSS altitude 130 | below barometric altitude. 131 | 132 | """ 133 | tc = common.typecode(msg) 134 | 135 | if tc is None or tc != 19: 136 | raise RuntimeError( 137 | "%s: Not a airborne velocity message, expecting TC=19" % msg 138 | ) 139 | 140 | msgbin = common.hex2bin(msg) 141 | sign = -1 if int(msgbin[80]) else 1 142 | value = common.bin2int(msgbin[81:88]) 143 | 144 | if value == 0 or value == 127: 145 | return None 146 | else: 147 | return sign * (value - 1) * 25 # in ft. 148 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds10.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 1,0 3 | # Data link capability report 4 | # ------------------------------------------ 5 | 6 | 7 | from ... import common 8 | 9 | 10 | def is10(msg: str) -> bool: 11 | """Check if a message is likely to be BDS code 1,0 12 | 13 | Args: 14 | msg (str): 28 hexdigits string 15 | 16 | Returns: 17 | bool: True or False 18 | """ 19 | 20 | if common.allzeros(msg): 21 | return False 22 | 23 | d = common.hex2bin(common.data(msg)) 24 | 25 | # first 8 bits must be 0x10 26 | if d[0:8] != "00010000": 27 | return False 28 | 29 | # bit 10 to 14 are reserved 30 | if common.bin2int(d[9:14]) != 0: 31 | return False 32 | 33 | # overlay capability conflict 34 | if d[14] == "1" and common.bin2int(d[16:23]) < 5: 35 | return False 36 | if d[14] == "0" and common.bin2int(d[16:23]) > 4: 37 | return False 38 | 39 | return True 40 | 41 | 42 | def ovc10(msg: str) -> int: 43 | """Return the overlay control capability 44 | 45 | Args: 46 | msg (str): 28 hexdigits string 47 | 48 | Returns: 49 | int: Whether the transponder is OVC capable 50 | """ 51 | d = common.hex2bin(common.data(msg)) 52 | 53 | return int(d[14]) 54 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds17.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 1,7 3 | # Common usage GICB capability report 4 | # ------------------------------------------ 5 | 6 | from typing import List 7 | 8 | from ... import common 9 | 10 | 11 | def is17(msg: str) -> bool: 12 | """Check if a message is likely to be BDS code 1,7 13 | 14 | Args: 15 | msg (str): 28 hexdigits string 16 | 17 | Returns: 18 | bool: True or False 19 | """ 20 | 21 | if common.allzeros(msg): 22 | return False 23 | 24 | d = common.hex2bin(common.data(msg)) 25 | 26 | if common.bin2int(d[24:56]) != 0: 27 | return False 28 | 29 | caps = cap17(msg) 30 | 31 | # basic BDS codes for ADS-B shall be supported 32 | # assuming ADS-B out is installed (2017EU/2020US mandate) 33 | # if not set(['BDS05', 'BDS06', 'BDS08', 'BDS09', 'BDS20']).issubset(caps): 34 | # return False 35 | 36 | # at least you can respond who you are 37 | if "BDS20" not in caps: 38 | return False 39 | 40 | return True 41 | 42 | 43 | def cap17(msg: str) -> List[str]: 44 | """Extract capacities from BDS 1,7 message 45 | 46 | Args: 47 | msg (str): 28 hexdigits string 48 | 49 | Returns: 50 | list: list of supported BDS codes 51 | """ 52 | allbds = [ 53 | "05", 54 | "06", 55 | "07", 56 | "08", 57 | "09", 58 | "0A", 59 | "20", 60 | "21", 61 | "40", 62 | "41", 63 | "42", 64 | "43", 65 | "44", 66 | "45", 67 | "48", 68 | "50", 69 | "51", 70 | "52", 71 | "53", 72 | "54", 73 | "55", 74 | "56", 75 | "5F", 76 | "60", 77 | ] 78 | 79 | d = common.hex2bin(common.data(msg)) 80 | idx = [i for i, v in enumerate(d[:24]) if v == "1"] 81 | capacity = ["BDS" + allbds[i] for i in idx] 82 | 83 | return capacity 84 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds20.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 2,0 3 | # Aircraft identification 4 | # ------------------------------------------ 5 | 6 | from ... import common 7 | 8 | 9 | def is20(msg: str) -> bool: 10 | """Check if a message is likely to be BDS code 2,0 11 | 12 | Args: 13 | msg (str): 28 hexdigits string 14 | 15 | Returns: 16 | bool: True or False 17 | """ 18 | 19 | if common.allzeros(msg): 20 | return False 21 | 22 | d = common.hex2bin(common.data(msg)) 23 | 24 | if d[0:8] != "00100000": 25 | return False 26 | 27 | # allow empty callsign 28 | if common.bin2int(d[8:56]) == 0: 29 | return True 30 | 31 | if "#" in cs20(msg): 32 | return False 33 | 34 | return True 35 | 36 | 37 | def cs20(msg: str) -> str: 38 | """Aircraft callsign 39 | 40 | Args: 41 | msg (str): 28 hexdigits string 42 | 43 | Returns: 44 | string: callsign, max. 8 chars 45 | """ 46 | chars = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######" 47 | 48 | d = common.hex2bin(common.data(msg)) 49 | 50 | cs = "" 51 | cs += chars[common.bin2int(d[8:14])] 52 | cs += chars[common.bin2int(d[14:20])] 53 | cs += chars[common.bin2int(d[20:26])] 54 | cs += chars[common.bin2int(d[26:32])] 55 | cs += chars[common.bin2int(d[32:38])] 56 | cs += chars[common.bin2int(d[38:44])] 57 | cs += chars[common.bin2int(d[44:50])] 58 | cs += chars[common.bin2int(d[50:56])] 59 | 60 | return cs 61 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds30.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 3,0 3 | # ACAS active resolution advisory 4 | # ------------------------------------------ 5 | 6 | from ... import common 7 | 8 | 9 | def is30(msg: str) -> bool: 10 | """Check if a message is likely to be BDS code 3,0 11 | 12 | Args: 13 | msg (str): 28 hexdigits string 14 | 15 | Returns: 16 | bool: True or False 17 | """ 18 | 19 | if common.allzeros(msg): 20 | return False 21 | 22 | d = common.hex2bin(common.data(msg)) 23 | 24 | if d[0:8] != "00110000": 25 | return False 26 | 27 | # threat type 3 not assigned 28 | if d[28:30] == "11": 29 | return False 30 | 31 | # reserved for ACAS III, in far future 32 | if common.bin2int(d[15:22]) >= 48: 33 | return False 34 | 35 | return True 36 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds40.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 4,0 3 | # Selected vertical intention 4 | # ------------------------------------------ 5 | 6 | import warnings 7 | from typing import Optional 8 | 9 | from ... import common 10 | 11 | 12 | def is40(msg: str) -> bool: 13 | """Check if a message is likely to be BDS code 4,0 14 | 15 | Args: 16 | msg (str): 28 hexdigits string 17 | 18 | Returns: 19 | bool: True or False 20 | """ 21 | 22 | if common.allzeros(msg): 23 | return False 24 | 25 | d = common.hex2bin(common.data(msg)) 26 | 27 | # status bit 1, 14, and 27 28 | 29 | if common.wrongstatus(d, 1, 2, 13): 30 | return False 31 | 32 | if common.wrongstatus(d, 14, 15, 26): 33 | return False 34 | 35 | if common.wrongstatus(d, 27, 28, 39): 36 | return False 37 | 38 | if common.wrongstatus(d, 48, 49, 51): 39 | return False 40 | 41 | if common.wrongstatus(d, 54, 55, 56): 42 | return False 43 | 44 | # bits 40-47 and 52-53 shall all be zero 45 | 46 | if common.bin2int(d[39:47]) != 0: 47 | return False 48 | 49 | if common.bin2int(d[51:53]) != 0: 50 | return False 51 | 52 | return True 53 | 54 | 55 | def selalt40mcp(msg: str) -> Optional[int]: 56 | """Selected altitude, MCP/FCU 57 | 58 | Args: 59 | msg (str): 28 hexdigits string 60 | 61 | Returns: 62 | int: altitude in feet 63 | """ 64 | d = common.hex2bin(common.data(msg)) 65 | 66 | if d[0] == "0": 67 | return None 68 | 69 | alt = common.bin2int(d[1:13]) * 16 # ft 70 | return alt 71 | 72 | 73 | def selalt40fms(msg: str) -> Optional[int]: 74 | """Selected altitude, FMS 75 | 76 | Args: 77 | msg (str): 28 hexdigits string 78 | 79 | Returns: 80 | int: altitude in feet 81 | """ 82 | d = common.hex2bin(common.data(msg)) 83 | 84 | if d[13] == "0": 85 | return None 86 | 87 | alt = common.bin2int(d[14:26]) * 16 # ft 88 | return alt 89 | 90 | 91 | def p40baro(msg: str) -> Optional[float]: 92 | """Barometric pressure setting 93 | 94 | Args: 95 | msg (str): 28 hexdigits string 96 | 97 | Returns: 98 | float: pressure in millibar 99 | """ 100 | d = common.hex2bin(common.data(msg)) 101 | 102 | if d[26] == "0": 103 | return None 104 | 105 | p = common.bin2int(d[27:39]) * 0.1 + 800 # millibar 106 | return p 107 | 108 | 109 | def alt40mcp(msg: str) -> Optional[int]: 110 | warnings.warn( 111 | """alt40mcp() has been renamed to selalt40mcp(). 112 | It will be removed in the future.""", 113 | DeprecationWarning, 114 | ) 115 | return selalt40mcp(msg) 116 | 117 | 118 | def alt40fms(msg: str) -> Optional[int]: 119 | warnings.warn( 120 | """alt40fms() has been renamed to selalt40fms(). 121 | It will be removed in the future.""", 122 | DeprecationWarning, 123 | ) 124 | return selalt40fms(msg) 125 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds44.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 4,4 3 | # Meteorological routine air report 4 | # ------------------------------------------ 5 | 6 | from typing import Optional, Tuple 7 | 8 | from ... import common 9 | 10 | 11 | def is44(msg: str) -> bool: 12 | """Check if a message is likely to be BDS code 4,4. 13 | 14 | Meteorological routine air report 15 | 16 | Args: 17 | msg (str): 28 hexdigits string 18 | 19 | Returns: 20 | bool: True or False 21 | 22 | """ 23 | if common.allzeros(msg): 24 | return False 25 | 26 | d = common.hex2bin(common.data(msg)) 27 | 28 | # status bit 5, 35, 47, 50 29 | if common.wrongstatus(d, 5, 6, 23): 30 | return False 31 | 32 | if common.wrongstatus(d, 35, 36, 46): 33 | return False 34 | 35 | if common.wrongstatus(d, 47, 48, 49): 36 | return False 37 | 38 | if common.wrongstatus(d, 50, 51, 56): 39 | return False 40 | 41 | # Bits 1-4 indicate source, values > 4 reserved and should not occur 42 | if common.bin2int(d[0:4]) > 4: 43 | return False 44 | 45 | vw, dw = wind44(msg) 46 | if vw is not None and vw > 250: 47 | return False 48 | 49 | temp, temp2 = temp44(msg) 50 | if min(temp, temp2) > 60 or max(temp, temp2) < -80: 51 | return False 52 | 53 | return True 54 | 55 | 56 | def wind44(msg: str) -> Tuple[Optional[int], Optional[float]]: 57 | """Wind speed and direction. 58 | 59 | Args: 60 | msg (str): 28 hexdigits string 61 | 62 | Returns: 63 | (int, float): speed (kt), direction (degree) 64 | 65 | """ 66 | d = common.hex2bin(common.data(msg)) 67 | 68 | status = int(d[4]) 69 | if not status: 70 | return None, None 71 | 72 | speed = common.bin2int(d[5:14]) # knots 73 | direction = common.bin2int(d[14:23]) * 180 / 256 # degree 74 | 75 | return speed, direction 76 | 77 | 78 | def temp44(msg: str) -> Tuple[float, float]: 79 | """Static air temperature. 80 | 81 | Args: 82 | msg (str): 28 hexdigits string 83 | 84 | Returns: 85 | float, float: temperature and alternative temperature in Celsius degree. 86 | Note: Two values returns due to what seems to be an inconsistency 87 | error in ICAO 9871 (2008) Appendix A-67. 88 | 89 | """ 90 | d = common.hex2bin(common.data(msg)) 91 | 92 | sign = int(d[23]) 93 | value = common.bin2int(d[24:34]) 94 | 95 | if sign: 96 | value = value - 1024 97 | 98 | temp = value * 0.25 # celsius 99 | 100 | temp_alternative = value * 0.125 # celsius 101 | 102 | return temp, temp_alternative 103 | 104 | 105 | def p44(msg: str) -> Optional[int]: 106 | """Static pressure. 107 | 108 | Args: 109 | msg (str): 28 hexdigits string 110 | 111 | Returns: 112 | int: static pressure in hPa 113 | 114 | """ 115 | d = common.hex2bin(common.data(msg)) 116 | 117 | if d[34] == "0": 118 | return None 119 | 120 | p = common.bin2int(d[35:46]) # hPa 121 | 122 | return p 123 | 124 | 125 | def hum44(msg: str) -> Optional[float]: 126 | """humidity 127 | 128 | Args: 129 | msg (str): 28 hexdigits string 130 | 131 | Returns: 132 | float: percentage of humidity, [0 - 100] % 133 | """ 134 | d = common.hex2bin(common.data(msg)) 135 | 136 | if d[49] == "0": 137 | return None 138 | 139 | hm = common.bin2int(d[50:56]) * 100 / 64 # % 140 | 141 | return hm 142 | 143 | 144 | def turb44(msg: str) -> Optional[int]: 145 | """Turbulence. 146 | 147 | Args: 148 | msg (str): 28 hexdigits string 149 | 150 | Returns: 151 | int: turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe 152 | 153 | """ 154 | d = common.hex2bin(common.data(msg)) 155 | 156 | if d[46] == "0": 157 | return None 158 | 159 | turb = common.bin2int(d[47:49]) 160 | 161 | return turb 162 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds45.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 4,5 3 | # Meteorological hazard report 4 | # ------------------------------------------ 5 | 6 | from typing import Optional 7 | 8 | from ... import common 9 | 10 | 11 | def is45(msg: str) -> bool: 12 | """Check if a message is likely to be BDS code 4,5. 13 | 14 | Meteorological hazard report 15 | 16 | Args: 17 | msg (str): 28 hexdigits string 18 | 19 | Returns: 20 | bool: True or False 21 | 22 | """ 23 | if common.allzeros(msg): 24 | return False 25 | 26 | d = common.hex2bin(common.data(msg)) 27 | 28 | # status bit 1, 4, 7, 10, 13, 16, 27, 39 29 | if common.wrongstatus(d, 1, 2, 3): 30 | return False 31 | 32 | if common.wrongstatus(d, 4, 5, 6): 33 | return False 34 | 35 | if common.wrongstatus(d, 7, 8, 9): 36 | return False 37 | 38 | if common.wrongstatus(d, 10, 11, 12): 39 | return False 40 | 41 | if common.wrongstatus(d, 13, 14, 15): 42 | return False 43 | 44 | if common.wrongstatus(d, 16, 17, 26): 45 | return False 46 | 47 | if common.wrongstatus(d, 27, 28, 38): 48 | return False 49 | 50 | if common.wrongstatus(d, 39, 40, 51): 51 | return False 52 | 53 | # reserved 54 | if common.bin2int(d[51:56]) != 0: 55 | return False 56 | 57 | temp = temp45(msg) 58 | if temp: 59 | if temp > 60 or temp < -80: 60 | return False 61 | 62 | return True 63 | 64 | 65 | def turb45(msg: str) -> Optional[int]: 66 | """Turbulence. 67 | 68 | Args: 69 | msg (str): 28 hexdigits string 70 | 71 | Returns: 72 | int: Turbulence level. 0=NIL, 1=Light, 2=Moderate, 3=Severe 73 | 74 | """ 75 | d = common.hex2bin(common.data(msg)) 76 | if d[0] == "0": 77 | return None 78 | 79 | turb = common.bin2int(d[1:3]) 80 | return turb 81 | 82 | 83 | def ws45(msg: str) -> Optional[int]: 84 | """Wind shear. 85 | 86 | Args: 87 | msg (str): 28 hexdigits string 88 | 89 | Returns: 90 | int: Wind shear level. 0=NIL, 1=Light, 2=Moderate, 3=Severe 91 | 92 | """ 93 | d = common.hex2bin(common.data(msg)) 94 | if d[3] == "0": 95 | return None 96 | 97 | ws = common.bin2int(d[4:6]) 98 | return ws 99 | 100 | 101 | def mb45(msg: str) -> Optional[int]: 102 | """Microburst. 103 | 104 | Args: 105 | msg (str): 28 hexdigits string 106 | 107 | Returns: 108 | int: Microburst level. 0=NIL, 1=Light, 2=Moderate, 3=Severe 109 | 110 | """ 111 | d = common.hex2bin(common.data(msg)) 112 | if d[6] == "0": 113 | return None 114 | 115 | mb = common.bin2int(d[7:9]) 116 | return mb 117 | 118 | 119 | def ic45(msg: str) -> Optional[int]: 120 | """Icing. 121 | 122 | Args: 123 | msg (str): 28 hexdigits string 124 | 125 | Returns: 126 | int: Icing level. 0=NIL, 1=Light, 2=Moderate, 3=Severe 127 | 128 | """ 129 | d = common.hex2bin(common.data(msg)) 130 | if d[9] == "0": 131 | return None 132 | 133 | ic = common.bin2int(d[10:12]) 134 | return ic 135 | 136 | 137 | def wv45(msg: str) -> Optional[int]: 138 | """Wake vortex. 139 | 140 | Args: 141 | msg (str): 28 hexdigits string 142 | 143 | Returns: 144 | int: Wake vortex level. 0=NIL, 1=Light, 2=Moderate, 3=Severe 145 | 146 | """ 147 | d = common.hex2bin(common.data(msg)) 148 | if d[12] == "0": 149 | return None 150 | 151 | ws = common.bin2int(d[13:15]) 152 | return ws 153 | 154 | 155 | def temp45(msg: str) -> Optional[float]: 156 | """Static air temperature. 157 | 158 | Args: 159 | msg (str): 28 hexdigits string 160 | 161 | Returns: 162 | float: tmeperature in Celsius degree 163 | 164 | """ 165 | d = common.hex2bin(common.data(msg)) 166 | 167 | sign = int(d[16]) 168 | value = common.bin2int(d[17:26]) 169 | 170 | if sign: 171 | value = value - 512 172 | 173 | temp = value * 0.25 # celsius 174 | 175 | return temp 176 | 177 | 178 | def p45(msg: str) -> Optional[int]: 179 | """Average static pressure. 180 | 181 | Args: 182 | msg (str): 28 hexdigits string 183 | 184 | Returns: 185 | int: static pressure in hPa 186 | 187 | """ 188 | d = common.hex2bin(common.data(msg)) 189 | if d[26] == "0": 190 | return None 191 | p = common.bin2int(d[27:38]) # hPa 192 | return p 193 | 194 | 195 | def rh45(msg: str) -> Optional[int]: 196 | """Radio height. 197 | 198 | Args: 199 | msg (str): 28 hexdigits string 200 | 201 | Returns: 202 | int: radio height in ft 203 | 204 | """ 205 | d = common.hex2bin(common.data(msg)) 206 | if d[38] == "0": 207 | return None 208 | rh = common.bin2int(d[39:51]) * 16 209 | return rh 210 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds50.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 5,0 3 | # Track and turn report 4 | # ------------------------------------------ 5 | 6 | from typing import Optional 7 | 8 | from ... import common 9 | 10 | 11 | def is50(msg: str) -> bool: 12 | """Check if a message is likely to be BDS code 5,0 13 | (Track and turn report) 14 | 15 | Args: 16 | msg (str): 28 hexdigits string 17 | 18 | Returns: 19 | bool: True or False 20 | """ 21 | 22 | if common.allzeros(msg): 23 | return False 24 | 25 | d = common.hex2bin(common.data(msg)) 26 | 27 | # status bit 1, 12, 24, 35, 46 28 | 29 | if common.wrongstatus(d, 1, 3, 11): 30 | return False 31 | 32 | if common.wrongstatus(d, 12, 13, 23): 33 | return False 34 | 35 | if common.wrongstatus(d, 24, 25, 34): 36 | return False 37 | 38 | if common.wrongstatus(d, 35, 36, 45): 39 | return False 40 | 41 | if common.wrongstatus(d, 46, 47, 56): 42 | return False 43 | 44 | roll = roll50(msg) 45 | if (roll is not None) and abs(roll) > 50: 46 | return False 47 | 48 | gs = gs50(msg) 49 | if gs is not None and gs > 600: 50 | return False 51 | 52 | tas = tas50(msg) 53 | if tas is not None and tas > 600: 54 | return False 55 | 56 | if (gs is not None) and (tas is not None) and (abs(tas - gs) > 200): 57 | return False 58 | 59 | return True 60 | 61 | 62 | def roll50(msg: str) -> Optional[float]: 63 | """Roll angle, BDS 5,0 message 64 | 65 | Args: 66 | msg (str): 28 hexdigits string 67 | 68 | Returns: 69 | float: angle in degrees, 70 | negative->left wing down, positive->right wing down 71 | """ 72 | d = common.hex2bin(common.data(msg)) 73 | 74 | if d[0] == "0": 75 | return None 76 | 77 | sign = int(d[1]) # 1 -> left wing down 78 | value = common.bin2int(d[2:11]) 79 | 80 | if sign: 81 | value = value - 512 82 | 83 | angle = value * 45 / 256 # degree 84 | return angle 85 | 86 | 87 | def trk50(msg: str) -> Optional[float]: 88 | """True track angle, BDS 5,0 message 89 | 90 | Args: 91 | msg (str): 28 hexdigits string 92 | 93 | Returns: 94 | float: angle in degrees to true north (from 0 to 360) 95 | """ 96 | d = common.hex2bin(common.data(msg)) 97 | 98 | if d[11] == "0": 99 | return None 100 | 101 | sign = int(d[12]) # 1 -> west 102 | value = common.bin2int(d[13:23]) 103 | 104 | if sign: 105 | value = value - 1024 106 | 107 | trk = value * 90 / 512.0 108 | 109 | # convert from [-180, 180] to [0, 360] 110 | if trk < 0: 111 | trk = 360 + trk 112 | 113 | return trk 114 | 115 | 116 | def gs50(msg: str) -> Optional[float]: 117 | """Ground speed, BDS 5,0 message 118 | 119 | Args: 120 | msg (str): 28 hexdigits string 121 | 122 | Returns: 123 | int: ground speed in knots 124 | """ 125 | d = common.hex2bin(common.data(msg)) 126 | 127 | if d[23] == "0": 128 | return None 129 | 130 | spd = common.bin2int(d[24:34]) * 2 # kts 131 | return spd 132 | 133 | 134 | def rtrk50(msg: str) -> Optional[float]: 135 | """Track angle rate, BDS 5,0 message 136 | 137 | Args: 138 | msg (str): 28 hexdigits string 139 | 140 | Returns: 141 | float: angle rate in degrees/second 142 | """ 143 | d = common.hex2bin(common.data(msg)) 144 | 145 | if d[34] == "0": 146 | return None 147 | 148 | sign = int(d[35]) # 1 -> negative value, two's complement 149 | value = common.bin2int(d[36:45]) 150 | 151 | if sign: 152 | value = value - 512 153 | 154 | angle = value * 8 / 256 # degree / sec 155 | return angle 156 | 157 | 158 | def tas50(msg: str) -> Optional[float]: 159 | """Aircraft true airspeed, BDS 5,0 message 160 | 161 | Args: 162 | msg (str): 28 hexdigits string 163 | 164 | Returns: 165 | int: true airspeed in knots 166 | """ 167 | d = common.hex2bin(common.data(msg)) 168 | 169 | if d[45] == "0": 170 | return None 171 | 172 | tas = common.bin2int(d[46:56]) * 2 # kts 173 | return tas 174 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds53.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 5,3 3 | # Air-referenced state vector 4 | # ------------------------------------------ 5 | 6 | from typing import Optional 7 | 8 | from ... import common 9 | 10 | 11 | def is53(msg: str) -> bool: 12 | """Check if a message is likely to be BDS code 5,3 13 | (Air-referenced state vector) 14 | 15 | Args: 16 | msg (str): 28 hexdigits string 17 | 18 | Returns: 19 | bool: True or False 20 | """ 21 | 22 | if common.allzeros(msg): 23 | return False 24 | 25 | d = common.hex2bin(common.data(msg)) 26 | 27 | # status bit 1, 13, 24, 34, 47 28 | 29 | if common.wrongstatus(d, 1, 3, 12): 30 | return False 31 | 32 | if common.wrongstatus(d, 13, 14, 23): 33 | return False 34 | 35 | if common.wrongstatus(d, 24, 25, 33): 36 | return False 37 | 38 | if common.wrongstatus(d, 34, 35, 46): 39 | return False 40 | 41 | if common.wrongstatus(d, 47, 49, 56): 42 | return False 43 | 44 | ias = ias53(msg) 45 | if ias is not None and ias > 500: 46 | return False 47 | 48 | mach = mach53(msg) 49 | if mach is not None and mach > 1: 50 | return False 51 | 52 | tas = tas53(msg) 53 | if tas is not None and tas > 500: 54 | return False 55 | 56 | vr = vr53(msg) 57 | if vr is not None and abs(vr) > 8000: 58 | return False 59 | 60 | return True 61 | 62 | 63 | def hdg53(msg: str) -> Optional[float]: 64 | """Magnetic heading, BDS 5,3 message 65 | 66 | Args: 67 | msg (str): 28 hexdigits string 68 | 69 | Returns: 70 | float: angle in degrees to true north (from 0 to 360) 71 | """ 72 | d = common.hex2bin(common.data(msg)) 73 | 74 | if d[0] == "0": 75 | return None 76 | 77 | sign = int(d[1]) # 1 -> west 78 | value = common.bin2int(d[2:12]) 79 | 80 | if sign: 81 | value = value - 1024 82 | 83 | hdg = value * 90 / 512 # degree 84 | 85 | # convert from [-180, 180] to [0, 360] 86 | if hdg < 0: 87 | hdg = 360 + hdg 88 | 89 | return hdg 90 | 91 | 92 | def ias53(msg: str) -> Optional[float]: 93 | """Indicated airspeed, DBS 5,3 message 94 | 95 | Args: 96 | msg (str): 28 hexdigits 97 | 98 | Returns: 99 | int: indicated arispeed in knots 100 | """ 101 | d = common.hex2bin(common.data(msg)) 102 | 103 | if d[12] == "0": 104 | return None 105 | 106 | ias = common.bin2int(d[13:23]) # knots 107 | return ias 108 | 109 | 110 | def mach53(msg: str) -> Optional[float]: 111 | """MACH number, DBS 5,3 message 112 | 113 | Args: 114 | msg (str): 28 hexdigits 115 | 116 | Returns: 117 | float: MACH number 118 | """ 119 | d = common.hex2bin(common.data(msg)) 120 | 121 | if d[23] == "0": 122 | return None 123 | 124 | mach = common.bin2int(d[24:33]) * 0.008 125 | return mach 126 | 127 | 128 | def tas53(msg: str) -> Optional[float]: 129 | """Aircraft true airspeed, BDS 5,3 message 130 | 131 | Args: 132 | msg (str): 28 hexdigits 133 | 134 | Returns: 135 | float: true airspeed in knots 136 | """ 137 | d = common.hex2bin(common.data(msg)) 138 | 139 | if d[33] == "0": 140 | return None 141 | 142 | tas = common.bin2int(d[34:46]) * 0.5 # kts 143 | return tas 144 | 145 | 146 | def vr53(msg: str) -> Optional[int]: 147 | """Vertical rate 148 | 149 | Args: 150 | msg (str): 28 hexdigits (BDS60) string 151 | 152 | Returns: 153 | int: vertical rate in feet/minutes 154 | """ 155 | d = common.hex2bin(common.data(msg)) 156 | 157 | if d[46] == "0": 158 | return None 159 | 160 | sign = int(d[47]) # 1 -> negative value, two's complement 161 | value = common.bin2int(d[48:56]) 162 | 163 | if value == 0 or value == 255: # all zeros or all ones 164 | return 0 165 | 166 | value = value - 256 if sign else value 167 | roc = value * 64 # feet/min 168 | 169 | return roc 170 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds60.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 6,0 3 | # Heading and speed report 4 | # ------------------------------------------ 5 | 6 | from typing import Optional 7 | 8 | from ... import common 9 | from ...extra import aero 10 | 11 | 12 | def is60(msg: str) -> bool: 13 | """Check if a message is likely to be BDS code 6,0 14 | 15 | Args: 16 | msg (str): 28 hexdigits string 17 | 18 | Returns: 19 | bool: True or False 20 | """ 21 | 22 | if common.allzeros(msg): 23 | return False 24 | 25 | d = common.hex2bin(common.data(msg)) 26 | 27 | # status bit 1, 13, 24, 35, 46 28 | 29 | if common.wrongstatus(d, 1, 2, 12): 30 | return False 31 | 32 | if common.wrongstatus(d, 13, 14, 23): 33 | return False 34 | 35 | if common.wrongstatus(d, 24, 25, 34): 36 | return False 37 | 38 | if common.wrongstatus(d, 35, 36, 45): 39 | return False 40 | 41 | if common.wrongstatus(d, 46, 47, 56): 42 | return False 43 | 44 | ias = ias60(msg) 45 | if ias is not None and ias > 500: 46 | return False 47 | 48 | mach = mach60(msg) 49 | if mach is not None and mach > 1: 50 | return False 51 | 52 | vr_baro = vr60baro(msg) 53 | if vr_baro is not None and abs(vr_baro) > 6000: 54 | return False 55 | 56 | vr_ins = vr60ins(msg) 57 | if vr_ins is not None and abs(vr_ins) > 6000: 58 | return False 59 | 60 | # additional check knowing altitude 61 | if (mach is not None) and (ias is not None) and (common.df(msg) == 20): 62 | alt = common.altcode(msg) 63 | if alt is not None: 64 | ias_ = aero.mach2cas(mach, alt * aero.ft) / aero.kts 65 | if abs(ias - ias_) > 20: 66 | return False 67 | 68 | return True 69 | 70 | 71 | def hdg60(msg: str) -> Optional[float]: 72 | """Megnetic heading of aircraft 73 | 74 | Args: 75 | msg (str): 28 hexdigits string 76 | 77 | Returns: 78 | float: heading in degrees to megnetic north (from 0 to 360) 79 | """ 80 | d = common.hex2bin(common.data(msg)) 81 | 82 | if d[0] == "0": 83 | return None 84 | 85 | sign = int(d[1]) # 1 -> west 86 | value = common.bin2int(d[2:12]) 87 | 88 | if sign: 89 | value = value - 1024 90 | 91 | hdg = value * 90 / 512 # degree 92 | 93 | # convert from [-180, 180] to [0, 360] 94 | if hdg < 0: 95 | hdg = 360 + hdg 96 | 97 | return hdg 98 | 99 | 100 | def ias60(msg: str) -> Optional[float]: 101 | """Indicated airspeed 102 | 103 | Args: 104 | msg (str): 28 hexdigits string 105 | 106 | Returns: 107 | int: indicated airspeed in knots 108 | """ 109 | d = common.hex2bin(common.data(msg)) 110 | 111 | if d[12] == "0": 112 | return None 113 | 114 | ias = common.bin2int(d[13:23]) # kts 115 | return ias 116 | 117 | 118 | def mach60(msg: str) -> Optional[float]: 119 | """Aircraft MACH number 120 | 121 | Args: 122 | msg (str): 28 hexdigits string 123 | 124 | Returns: 125 | float: MACH number 126 | """ 127 | d = common.hex2bin(common.data(msg)) 128 | 129 | if d[23] == "0": 130 | return None 131 | 132 | mach = common.bin2int(d[24:34]) * 2.048 / 512.0 133 | return mach 134 | 135 | 136 | def vr60baro(msg: str) -> Optional[int]: 137 | """Vertical rate from barometric measurement, this value may be very noisy. 138 | 139 | Args: 140 | msg (str): 28 hexdigits string 141 | 142 | Returns: 143 | int: vertical rate in feet/minutes 144 | """ 145 | d = common.hex2bin(common.data(msg)) 146 | 147 | if d[34] == "0": 148 | return None 149 | 150 | sign = int(d[35]) # 1 -> negative value, two's complement 151 | value = common.bin2int(d[36:45]) 152 | 153 | if sign: 154 | value = value - 512 155 | 156 | roc = value * 32 # feet/min 157 | return roc 158 | 159 | 160 | def vr60ins(msg: str) -> Optional[int]: 161 | """Vertical rate measurd by onbard equiments (IRS, AHRS) 162 | 163 | Args: 164 | msg (str): 28 hexdigits string 165 | 166 | Returns: 167 | int: vertical rate in feet/minutes 168 | """ 169 | d = common.hex2bin(common.data(msg)) 170 | 171 | if d[45] == "0": 172 | return None 173 | 174 | sign = int(d[46]) # 1 -> negative value, two's complement 175 | value = common.bin2int(d[47:56]) 176 | 177 | if sign: 178 | value = value - 512 179 | 180 | roc = value * 32 # feet/min 181 | return roc 182 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/bds/bds61.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------ 2 | # BDS 6,1 3 | # ADS-B TC=28 4 | # Aircraft Airborne status 5 | # ------------------------------------------ 6 | 7 | from ... import common 8 | 9 | 10 | def is_emergency(msg: str) -> bool: 11 | """Check if the aircraft is reporting an emergency. 12 | 13 | Non-emergencies are either a subtype of zero (no information) or 14 | subtype of one and a value of zero (no emergency). 15 | Subtype = 2 indicates an ACAS RA broadcast, look in BDS 3,0 16 | 17 | :param msg: 28 bytes hexadecimal message string 18 | :return: if the aircraft has declared an emergency 19 | """ 20 | if common.typecode(msg) != 28: 21 | raise RuntimeError( 22 | "%s: Not an airborne status message, expecting TC=28" % msg 23 | ) 24 | 25 | mb = common.hex2bin(msg)[32:] 26 | subtype = common.bin2int(mb[5:8]) 27 | 28 | if subtype == 2: 29 | raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented") 30 | 31 | emergency_state = common.bin2int(mb[8:11]) 32 | 33 | if subtype == 1 and emergency_state == 1: 34 | return True 35 | else: 36 | return False 37 | 38 | 39 | def emergency_state(msg: str) -> int: 40 | """Decode aircraft emergency state. 41 | 42 | Value Meaning 43 | ----- ----------------------- 44 | 0 No emergency 45 | 1 General emergency 46 | 2 Lifeguard/Medical 47 | 3 Minimum fuel 48 | 4 No communications 49 | 5 Unlawful communications 50 | 6-7 Reserved 51 | 52 | :param msg: 28 bytes hexadecimal message string 53 | :return: emergency state 54 | """ 55 | 56 | mb = common.hex2bin(msg)[32:] 57 | subtype = common.bin2int(mb[5:8]) 58 | 59 | if subtype == 2: 60 | raise RuntimeError("%s: Emergency message is ACAS-RA, not implemented") 61 | 62 | emergency_state = common.bin2int(mb[8:11]) 63 | return emergency_state 64 | 65 | 66 | def emergency_squawk(msg: str) -> str: 67 | """Decode squawk code. 68 | 69 | Emergency value 1: squawk 7700. 70 | Emergency value 4: squawk 7600. 71 | Emergency value 5: squawk 7500. 72 | 73 | :param msg: 28 bytes hexadecimal message string 74 | :return: aircraft squawk code 75 | """ 76 | if common.typecode(msg) != 28: 77 | raise RuntimeError( 78 | "%s: Not an airborne status message, expecting TC=28" % msg 79 | ) 80 | 81 | msgbin = common.hex2bin(msg) 82 | 83 | # construct the 13 bits Mode A ID code 84 | idcode = msgbin[43:56] 85 | 86 | squawk = common.squawk(idcode) 87 | return squawk 88 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/commb.py: -------------------------------------------------------------------------------- 1 | """Comm-B module. 2 | 3 | The Comm-B module imports all functions from the following modules: 4 | 5 | ELS - elementary surveillance 6 | 7 | - pyModeS.decoder.bds.bds10 8 | - pyModeS.decoder.bds.bds17 9 | - pyModeS.decoder.bds.bds20 10 | - pyModeS.decoder.bds.bds30 11 | 12 | EHS - enhanced surveillance 13 | 14 | - pyModeS.decoder.bds.bds40 15 | - pyModeS.decoder.bds.bds50 16 | - pyModeS.decoder.bds.bds60 17 | 18 | MRAR and MHR 19 | 20 | - pyModeS.decoder.bds.bds44 21 | - pyModeS.decoder.bds.bds45 22 | 23 | """ 24 | 25 | # ELS - elementary surveillance 26 | from .bds.bds10 import is10, ovc10 27 | from .bds.bds17 import is17, cap17 28 | from .bds.bds20 import is20, cs20 29 | from .bds.bds30 import is30 30 | 31 | # ELS - enhanced surveillance 32 | from .bds.bds40 import ( 33 | is40, 34 | selalt40fms, 35 | selalt40mcp, 36 | p40baro, 37 | alt40fms, 38 | alt40mcp, 39 | ) 40 | from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50 41 | from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins 42 | 43 | # MRAR and MHR 44 | from .bds.bds44 import is44, wind44, temp44, p44, hum44, turb44 45 | from .bds.bds45 import is45, turb45, ws45, mb45, ic45, wv45, temp45, p45, rh45 46 | 47 | __all__ = [ 48 | "is10", 49 | "ovc10", 50 | "is17", 51 | "cap17", 52 | "is20", 53 | "cs20", 54 | "is30", 55 | "is40", 56 | "selalt40fms", 57 | "selalt40mcp", 58 | "p40baro", 59 | "alt40fms", 60 | "alt40mcp", 61 | "is50", 62 | "roll50", 63 | "trk50", 64 | "gs50", 65 | "rtrk50", 66 | "tas50", 67 | "is60", 68 | "hdg60", 69 | "ias60", 70 | "mach60", 71 | "vr60baro", 72 | "vr60ins", 73 | "is44", 74 | "wind44", 75 | "temp44", 76 | "p44", 77 | "hum44", 78 | "turb44", 79 | "is45", 80 | "turb45", 81 | "ws45", 82 | "mb45", 83 | "ic45", 84 | "wv45", 85 | "temp45", 86 | "p45", 87 | "rh45", 88 | ] 89 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/ehs.py: -------------------------------------------------------------------------------- 1 | """EHS Wrapper. 2 | 3 | ``pyModeS.ehs`` is deprecated, please use ``pyModeS.commb`` instead. 4 | 5 | The EHS wrapper imports all functions from the following modules: 6 | - pyModeS.decoder.bds.bds40 7 | - pyModeS.decoder.bds.bds50 8 | - pyModeS.decoder.bds.bds60 9 | 10 | """ 11 | 12 | import warnings 13 | 14 | from .bds.bds40 import ( 15 | is40, 16 | selalt40fms, 17 | selalt40mcp, 18 | p40baro, 19 | alt40fms, 20 | alt40mcp, 21 | ) 22 | from .bds.bds50 import is50, roll50, trk50, gs50, rtrk50, tas50 23 | from .bds.bds60 import is60, hdg60, ias60, mach60, vr60baro, vr60ins 24 | from .bds import infer 25 | 26 | __all__ = [ 27 | "is40", 28 | "selalt40fms", 29 | "selalt40mcp", 30 | "p40baro", 31 | "alt40fms", 32 | "alt40mcp", 33 | "is50", 34 | "roll50", 35 | "trk50", 36 | "gs50", 37 | "rtrk50", 38 | "tas50", 39 | "is60", 40 | "hdg60", 41 | "ias60", 42 | "mach60", 43 | "vr60baro", 44 | "vr60ins", 45 | "infer", 46 | ] 47 | 48 | warnings.simplefilter("once", DeprecationWarning) 49 | warnings.warn( 50 | "pms.ehs module is deprecated. Please use pms.commb instead.", 51 | DeprecationWarning, 52 | ) 53 | 54 | 55 | def BDS(msg): 56 | warnings.warn( 57 | "pms.ehs.BDS() is deprecated, use pms.bds.infer() instead.", 58 | DeprecationWarning, 59 | ) 60 | return infer(msg) 61 | 62 | 63 | def icao(msg): 64 | from . import common 65 | 66 | return common.icao(msg) 67 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/els.py: -------------------------------------------------------------------------------- 1 | """ELS Wrapper. 2 | 3 | ``pyModeS.els`` is deprecated, please use ``pyModeS.commb`` instead. 4 | 5 | The ELS wrapper imports all functions from the following modules: 6 | - pyModeS.decoder.bds.bds10 7 | - pyModeS.decoder.bds.bds17 8 | - pyModeS.decoder.bds.bds20 9 | - pyModeS.decoder.bds.bds30 10 | 11 | """ 12 | 13 | import warnings 14 | 15 | from .bds.bds10 import is10, ovc10 16 | from .bds.bds17 import cap17, is17 17 | from .bds.bds20 import cs20, is20 18 | from .bds.bds30 import is30 19 | 20 | warnings.simplefilter("once", DeprecationWarning) 21 | warnings.warn( 22 | "pms.els module is deprecated. Please use pms.commb instead.", 23 | DeprecationWarning, 24 | ) 25 | 26 | 27 | __all__ = [ 28 | "is10", 29 | "ovc10", 30 | "is17", 31 | "cap17", 32 | "is20", 33 | "cs20", 34 | "is30", 35 | ] 36 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/flarm/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | from typing_extensions import Annotated 3 | 4 | from .decode import flarm as flarm_decode 5 | 6 | __all__ = ["DecodedMessage", "flarm"] 7 | 8 | 9 | class DecodedMessage(TypedDict): 10 | timestamp: int 11 | icao24: str 12 | latitude: float 13 | longitude: float 14 | altitude: Annotated[int, "m"] 15 | vertical_speed: Annotated[float, "m/s"] 16 | groundspeed: int 17 | track: int 18 | type: str 19 | sensorLatitude: float 20 | sensorLongitude: float 21 | isIcao24: bool 22 | noTrack: bool 23 | stealth: bool 24 | 25 | 26 | flarm = flarm_decode 27 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/flarm/core.c: -------------------------------------------------------------------------------- 1 | #include "core.h" 2 | 3 | /* 4 | * 5 | * https://pastebin.com/YK2f8bfm 6 | * 7 | * NEW ENCRYPTION 8 | * 9 | * Swiss glider anti-colission system moved to a new encryption scheme: XXTEA 10 | * The algorithm encrypts all the packet after the header: total 20 bytes or 5 long int words of data 11 | * 12 | * XXTEA description and code are found here: http://en.wikipedia.org/wiki/XXTEA 13 | * The system uses 6 iterations of the main loop. 14 | * 15 | * The system version 6 sends two type of packets: position and ... some unknown data 16 | * The difference is made by bit 0 of byte 3 of the packet: for position data this bit is zero. 17 | * 18 | * For position data the key used depends on the time and transmitting device address. 19 | * The key is as well obscured by a weird algorithm. 20 | * The code to generate the key is: 21 | * 22 | * */ 23 | 24 | void make_key(int *key, long time, long address) 25 | { 26 | const long key1[4] = {0xe43276df, 0xdca83759, 0x9802b8ac, 0x4675a56b}; 27 | const long key1b[4] = {0xfc78ea65, 0x804b90ea, 0xb76542cd, 0x329dfa32}; 28 | const long *table = ((((time >> 23) & 255) & 0x01) != 0) ? key1b : key1; 29 | 30 | for (int i = 0; i < 4; i++) 31 | { 32 | key[i] = obscure(table[i] ^ ((time >> 6) ^ address), 0x045D9F3B) ^ 0x87B562F4; 33 | } 34 | } 35 | 36 | long obscure(long key, unsigned long seed) 37 | { 38 | unsigned int m1 = seed * (key ^ (key >> 16)); 39 | unsigned int m2 = seed * (m1 ^ (m1 >> 16)); 40 | return m2 ^ (m2 >> 16); 41 | } 42 | 43 | /* 44 | * NEW PACKET FORMAT: 45 | * 46 | * Byte Bits 47 | * 0 AAAA AAAA device address 48 | * 1 AAAA AAAA 49 | * 2 AAAA AAAA 50 | * 3 00aa 0000 aa = 10 or 01 51 | * 52 | * 4 vvvv vvvv vertical speed 53 | * 5 xxxx xxvv 54 | * 6 gggg gggg GPS status 55 | * 7 tttt gggg plane type 56 | * 57 | * 8 LLLL LLLL Latitude 58 | * 9 LLLL LLLL 59 | * 10 aaaa aLLL 60 | * 11 aaaa aaaa Altitude 61 | * 62 | * 12 NNNN NNNN Longitude 63 | * 13 NNNN NNNN 64 | * 14 xxxx NNNN 65 | * 15 FFxx xxxx multiplying factor 66 | * 67 | * 16 SSSS SSSS as in version 4 68 | * 17 ssss ssss 69 | * 18 KKKK KKKK 70 | * 19 kkkk kkkk 71 | * 72 | * 20 EEEE EEEE 73 | * 21 eeee eeee 74 | * 22 PPPP PPPP 75 | * 24 pppp pppp 76 | * */ 77 | 78 | /* 79 | * https://en.wikipedia.org/wiki/XXTEA 80 | */ 81 | 82 | void btea(uint32_t *v, int n, uint32_t const key[4]) 83 | { 84 | uint32_t y, z, sum; 85 | unsigned p, rounds, e; 86 | if (n > 1) 87 | { /* Coding Part */ 88 | /* Unused, should remove? */ 89 | rounds = 6 + 52 / n; 90 | sum = 0; 91 | z = v[n - 1]; 92 | do 93 | { 94 | sum += DELTA; 95 | e = (sum >> 2) & 3; 96 | for (p = 0; p < (unsigned)n - 1; p++) 97 | { 98 | y = v[p + 1]; 99 | z = v[p] += MX; 100 | } 101 | y = v[0]; 102 | z = v[n - 1] += MX; 103 | } while (--rounds); 104 | } 105 | else if (n < -1) 106 | { /* Decoding Part */ 107 | n = -n; 108 | rounds = 6; // + 52 / n; 109 | sum = rounds * DELTA; 110 | y = v[0]; 111 | do 112 | { 113 | e = (sum >> 2) & 3; 114 | for (p = n - 1; p > 0; p--) 115 | { 116 | z = v[p - 1]; 117 | y = v[p] -= MX; 118 | } 119 | z = v[n - 1]; 120 | y = v[0] -= MX; 121 | sum -= DELTA; 122 | } while (--rounds); 123 | } 124 | } -------------------------------------------------------------------------------- /src/pyModeS/decoder/flarm/core.h: -------------------------------------------------------------------------------- 1 | #ifndef __CORE_H__ 2 | #define __CORE_H__ 3 | 4 | #include 5 | 6 | #define DELTA 0x9e3779b9 7 | #define MX (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z))) 8 | 9 | void make_key(int *key, long time, long address); 10 | long obscure(long key, unsigned long seed); 11 | void btea(uint32_t *v, int n, uint32_t const key[4]); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/flarm/core.pxd: -------------------------------------------------------------------------------- 1 | 2 | cdef extern from "core.h": 3 | void make_key(int*, long time, long address) 4 | void btea(int*, int, int*) -------------------------------------------------------------------------------- /src/pyModeS/decoder/flarm/decode.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from . import DecodedMessage 4 | 5 | AIRCRAFT_TYPES: list[str] 6 | 7 | 8 | def flarm( 9 | timestamp: int, 10 | msg: str, 11 | refLat: float, 12 | refLon: float, 13 | **kwargs: Any, 14 | ) -> DecodedMessage: ... 15 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/flarm/decode.pyx: -------------------------------------------------------------------------------- 1 | from cpython cimport array 2 | 3 | from .core cimport make_key as c_make_key, btea as c_btea 4 | 5 | import array 6 | import math 7 | from ctypes import c_byte 8 | from textwrap import wrap 9 | 10 | AIRCRAFT_TYPES = [ 11 | "Unknown", # 0 12 | "Glider", # 1 13 | "Tow-Plane", # 2 14 | "Helicopter", # 3 15 | "Parachute", # 4 16 | "Parachute Drop-Plane", # 5 17 | "Hangglider", # 6 18 | "Paraglider", # 7 19 | "Aircraft", # 8 20 | "Jet", # 9 21 | "UFO", # 10 22 | "Balloon", # 11 23 | "Airship", # 12 24 | "UAV", # 13 25 | "Reserved", # 14 26 | "Static Obstacle", # 15 27 | ] 28 | 29 | cdef long bytearray2int(str icao24): 30 | return ( 31 | (int(icao24[4:6], 16) & 0xFF) 32 | | ((int(icao24[2:4], 16) & 0xFF) << 8) 33 | | ((int(icao24[:2], 16) & 0xFF) << 16) 34 | ) 35 | 36 | cpdef array.array make_key(long timestamp, str icao24): 37 | cdef long addr = bytearray2int(icao24) 38 | cdef array.array a = array.array('i', [0, 0, 0, 0]) 39 | c_make_key(a.data.as_ints, timestamp, (addr << 8) & 0xffffff) 40 | return a 41 | 42 | cpdef array.array btea(long timestamp, str msg): 43 | cdef int p 44 | cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2] 45 | cdef array.array key = make_key(timestamp, icao24) 46 | 47 | pieces = wrap(msg[8:], 8) 48 | cdef array.array toDecode = array.array('i', len(pieces) * [0]) 49 | for i, piece in enumerate(pieces): 50 | p = 0 51 | for elt in wrap(piece, 2)[::-1]: 52 | p = (p << 8) + int(elt, 16) 53 | toDecode[i] = p 54 | 55 | c_btea(toDecode.data.as_ints, -5, key.data.as_ints) 56 | return toDecode 57 | 58 | cdef float velocity(int ns, int ew): 59 | return math.hypot(ew / 4, ns / 4) 60 | 61 | def heading(ns, ew, velocity): 62 | if velocity < 1e-6: 63 | velocity = 1 64 | return (math.atan2(ew / velocity / 4, ns / velocity / 4) / 0.01745) % 360 65 | 66 | def turningRate(a1, a2): 67 | return ((((a2 - a1)) + 540) % 360) - 180 68 | 69 | def flarm(long timestamp, str msg, float refLat, float refLon, **kwargs): 70 | """Decode a FLARM message. 71 | 72 | Args: 73 | timestamp (int) 74 | msg (str) 75 | refLat (float): the receiver's location 76 | refLon (float): the receiver's location 77 | 78 | Returns: 79 | a dictionary with all decoded fields. Any extra keyword argument passed 80 | is included in the output dictionary. 81 | """ 82 | cdef str icao24 = msg[4:6] + msg[2:4] + msg[:2] 83 | cdef int magic = int(msg[6:8], 16) 84 | 85 | if magic != 0x10 and magic != 0x20: 86 | return None 87 | 88 | cdef array.array decoded = btea(timestamp, msg) 89 | 90 | cdef int aircraft_type = (decoded[0] >> 28) & 0xF 91 | cdef int gps = (decoded[0] >> 16) & 0xFFF 92 | cdef int raw_vs = c_byte(decoded[0] & 0x3FF).value 93 | 94 | noTrack = ((decoded[0] >> 14) & 0x1) == 1 95 | stealth = ((decoded[0] >> 13) & 0x1) == 1 96 | 97 | cdef int altitude = (decoded[1] >> 19) & 0x1FFF 98 | 99 | cdef int lat = decoded[1] & 0x7FFFF 100 | 101 | cdef int mult_factor = 1 << ((decoded[2] >> 30) & 0x3) 102 | cdef int lon = decoded[2] & 0xFFFFF 103 | 104 | ns = list( 105 | c_byte((decoded[3] >> (i * 8)) & 0xFF).value * mult_factor 106 | for i in range(4) 107 | ) 108 | ew = list( 109 | c_byte((decoded[4] >> (i * 8)) & 0xFF).value * mult_factor 110 | for i in range(4) 111 | ) 112 | 113 | cdef int roundLat = int(refLat * 1e7) >> 7 114 | lat = (lat - roundLat) % 0x080000 115 | if lat >= 0x040000: 116 | lat -= 0x080000 117 | lat = (((lat + roundLat) << 7) + 0x40) 118 | 119 | roundLon = int(refLon * 1e7) >> 7 120 | lon = (lon - roundLon) % 0x100000 121 | if lon >= 0x080000: 122 | lon -= 0x100000 123 | lon = (((lon + roundLon) << 7) + 0x40) 124 | 125 | speed = sum(velocity(n, e) for n, e in zip(ns, ew)) / 4 126 | 127 | heading4 = heading(ns[0], ew[0], speed) 128 | heading8 = heading(ns[1], ew[1], speed) 129 | 130 | return dict( 131 | timestamp=timestamp, 132 | icao24=icao24, 133 | latitude=lat * 1e-7, 134 | longitude=lon * 1e-7, 135 | geoaltitude=altitude, 136 | vertical_speed=raw_vs * mult_factor / 10, 137 | groundspeed=speed, 138 | track=heading4 - 4 * turningRate(heading4, heading8) / 4, 139 | type=AIRCRAFT_TYPES[aircraft_type], 140 | sensorLatitude=refLat, 141 | sensorLongitude=refLon, 142 | isIcao24=magic==0x10, 143 | noTrack=noTrack, 144 | stealth=stealth, 145 | gps=gps, 146 | **kwargs 147 | ) -------------------------------------------------------------------------------- /src/pyModeS/decoder/surv.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decode short roll call surveillance replies, with downlink format 4 or 5 3 | """ 4 | 5 | from __future__ import annotations 6 | from typing import Callable, TypeVar 7 | 8 | from .. import common 9 | 10 | T = TypeVar("T") 11 | F = Callable[[str], T] 12 | 13 | 14 | def _checkdf(func: F[T]) -> F[T]: 15 | """Ensure downlink format is 4 or 5.""" 16 | 17 | def wrapper(msg: str) -> T: 18 | df = common.df(msg) 19 | if df not in [4, 5]: 20 | raise RuntimeError( 21 | "Incorrect downlink format, expect 4 or 5, got {}".format(df) 22 | ) 23 | return func(msg) 24 | 25 | return wrapper 26 | 27 | 28 | @_checkdf 29 | def fs(msg: str) -> tuple[int, str]: 30 | """Decode flight status. 31 | 32 | Args: 33 | msg (str): 14 hexdigits string 34 | Returns: 35 | int, str: flight status, description 36 | 37 | """ 38 | msgbin = common.hex2bin(msg) 39 | fs = common.bin2int(msgbin[5:8]) 40 | text = "" 41 | 42 | if fs == 0: 43 | text = "no alert, no SPI, aircraft is airborne" 44 | elif fs == 1: 45 | text = "no alert, no SPI, aircraft is on-ground" 46 | elif fs == 2: 47 | text = "alert, no SPI, aircraft is airborne" 48 | elif fs == 3: 49 | text = "alert, no SPI, aircraft is on-ground" 50 | elif fs == 4: 51 | text = "alert, SPI, aircraft is airborne or on-ground" 52 | elif fs == 5: 53 | text = "no alert, SPI, aircraft is airborne or on-ground" 54 | 55 | return fs, text 56 | 57 | 58 | @_checkdf 59 | def dr(msg: str) -> tuple[int, str]: 60 | """Decode downlink request. 61 | 62 | Args: 63 | msg (str): 14 hexdigits string 64 | Returns: 65 | int, str: downlink request, description 66 | 67 | """ 68 | msgbin = common.hex2bin(msg) 69 | dr = common.bin2int(msgbin[8:13]) 70 | 71 | text = "" 72 | 73 | if dr == 0: 74 | text = "no downlink request" 75 | elif dr == 1: 76 | text = "request to send Comm-B message" 77 | elif dr == 4: 78 | text = "Comm-B broadcast 1 available" 79 | elif dr == 5: 80 | text = "Comm-B broadcast 2 available" 81 | elif dr >= 16: 82 | text = "ELM downlink segments available: {}".format(dr - 15) 83 | 84 | return dr, text 85 | 86 | 87 | @_checkdf 88 | def um(msg: str) -> tuple[int, int, None | str]: 89 | """Decode utility message. 90 | 91 | Utility message contains interrogator identifier and reservation type. 92 | 93 | Args: 94 | msg (str): 14 hexdigits string 95 | Returns: 96 | int, str: interrogator identifier code that triggered the reply, and 97 | reservation type made by the interrogator 98 | """ 99 | msgbin = common.hex2bin(msg) 100 | iis = common.bin2int(msgbin[13:17]) 101 | ids = common.bin2int(msgbin[17:19]) 102 | if ids == 0: 103 | ids_text = None 104 | if ids == 1: 105 | ids_text = "Comm-B interrogator identifier code" 106 | if ids == 2: 107 | ids_text = "Comm-C interrogator identifier code" 108 | if ids == 3: 109 | ids_text = "Comm-D interrogator identifier code" 110 | return iis, ids, ids_text 111 | 112 | 113 | @_checkdf 114 | def altitude(msg: str) -> None | int: 115 | """Decode altitude. 116 | 117 | Args: 118 | msg (String): 14 hexdigits string 119 | 120 | Returns: 121 | int: altitude in ft 122 | 123 | """ 124 | return common.altcode(msg) 125 | 126 | 127 | @_checkdf 128 | def identity(msg: str) -> str: 129 | """Decode squawk code. 130 | 131 | Args: 132 | msg (String): 14 hexdigits string 133 | 134 | Returns: 135 | string: squawk code 136 | 137 | """ 138 | return common.idcode(msg) 139 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/uncertainty.py: -------------------------------------------------------------------------------- 1 | """Uncertainty parameters. 2 | 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import sys 8 | 9 | if sys.version_info < (3, 8): 10 | from typing_extensions import TypedDict 11 | else: 12 | from typing import TypedDict 13 | 14 | NA = None 15 | 16 | TC_NUCp_lookup = { 17 | 0: 0, 18 | 5: 9, 19 | 6: 8, 20 | 7: 7, 21 | 8: 6, 22 | 9: 9, 23 | 10: 8, 24 | 11: 7, 25 | 12: 6, 26 | 13: 5, 27 | 14: 4, 28 | 15: 3, 29 | 16: 2, 30 | 17: 1, 31 | 18: 0, 32 | 20: 9, 33 | 21: 8, 34 | 22: 0, 35 | } 36 | 37 | TC_NICv1_lookup: dict[int, int | dict[int, int]] = { 38 | 5: 11, 39 | 6: 10, 40 | 7: 9, 41 | 8: 0, 42 | 9: 11, 43 | 10: 10, 44 | 11: {1: 9, 0: 8}, 45 | 12: 7, 46 | 13: 6, 47 | 14: 5, 48 | 15: 4, 49 | 16: {1: 3, 0: 2}, 50 | 17: 1, 51 | 18: 0, 52 | 20: 11, 53 | 21: 10, 54 | 22: 0, 55 | } 56 | 57 | TC_NICv2_lookup: dict[int, int | dict[int, int]] = { 58 | 5: 11, 59 | 6: 10, 60 | 7: {2: 9, 0: 8}, 61 | 8: {3: 7, 2: 6, 1: 6, 0: 0}, 62 | 9: 11, 63 | 10: 10, 64 | 11: {3: 9, 0: 8}, 65 | 12: 7, 66 | 13: 6, 67 | 14: 5, 68 | 15: 4, 69 | 16: {3: 3, 0: 2}, 70 | 17: 1, 71 | 18: 0, 72 | 20: 11, 73 | 21: 10, 74 | 22: 0, 75 | } 76 | 77 | 78 | class NUCpEntry(TypedDict): 79 | HPL: None | float 80 | RCu: None | int 81 | RCv: None | int 82 | 83 | 84 | NUCp: dict[int, NUCpEntry] = { 85 | 9: {"HPL": 7.5, "RCu": 3, "RCv": 4}, 86 | 8: {"HPL": 25, "RCu": 10, "RCv": 15}, 87 | 7: {"HPL": 185, "RCu": 93, "RCv": NA}, 88 | 6: {"HPL": 370, "RCu": 185, "RCv": NA}, 89 | 5: {"HPL": 926, "RCu": 463, "RCv": NA}, 90 | 4: {"HPL": 1852, "RCu": 926, "RCv": NA}, 91 | 3: {"HPL": 3704, "RCu": 1852, "RCv": NA}, 92 | 2: {"HPL": 18520, "RCu": 9260, "RCv": NA}, 93 | 1: {"HPL": 37040, "RCu": 18520, "RCv": NA}, 94 | 0: {"HPL": NA, "RCu": NA, "RCv": NA}, 95 | } 96 | 97 | 98 | class NUCvEntry(TypedDict): 99 | HVE: None | float 100 | VVE: None | float 101 | 102 | 103 | NUCv: dict[int, NUCvEntry] = { 104 | 0: {"HVE": NA, "VVE": NA}, 105 | 1: {"HVE": 10, "VVE": 15.2}, 106 | 2: {"HVE": 3, "VVE": 4.5}, 107 | 3: {"HVE": 1, "VVE": 1.5}, 108 | 4: {"HVE": 0.3, "VVE": 0.46}, 109 | } 110 | 111 | 112 | class NACpEntry(TypedDict): 113 | EPU: None | int 114 | VEPU: None | int 115 | 116 | 117 | NACp: dict[int, NACpEntry] = { 118 | 11: {"EPU": 3, "VEPU": 4}, 119 | 10: {"EPU": 10, "VEPU": 15}, 120 | 9: {"EPU": 30, "VEPU": 45}, 121 | 8: {"EPU": 93, "VEPU": NA}, 122 | 7: {"EPU": 185, "VEPU": NA}, 123 | 6: {"EPU": 556, "VEPU": NA}, 124 | 5: {"EPU": 926, "VEPU": NA}, 125 | 4: {"EPU": 1852, "VEPU": NA}, 126 | 3: {"EPU": 3704, "VEPU": NA}, 127 | 2: {"EPU": 7408, "VEPU": NA}, 128 | 1: {"EPU": 18520, "VEPU": NA}, 129 | 0: {"EPU": NA, "VEPU": NA}, 130 | } 131 | 132 | 133 | class NACvEntry(TypedDict): 134 | HFOMr: None | float 135 | VFOMr: None | float 136 | 137 | 138 | NACv: dict[int, NACvEntry] = { 139 | 0: {"HFOMr": NA, "VFOMr": NA}, 140 | 1: {"HFOMr": 10, "VFOMr": 15.2}, 141 | 2: {"HFOMr": 3, "VFOMr": 4.5}, 142 | 3: {"HFOMr": 1, "VFOMr": 1.5}, 143 | 4: {"HFOMr": 0.3, "VFOMr": 0.46}, 144 | } 145 | 146 | 147 | class SILEntry(TypedDict): 148 | PE_RCu: None | float 149 | PE_VPL: None | float 150 | 151 | 152 | SIL: dict[int, SILEntry] = { 153 | 3: {"PE_RCu": 1e-7, "PE_VPL": 2e-7}, 154 | 2: {"PE_RCu": 1e-5, "PE_VPL": 1e-5}, 155 | 1: {"PE_RCu": 1e-3, "PE_VPL": 1e-3}, 156 | 0: {"PE_RCu": NA, "PE_VPL": NA}, 157 | } 158 | 159 | 160 | class NICv1Entry(TypedDict): 161 | Rc: None | float 162 | VPL: None | float 163 | 164 | 165 | NICv1: dict[int, dict[int, NICv1Entry]] = { 166 | # NIC is used as the index at second Level 167 | 11: {0: {"Rc": 7.5, "VPL": 11}}, 168 | 10: {0: {"Rc": 25, "VPL": 37.5}}, 169 | 9: {1: {"Rc": 75, "VPL": 112}}, 170 | 8: {0: {"Rc": 185, "VPL": NA}}, 171 | 7: {0: {"Rc": 370, "VPL": NA}}, 172 | 6: {0: {"Rc": 926, "VPL": NA}, 1: {"Rc": 1111, "VPL": NA}}, 173 | 5: {0: {"Rc": 1852, "VPL": NA}}, 174 | 4: {0: {"Rc": 3702, "VPL": NA}}, 175 | 3: {1: {"Rc": 7408, "VPL": NA}}, 176 | 2: {0: {"Rc": 14008, "VPL": NA}}, 177 | 1: {0: {"Rc": 37000, "VPL": NA}}, 178 | 0: {0: {"Rc": NA, "VPL": NA}}, 179 | } 180 | 181 | 182 | class NICv2Entry(TypedDict): 183 | Rc: None | float 184 | 185 | 186 | NICv2: dict[int, dict[int, NICv2Entry]] = { 187 | # Decimal value of [NICa NICb/NICc] is used as the index at second Level 188 | 11: {0: {"Rc": 7.5}}, 189 | 10: {0: {"Rc": 25}}, 190 | 9: {2: {"Rc": 75}, 3: {"Rc": 75}}, 191 | 8: {0: {"Rc": 185}}, 192 | 7: {0: {"Rc": 370}, 3: {"Rc": 370}}, 193 | 6: {0: {"Rc": 926}, 1: {"Rc": 556}, 2: {"Rc": 556}, 3: {"Rc": 1111}}, 194 | 5: {0: {"Rc": 1852}}, 195 | 4: {0: {"Rc": 3702}}, 196 | 3: {3: {"Rc": 7408}}, 197 | 2: {0: {"Rc": 14008}}, 198 | 1: {0: {"Rc": 37000}}, 199 | 0: {0: {"Rc": NA}}, 200 | } 201 | -------------------------------------------------------------------------------- /src/pyModeS/decoder/uplink.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from .. import common 3 | from textwrap import wrap 4 | 5 | 6 | def uplink_icao(msg: str) -> str: 7 | "Calculate the ICAO address from a Mode-S interrogation (uplink message)" 8 | p_gen = 0xFFFA0480 << ((len(msg) - 14) * 4) 9 | data = int(msg[:-6], 16) 10 | PA = int(msg[-6:], 16) 11 | ad = 0 12 | topbit = 0b1 << (len(msg) * 4 - 25) 13 | for j in range(0, len(msg) * 4, 1): 14 | if data & topbit: 15 | data ^= p_gen 16 | data = (data << 1) + ((PA >> 23) & 1) 17 | PA = PA << 1 18 | if j > (len(msg) * 4 - 26): 19 | ad = ad + ((data >> (len(msg) * 4 - 25)) & 1) 20 | ad = ad << 1 21 | return "%06X" % (ad >> 2) 22 | 23 | 24 | def uf(msg: str) -> int: 25 | """Decode Uplink Format value, bits 1 to 5.""" 26 | ufbin = common.hex2bin(msg[:2]) 27 | return min(common.bin2int(ufbin[0:5]), 24) 28 | 29 | 30 | def bds(msg: str) -> Optional[str]: 31 | "Decode requested BDS register from selective (Roll Call) interrogation." 32 | UF = uf(msg) 33 | msgbin = common.hex2bin(msg) 34 | msgbin_split = wrap(msgbin, 8) 35 | mbytes = list(map(common.bin2int, msgbin_split)) 36 | 37 | if UF in {4, 5, 20, 21}: 38 | 39 | di = mbytes[1] & 0x7 # DI - Designator Identification 40 | RR = mbytes[1] >> 3 & 0x1F 41 | if RR > 15: 42 | BDS1 = RR - 16 43 | if di == 7: 44 | RRS = mbytes[2] & 0x0F 45 | BDS2 = RRS 46 | elif di == 3: 47 | RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5) 48 | BDS2 = RRS 49 | else: 50 | # for other values of DI, the BDS2 is assumed 0 51 | # (as per ICAO Annex 10 Vol IV) 52 | BDS2 = 0 53 | 54 | return str(format(BDS1,"X")) + str(format(BDS2,"X")) 55 | else: 56 | return None 57 | else: 58 | return None 59 | 60 | 61 | def pr(msg: str) -> Optional[int]: 62 | """Decode PR (probability of reply) field from All Call interrogation. 63 | Interpretation: 64 | 0 signifies reply with probability of 1 65 | 1 signifies reply with probability of 1/2 66 | 2 signifies reply with probability of 1/4 67 | 3 signifies reply with probability of 1/8 68 | 4 signifies reply with probability of 1/16 69 | 5, 6, 7 not assigned 70 | 8 signifies disregard lockout, reply with probability of 1 71 | 9 signifies disregard lockout, reply with probability of 1/2 72 | 10 signifies disregard lockout, reply with probability of 1/4 73 | 11 signifies disregard lockout, reply with probability of 1/8 74 | 12 signifies disregard lockout, reply with probability of 1/16 75 | 13, 14, 15 not assigned. 76 | """ 77 | msgbin = common.hex2bin(msg) 78 | msgbin_split = wrap(msgbin, 8) 79 | mbytes = list(map(common.bin2int, msgbin_split)) 80 | if uf(msg) == 11: 81 | return ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7) 82 | else: 83 | return None 84 | 85 | 86 | def ic(msg: str) -> Optional[str]: 87 | """Decode IC (interrogator code) from a ground-based interrogation.""" 88 | 89 | UF = uf(msg) 90 | msgbin = common.hex2bin(msg) 91 | msgbin_split = wrap(msgbin, 8) 92 | mbytes = list(map(common.bin2int, msgbin_split)) 93 | IC = None 94 | if UF == 11: 95 | 96 | codeLabel = mbytes[1] & 0x7 97 | icField = (mbytes[1] >> 3) & 0xF 98 | 99 | # Store the Interogator Code 100 | ic_switcher = { 101 | 0: "II" + str(icField), 102 | 1: "SI" + str(icField), 103 | 2: "SI" + str(icField + 16), 104 | 3: "SI" + str(icField + 32), 105 | 4: "SI" + str(icField + 48), 106 | } 107 | IC = ic_switcher.get(codeLabel, "") 108 | 109 | if UF in {4, 5, 20, 21}: 110 | di = mbytes[1] & 0x7 111 | RR = mbytes[1] >> 3 & 0x1F 112 | if RR > 15: 113 | BDS1 = RR - 16 # noqa: F841 114 | if di == 0 or di == 1 or di == 7: 115 | # II 116 | II = (mbytes[2] >> 4) & 0xF 117 | IC = "II" + str(II) 118 | elif di == 3: 119 | # SI 120 | SI = (mbytes[2] >> 2) & 0x3F 121 | IC = "SI" + str(SI) 122 | return IC 123 | 124 | 125 | def lockout(msg): 126 | """Decode the lockout command from selective (Roll Call) interrogation.""" 127 | msgbin = common.hex2bin(msg) 128 | msgbin_split = wrap(msgbin, 8) 129 | mbytes = list(map(common.bin2int, msgbin_split)) 130 | 131 | if uf(msg) in {4, 5, 20, 21}: 132 | lockout = False 133 | di = mbytes[1] & 0x7 134 | if (di == 1 or di == 7): 135 | # LOS 136 | if ((mbytes[3] & 0x40) >> 6) == 1: 137 | lockout = True 138 | elif di == 3: 139 | # LSS 140 | if ((mbytes[2] & 0x2) >> 1) == 1: 141 | lockout = True 142 | return lockout 143 | else: 144 | return None 145 | 146 | 147 | def uplink_fields(msg): 148 | """Decode individual fields of a ground-based interrogation.""" 149 | msgbin = common.hex2bin(msg) 150 | msgbin_split = wrap(msgbin, 8) 151 | mbytes = list(map(common.bin2int, msgbin_split)) 152 | PR = "" 153 | IC = "" 154 | lockout = False 155 | di = "" 156 | RR = "" 157 | RRS = "" 158 | BDS = "" 159 | if uf(msg) == 11: 160 | 161 | # Probability of Reply decoding 162 | 163 | PR = ((mbytes[0] & 0x7) << 1) | ((mbytes[1] & 0x80) >> 7) 164 | 165 | # Get cl and ic bit fields from the data 166 | # Decode the SI or II interrogator code 167 | codeLabel = mbytes[1] & 0x7 168 | icField = (mbytes[1] >> 3) & 0xF 169 | 170 | # Store the Interogator Code 171 | ic_switcher = { 172 | 0: "II" + str(icField), 173 | 1: "SI" + str(icField), 174 | 2: "SI" + str(icField + 16), 175 | 3: "SI" + str(icField + 32), 176 | 4: "SI" + str(icField + 48), 177 | } 178 | IC = ic_switcher.get(codeLabel, "") 179 | 180 | if uf(msg) in {4, 5, 20, 21}: 181 | # Decode the DI and get the lockout information conveniently 182 | # (LSS or LOS) 183 | 184 | # DI - Designator Identification 185 | di = mbytes[1] & 0x7 186 | RR = mbytes[1] >> 3 & 0x1F 187 | if RR > 15: 188 | BDS1 = RR - 16 189 | BDS2 = 0 190 | if di == 0: 191 | # II 192 | II = (mbytes[2] >> 4) & 0xF 193 | IC = "II" + str(II) 194 | elif di == 1: 195 | # II 196 | II = (mbytes[2] >> 4) & 0xF 197 | IC = "II" + str(II) 198 | if ((mbytes[3] & 0x40) >> 6) == 1: 199 | lockout = True 200 | elif di == 7: 201 | # LOS 202 | if ((mbytes[3] & 0x40) >> 6) == 1: 203 | lockout = True 204 | # II 205 | II = (mbytes[2] >> 4) & 0xF 206 | IC = "II" + str(II) 207 | RRS = mbytes[2] & 0x0F 208 | BDS2 = RRS 209 | elif di == 3: 210 | # LSS 211 | if ((mbytes[2] & 0x2) >> 1) == 1: 212 | lockout = True 213 | # SI 214 | SI = (mbytes[2] >> 2) & 0x3F 215 | IC = "SI" + str(SI) 216 | RRS = ((mbytes[2] & 0x1) << 3) | ((mbytes[3] & 0xE0) >> 5) 217 | BDS2 = RRS 218 | if RR > 15: 219 | BDS = str(format(BDS1,"X")) + str(format(BDS2,"X")) 220 | 221 | return { 222 | "DI": di, 223 | "IC": IC, 224 | "LOS": lockout, 225 | "PR": PR, 226 | "RR": RR, 227 | "RRS": RRS, 228 | "BDS": BDS, 229 | } 230 | -------------------------------------------------------------------------------- /src/pyModeS/extra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junzis/pyModeS/6401a3963c1f7a11884b64e23fa2f7b0c72f2b2e/src/pyModeS/extra/__init__.py -------------------------------------------------------------------------------- /src/pyModeS/extra/aero.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions for aeronautics in this module 3 | 4 | - physical quantities always in SI units 5 | - lat,lon,course and heading in degrees 6 | 7 | International Standard Atmosphere 8 | :: 9 | 10 | p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m] 11 | a = vsound(H) # speed of sound [m/s] as function of H[m] 12 | p = pressure(H) # calls atmos but returns only pressure [Pa] 13 | T = temperature(H) # calculates temperature [K] 14 | rho = density(H) # calls atmos but returns only pressure [Pa] 15 | 16 | Speed conversion at altitude H[m] in ISA 17 | :: 18 | 19 | Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion 20 | Vtas = mach2tas(Mach,H) # mach number to true airspeed (Vtas) conversion 21 | Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m] 22 | Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m] 23 | Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m] 24 | Vcas = tas2cas(Vtas,H) # Vtas to Vcas conversion both m/s, H in [m] 25 | Vcas = mach2cas(Mach,H) # Mach to Vcas conversion Vcas in m/s, H in [m] 26 | Mach = cas2mach(Vcas,H) # Vcas to mach copnversion Vcas in m/s, H in [m] 27 | 28 | """ 29 | 30 | import numpy as np 31 | 32 | """Aero and geo Constants """ 33 | kts = 0.514444 # knot -> m/s 34 | ft = 0.3048 # ft -> m 35 | fpm = 0.00508 # ft/min -> m/s 36 | inch = 0.0254 # inch -> m 37 | sqft = 0.09290304 # 1 square foot 38 | nm = 1852 # nautical mile -> m 39 | lbs = 0.453592 # pound -> kg 40 | g0 = 9.80665 # m/s2, Sea level gravity constant 41 | R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA 42 | p0 = 101325 # Pa, air pressure, sea level ISA 43 | rho0 = 1.225 # kg/m3, air density, sea level ISA 44 | T0 = 288.15 # K, temperature, sea level ISA 45 | gamma = 1.40 # cp/cv for air 46 | gamma1 = 0.2 # (gamma-1)/2 for air 47 | gamma2 = 3.5 # gamma/(gamma-1) for air 48 | beta = -0.0065 # [K/m] ISA temp gradient below tropopause 49 | r_earth = 6371000 # m, average earth radius 50 | a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0) 51 | 52 | 53 | def atmos(H): 54 | # H in metres 55 | T = np.maximum(288.15 - 0.0065 * H, 216.65) 56 | rhotrop = 1.225 * (T / 288.15) ** 4.256848030018761 57 | dhstrat = np.maximum(0.0, H - 11000.0) 58 | rho = rhotrop * np.exp(-dhstrat / 6341.552161) 59 | p = rho * R * T 60 | return p, rho, T 61 | 62 | 63 | def temperature(H): 64 | p, r, T = atmos(H) 65 | return T 66 | 67 | 68 | def pressure(H): 69 | p, r, T = atmos(H) 70 | return p 71 | 72 | 73 | def density(H): 74 | p, r, T = atmos(H) 75 | return r 76 | 77 | 78 | def vsound(H): 79 | """Speed of sound""" 80 | T = temperature(H) 81 | a = np.sqrt(gamma * R * T) 82 | return a 83 | 84 | 85 | def distance(lat1, lon1, lat2, lon2, H=0): 86 | """ 87 | Compute spherical distance from spherical coordinates. 88 | 89 | For two locations in spherical coordinates 90 | (1, theta, phi) and (1, theta', phi') 91 | cosine( arc length ) = 92 | sin phi sin phi' cos(theta-theta') + cos phi cos phi' 93 | distance = rho * arc length 94 | """ 95 | 96 | # phi = 90 - latitude 97 | phi1 = np.radians(90 - lat1) 98 | phi2 = np.radians(90 - lat2) 99 | 100 | # theta = longitude 101 | theta1 = np.radians(lon1) 102 | theta2 = np.radians(lon2) 103 | 104 | cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) + np.cos(phi1) * np.cos( 105 | phi2 106 | ) 107 | cos = np.where(cos > 1, 1, cos) 108 | 109 | arc = np.arccos(cos) 110 | dist = arc * (r_earth + H) # meters, radius of earth 111 | return dist 112 | 113 | 114 | def bearing(lat1, lon1, lat2, lon2): 115 | lat1 = np.radians(lat1) 116 | lon1 = np.radians(lon1) 117 | lat2 = np.radians(lat2) 118 | lon2 = np.radians(lon2) 119 | x = np.sin(lon2 - lon1) * np.cos(lat2) 120 | y = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(lon2 - lon1) 121 | initial_bearing = np.arctan2(x, y) 122 | initial_bearing = np.degrees(initial_bearing) 123 | bearing = (initial_bearing + 360) % 360 124 | return bearing 125 | 126 | 127 | # ----------------------------------------------------- 128 | # Speed conversions, altitude H all in meters 129 | # ----------------------------------------------------- 130 | def tas2mach(Vtas, H): 131 | """True Airspeed to Mach number""" 132 | a = vsound(H) 133 | Mach = Vtas / a 134 | return Mach 135 | 136 | 137 | def mach2tas(Mach, H): 138 | """Mach number to True Airspeed""" 139 | a = vsound(H) 140 | Vtas = Mach * a 141 | return Vtas 142 | 143 | 144 | def eas2tas(Veas, H): 145 | """Equivalent Airspeed to True Airspeed""" 146 | rho = density(H) 147 | Vtas = Veas * np.sqrt(rho0 / rho) 148 | return Vtas 149 | 150 | 151 | def tas2eas(Vtas, H): 152 | """True Airspeed to Equivalent Airspeed""" 153 | rho = density(H) 154 | Veas = Vtas * np.sqrt(rho / rho0) 155 | return Veas 156 | 157 | 158 | def cas2tas(Vcas, H): 159 | """Calibrated Airspeed to True Airspeed""" 160 | p, rho, T = atmos(H) 161 | qdyn = p0 * ((1 + rho0 * Vcas * Vcas / (7 * p0)) ** 3.5 - 1.0) 162 | Vtas = np.sqrt(7 * p / rho * ((1 + qdyn / p) ** (2 / 7.0) - 1.0)) 163 | return Vtas 164 | 165 | 166 | def tas2cas(Vtas, H): 167 | """True Airspeed to Calibrated Airspeed""" 168 | p, rho, T = atmos(H) 169 | qdyn = p * ((1 + rho * Vtas * Vtas / (7 * p)) ** 3.5 - 1.0) 170 | Vcas = np.sqrt(7 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2 / 7.0) - 1.0)) 171 | return Vcas 172 | 173 | 174 | def mach2cas(Mach, H): 175 | """Mach number to Calibrated Airspeed""" 176 | Vtas = mach2tas(Mach, H) 177 | Vcas = tas2cas(Vtas, H) 178 | return Vcas 179 | 180 | 181 | def cas2mach(Vcas, H): 182 | """Calibrated Airspeed to Mach number""" 183 | Vtas = cas2tas(Vcas, H) 184 | Mach = tas2mach(Vtas, H) 185 | return Mach 186 | -------------------------------------------------------------------------------- /src/pyModeS/extra/rtlreader.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | import traceback 5 | import numpy as np 6 | import pyModeS as pms 7 | 8 | from typing import Any 9 | 10 | 11 | import_msg = """ 12 | --------------------------------------------------------------------- 13 | Warning: pyrtlsdr not installed (required for using RTL-SDR devices)! 14 | ---------------------------------------------------------------------""" 15 | 16 | try: 17 | import rtlsdr # type: ignore 18 | except ImportError: 19 | print(import_msg) 20 | 21 | sampling_rate = 2e6 22 | smaples_per_microsec = 2 23 | 24 | modes_frequency = 1090e6 25 | buffer_size = 1024 * 200 26 | read_size = 1024 * 100 27 | 28 | pbits = 8 29 | fbits = 112 30 | preamble = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0] 31 | th_amp_diff = 0.8 # signal amplitude threshold difference between 0 and 1 bit 32 | 33 | 34 | class RtlReader(object): 35 | def __init__(self, **kwargs) -> None: 36 | super(RtlReader, self).__init__() 37 | self.signal_buffer: list[float] = [] # amplitude of the sample only 38 | self.sdr = rtlsdr.RtlSdr() 39 | self.sdr.sample_rate = sampling_rate 40 | self.sdr.center_freq = modes_frequency 41 | self.sdr.gain = "auto" 42 | 43 | self.debug = kwargs.get("debug", False) 44 | self.raw_pipe_in = None 45 | self.stop_flag = False 46 | self.noise_floor = 1e6 47 | 48 | self.exception_queue = None 49 | 50 | def _calc_noise(self) -> float: 51 | """Calculate noise floor""" 52 | window = smaples_per_microsec * 100 53 | total_len = len(self.signal_buffer) 54 | means = ( 55 | np.array(self.signal_buffer[: total_len // window * window]) 56 | .reshape(-1, window) 57 | .mean(axis=1) 58 | ) 59 | return min(means) 60 | 61 | def _process_buffer(self) -> list[list[Any]]: 62 | """process raw IQ data in the buffer""" 63 | 64 | # update noise floor 65 | self.noise_floor = min(self._calc_noise(), self.noise_floor) 66 | 67 | # set minimum signal amplitude 68 | min_sig_amp = 3.162 * self.noise_floor # 10 dB SNR 69 | 70 | # Mode S messages 71 | messages = [] 72 | 73 | buffer_length = len(self.signal_buffer) 74 | 75 | i = 0 76 | while i < buffer_length: 77 | if self.signal_buffer[i] < min_sig_amp: 78 | i += 1 79 | continue 80 | 81 | frame_start = i + pbits * 2 82 | if self._check_preamble(self.signal_buffer[i:frame_start]): 83 | frame_length = (fbits + 1) * 2 84 | frame_end = frame_start + frame_length 85 | frame_pulses = self.signal_buffer[frame_start:frame_end] 86 | 87 | threshold = max(frame_pulses) * 0.2 88 | 89 | msgbin: list[int] = [] 90 | for j in range(0, frame_length, 2): 91 | j_2 = j + 2 92 | p2 = frame_pulses[j:j_2] 93 | if len(p2) < 2: 94 | break 95 | 96 | if p2[0] < threshold and p2[1] < threshold: 97 | break 98 | elif p2[0] >= p2[1]: 99 | c = 1 100 | elif p2[0] < p2[1]: 101 | c = 0 102 | else: 103 | msgbin = [] 104 | break 105 | 106 | msgbin.append(c) 107 | 108 | # advance i with a jump 109 | i = frame_start + j 110 | 111 | if len(msgbin) > 0: 112 | msghex = pms.bin2hex("".join([str(i) for i in msgbin])) 113 | if self._check_msg(msghex): 114 | messages.append([msghex, time.time()]) 115 | if self.debug: 116 | self._debug_msg(msghex) 117 | 118 | # elif i > buffer_length - 500: 119 | # # save some for next process 120 | # break 121 | else: 122 | i += 1 123 | 124 | # reset the buffer 125 | self.signal_buffer = self.signal_buffer[i:] 126 | 127 | return messages 128 | 129 | def _check_preamble(self, pulses) -> bool: 130 | if len(pulses) != 16: 131 | return False 132 | 133 | for i in range(16): 134 | if abs(pulses[i] - preamble[i]) > th_amp_diff: 135 | return False 136 | 137 | return True 138 | 139 | def _check_msg(self, msg) -> bool: 140 | df = pms.df(msg) 141 | msglen = len(msg) 142 | if df == 17 and msglen == 28: 143 | if pms.crc(msg) == 0: 144 | return True 145 | elif df in [20, 21] and msglen == 28: 146 | return True 147 | elif df in [4, 5, 11] and msglen == 14: 148 | return True 149 | return False 150 | 151 | def _debug_msg(self, msg) -> None: 152 | df = pms.df(msg) 153 | msglen = len(msg) 154 | if df == 17 and msglen == 28: 155 | print(msg, pms.icao(msg), pms.crc(msg)) 156 | elif df in [20, 21] and msglen == 28: 157 | print(msg, pms.icao(msg)) 158 | elif df in [4, 5, 11] and msglen == 14: 159 | print(msg, pms.icao(msg)) 160 | else: 161 | # print("[*]", msg) 162 | pass 163 | 164 | def _read_callback(self, data, rtlsdr_obj) -> None: 165 | amp = np.absolute(data) 166 | self.signal_buffer.extend(amp.tolist()) 167 | 168 | if len(self.signal_buffer) >= buffer_size: 169 | messages = self._process_buffer() 170 | self.handle_messages(messages) 171 | 172 | def handle_messages(self, messages) -> None: 173 | """re-implement this method to handle the messages""" 174 | for msg, t in messages: 175 | # print("%15.9f %s" % (t, msg)) 176 | pass 177 | 178 | def stop(self, *args, **kwargs) -> None: 179 | self.sdr.close() 180 | 181 | def run( 182 | self, raw_pipe_in=None, stop_flag=None, exception_queue=None 183 | ) -> None: 184 | self.raw_pipe_in = raw_pipe_in 185 | self.exception_queue = exception_queue 186 | self.stop_flag = stop_flag 187 | 188 | try: 189 | # raise RuntimeError("test exception") 190 | 191 | while True: 192 | data = self.sdr.read_samples(read_size) 193 | self._read_callback(data, None) 194 | 195 | except Exception as e: 196 | tb = traceback.format_exc() 197 | if self.exception_queue is not None: 198 | self.exception_queue.put(tb) 199 | raise e 200 | 201 | 202 | if __name__ == "__main__": 203 | import signal 204 | 205 | rtl = RtlReader() 206 | signal.signal(signal.SIGINT, rtl.stop) 207 | 208 | rtl.debug = True 209 | rtl.run() 210 | -------------------------------------------------------------------------------- /src/pyModeS/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junzis/pyModeS/6401a3963c1f7a11884b64e23fa2f7b0c72f2b2e/src/pyModeS/py.typed -------------------------------------------------------------------------------- /src/pyModeS/streamer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junzis/pyModeS/6401a3963c1f7a11884b64e23fa2f7b0c72f2b2e/src/pyModeS/streamer/__init__.py -------------------------------------------------------------------------------- /src/pyModeS/streamer/decode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import traceback 4 | import datetime 5 | import csv 6 | import pyModeS as pms 7 | 8 | 9 | class Decode: 10 | def __init__(self, latlon=None, dumpto=None): 11 | 12 | self.acs = dict() 13 | 14 | if latlon is not None: 15 | self.lat0 = float(latlon[0]) 16 | self.lon0 = float(latlon[1]) 17 | else: 18 | self.lat0 = None 19 | self.lon0 = None 20 | 21 | self.t = 0 22 | self.cache_timeout = 60 # seconds 23 | 24 | if dumpto is not None and os.path.isdir(dumpto): 25 | self.dumpto = dumpto 26 | else: 27 | self.dumpto = None 28 | 29 | def process_raw(self, adsb_ts, adsb_msg, commb_ts, commb_msg, tnow=None): 30 | """process a chunk of adsb and commb messages received in the same 31 | time period. 32 | """ 33 | if tnow is None: 34 | tnow = time.time() 35 | 36 | self.t = tnow 37 | 38 | local_updated_acs_buffer = [] 39 | output_buffer = [] 40 | 41 | # process adsb message 42 | for t, msg in zip(adsb_ts, adsb_msg): 43 | icao = pms.icao(msg) 44 | tc = pms.adsb.typecode(msg) 45 | 46 | if icao not in self.acs: 47 | self.acs[icao] = { 48 | "live": None, 49 | "call": None, 50 | "lat": None, 51 | "lon": None, 52 | "alt": None, 53 | "gs": None, 54 | "trk": None, 55 | "roc": None, 56 | "tas": None, 57 | "roll": None, 58 | "rtrk": None, 59 | "ias": None, 60 | "mach": None, 61 | "hdg": None, 62 | "ver": None, 63 | "HPL": None, 64 | "RCu": None, 65 | "RCv": None, 66 | "HVE": None, 67 | "VVE": None, 68 | "Rc": None, 69 | "VPL": None, 70 | "EPU": None, 71 | "VEPU": None, 72 | "HFOMr": None, 73 | "VFOMr": None, 74 | "PE_RCu": None, 75 | "PE_VPL": None, 76 | "hum44" : None, 77 | "p44" : None, 78 | "temp44" : None, 79 | "turb44" : None, 80 | "wind44" : None, 81 | } 82 | 83 | self.acs[icao]["tc"] = tc 84 | self.acs[icao]["icao"] = icao 85 | self.acs[icao]["t"] = t 86 | self.acs[icao]["live"] = int(t) 87 | 88 | if 1 <= tc <= 4: 89 | cs = pms.adsb.callsign(msg) 90 | self.acs[icao]["call"] = cs 91 | output_buffer.append([t, icao, "cs", cs]) 92 | 93 | if (5 <= tc <= 8) or (tc == 19): 94 | vdata = pms.adsb.velocity(msg) 95 | if vdata is None: 96 | continue 97 | 98 | spd, trk, roc, tag = vdata 99 | if tag != "GS": 100 | continue 101 | if (spd is None) or (trk is None): 102 | continue 103 | 104 | self.acs[icao]["gs"] = spd 105 | self.acs[icao]["trk"] = trk 106 | self.acs[icao]["roc"] = roc 107 | self.acs[icao]["tv"] = t 108 | 109 | output_buffer.append([t, icao, "gs", spd]) 110 | output_buffer.append([t, icao, "trk", trk]) 111 | output_buffer.append([t, icao, "roc", roc]) 112 | 113 | if 5 <= tc <= 18: 114 | oe = pms.adsb.oe_flag(msg) 115 | self.acs[icao][oe] = msg 116 | self.acs[icao]["t" + str(oe)] = t 117 | 118 | if ("tpos" in self.acs[icao]) and (t - self.acs[icao]["tpos"] < 180): 119 | # use single message decoding 120 | rlat = self.acs[icao]["lat"] 121 | rlon = self.acs[icao]["lon"] 122 | latlon = pms.adsb.position_with_ref(msg, rlat, rlon) 123 | elif ( 124 | ("t0" in self.acs[icao]) 125 | and ("t1" in self.acs[icao]) 126 | and (abs(self.acs[icao]["t0"] - self.acs[icao]["t1"]) < 10) 127 | ): 128 | # use multi message decoding 129 | try: 130 | latlon = pms.adsb.position( 131 | self.acs[icao][0], 132 | self.acs[icao][1], 133 | self.acs[icao]["t0"], 134 | self.acs[icao]["t1"], 135 | self.lat0, 136 | self.lon0, 137 | ) 138 | except: 139 | # mix of surface and airborne position message 140 | continue 141 | else: 142 | latlon = None 143 | 144 | if latlon is not None: 145 | self.acs[icao]["tpos"] = t 146 | self.acs[icao]["lat"] = latlon[0] 147 | self.acs[icao]["lon"] = latlon[1] 148 | 149 | alt = pms.adsb.altitude(msg) 150 | self.acs[icao]["alt"] = alt 151 | 152 | output_buffer.append([t, icao, "lat", latlon[0]]) 153 | output_buffer.append([t, icao, "lon", latlon[1]]) 154 | output_buffer.append([t, icao, "alt", alt]) 155 | 156 | local_updated_acs_buffer.append(icao) 157 | 158 | # Uncertainty & accuracy 159 | ac = self.acs[icao] 160 | 161 | if 9 <= tc <= 18: 162 | ac["nic_bc"] = pms.adsb.nic_b(msg) 163 | 164 | if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22): 165 | ac["NUCp"], ac["HPL"], ac["RCu"], ac["RCv"] = pms.adsb.nuc_p(msg) 166 | 167 | if (ac["ver"] == 1) and ("nic_s" in ac.keys()): 168 | ac["NIC"], ac["Rc"], ac["VPL"] = pms.adsb.nic_v1(msg, ac["nic_s"]) 169 | elif ( 170 | (ac["ver"] == 2) 171 | and ("nic_a" in ac.keys()) 172 | and ("nic_bc" in ac.keys()) 173 | ): 174 | ac["NIC"], ac["Rc"] = pms.adsb.nic_v2(msg, ac["nic_a"], ac["nic_bc"]) 175 | 176 | if tc == 19: 177 | ac["NUCv"], ac["HVE"], ac["VVE"] = pms.adsb.nuc_v(msg) 178 | if ac["ver"] in [1, 2]: 179 | ac["NACv"], ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg) 180 | 181 | if tc == 29: 182 | ac["PE_RCu"], ac["PE_VPL"], ac["base"] = pms.adsb.sil(msg, ac["ver"]) 183 | ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg) 184 | 185 | if tc == 31: 186 | ac["ver"] = pms.adsb.version(msg) 187 | ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg) 188 | ac["PE_RCu"], ac["PE_VPL"], ac["sil_base"] = pms.adsb.sil( 189 | msg, ac["ver"] 190 | ) 191 | 192 | if ac["ver"] == 1: 193 | ac["nic_s"] = pms.adsb.nic_s(msg) 194 | elif ac["ver"] == 2: 195 | ac["nic_a"], ac["nic_bc"] = pms.adsb.nic_a_c(msg) 196 | 197 | # process commb message 198 | for t, msg in zip(commb_ts, commb_msg): 199 | icao = pms.icao(msg) 200 | 201 | if icao not in self.acs: 202 | continue 203 | 204 | self.acs[icao]["icao"] = icao 205 | self.acs[icao]["t"] = t 206 | self.acs[icao]["live"] = int(t) 207 | 208 | bds = pms.bds.infer(msg) 209 | 210 | if bds == "BDS50": 211 | roll50 = pms.commb.roll50(msg) 212 | trk50 = pms.commb.trk50(msg) 213 | rtrk50 = pms.commb.rtrk50(msg) 214 | gs50 = pms.commb.gs50(msg) 215 | tas50 = pms.commb.tas50(msg) 216 | 217 | self.acs[icao]["t50"] = t 218 | if tas50: 219 | self.acs[icao]["tas"] = tas50 220 | output_buffer.append([t, icao, "tas50", tas50]) 221 | if roll50: 222 | self.acs[icao]["roll"] = roll50 223 | output_buffer.append([t, icao, "roll50", roll50]) 224 | if rtrk50: 225 | self.acs[icao]["rtrk"] = rtrk50 226 | output_buffer.append([t, icao, "rtrk50", rtrk50]) 227 | 228 | if trk50: 229 | self.acs[icao]["trk50"] = trk50 230 | output_buffer.append([t, icao, "trk50", trk50]) 231 | if gs50: 232 | self.acs[icao]["gs50"] = gs50 233 | output_buffer.append([t, icao, "gs50", gs50]) 234 | 235 | elif bds == "BDS60": 236 | ias60 = pms.commb.ias60(msg) 237 | hdg60 = pms.commb.hdg60(msg) 238 | mach60 = pms.commb.mach60(msg) 239 | roc60baro = pms.commb.vr60baro(msg) 240 | roc60ins = pms.commb.vr60ins(msg) 241 | 242 | if ias60 or hdg60 or mach60: 243 | self.acs[icao]["t60"] = t 244 | if ias60: 245 | self.acs[icao]["ias"] = ias60 246 | output_buffer.append([t, icao, "ias60", ias60]) 247 | if hdg60: 248 | self.acs[icao]["hdg"] = hdg60 249 | output_buffer.append([t, icao, "hdg60", hdg60]) 250 | if mach60: 251 | self.acs[icao]["mach"] = mach60 252 | output_buffer.append([t, icao, "mach60", mach60]) 253 | 254 | if roc60baro: 255 | self.acs[icao]["roc60baro"] = roc60baro 256 | output_buffer.append([t, icao, "roc60baro", roc60baro]) 257 | if roc60ins: 258 | self.acs[icao]["roc60ins"] = roc60ins 259 | output_buffer.append([t, icao, "roc60ins", roc60ins]) 260 | 261 | elif bds == "BDS44": 262 | if(pms.commb.is44(msg)): 263 | self.acs[icao]["hum44"] = pms.commb.hum44(msg) 264 | self.acs[icao]["p44"] = pms.commb.p44(msg) 265 | self.acs[icao]["temp44"] = pms.commb.temp44(msg) 266 | self.acs[icao]["turb44"] = pms.commb.turb44(msg) 267 | self.acs[icao]["wind44"] = pms.commb.wind44(msg) 268 | 269 | # clear up old data 270 | for icao in list(self.acs.keys()): 271 | if self.t - self.acs[icao]["live"] > self.cache_timeout: 272 | del self.acs[icao] 273 | continue 274 | 275 | if self.dumpto is not None: 276 | dh = str(datetime.datetime.now().strftime("%Y%m%d_%H")) 277 | fn = self.dumpto + "/pymodes_dump_%s.csv" % dh 278 | output_buffer.sort(key=lambda x: x[0]) 279 | with open(fn, "a") as f: 280 | writer = csv.writer(f) 281 | writer.writerows(output_buffer) 282 | 283 | return 284 | 285 | def get_aircraft(self): 286 | """all aircraft that are stored in memory""" 287 | acs = self.acs 288 | return acs 289 | 290 | def run(self, raw_pipe_out, ac_pipe_in, exception_queue): 291 | local_buffer = [] 292 | while True: 293 | try: 294 | while raw_pipe_out.poll(): 295 | data = raw_pipe_out.recv() 296 | local_buffer.append(data) 297 | 298 | for data in local_buffer: 299 | self.process_raw( 300 | data["adsb_ts"], 301 | data["adsb_msg"], 302 | data["commb_ts"], 303 | data["commb_msg"], 304 | ) 305 | local_buffer = [] 306 | 307 | acs = self.get_aircraft() 308 | ac_pipe_in.send(acs) 309 | time.sleep(0.001) 310 | 311 | except Exception as e: 312 | tb = traceback.format_exc() 313 | exception_queue.put((e, tb)) 314 | -------------------------------------------------------------------------------- /src/pyModeS/streamer/modeslive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import time 6 | import argparse 7 | import curses 8 | import signal 9 | import multiprocessing 10 | from pyModeS.streamer.decode import Decode 11 | from pyModeS.streamer.screen import Screen 12 | from pyModeS.streamer.source import NetSource, RtlSdrSource # , RtlSdrSource24 13 | 14 | 15 | def main(): 16 | 17 | support_rawtypes = ["raw", "beast", "skysense"] 18 | 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument( 21 | "--source", 22 | help='Choose data source, "rtlsdr", "rtlsdr24" or "net"', 23 | required=True, 24 | default="net", 25 | ) 26 | parser.add_argument( 27 | "--connect", 28 | help="Define server, port and data type. Supported data types are: {}".format( 29 | support_rawtypes 30 | ), 31 | nargs=3, 32 | metavar=("SERVER", "PORT", "DATATYPE"), 33 | default=None, 34 | required=False, 35 | ) 36 | parser.add_argument( 37 | "--latlon", 38 | help="Receiver latitude and longitude, needed for the surface position, default none", 39 | nargs=2, 40 | metavar=("LAT", "LON"), 41 | default=None, 42 | required=False, 43 | ) 44 | parser.add_argument( 45 | "--show-uncertainty", 46 | dest="uncertainty", 47 | help="Display uncertainty values, default off", 48 | action="store_true", 49 | required=False, 50 | default=False, 51 | ) 52 | parser.add_argument( 53 | "--dumpto", 54 | help="Folder to dump decoded output, default none", 55 | required=False, 56 | default=None, 57 | ) 58 | args = parser.parse_args() 59 | 60 | SOURCE = args.source 61 | LATLON = args.latlon 62 | UNCERTAINTY = args.uncertainty 63 | DUMPTO = args.dumpto 64 | 65 | if SOURCE in ["rtlsdr", "rtlsdr24"]: 66 | pass 67 | elif SOURCE == "net": 68 | if args.connect is None: 69 | print("Error: --connect argument must not be empty.") 70 | else: 71 | SERVER, PORT, DATATYPE = args.connect 72 | if DATATYPE not in support_rawtypes: 73 | print( 74 | "Data type not supported, available ones are %s" 75 | % support_rawtypes 76 | ) 77 | 78 | else: 79 | print('Source must be "rtlsdr" or "net".') 80 | sys.exit(1) 81 | 82 | if DUMPTO is not None: 83 | # append to current folder except root is given 84 | if DUMPTO[0] != "/": 85 | DUMPTO = os.getcwd() + "/" + DUMPTO 86 | 87 | if not os.path.isdir(DUMPTO): 88 | print("Error: dump folder (%s) does not exist" % DUMPTO) 89 | sys.exit(1) 90 | 91 | # redirect all stdout to null, avoiding messing up with the screen 92 | sys.stdout = open(os.devnull, "w") 93 | 94 | # Explicitly set the start method to fork to avoid errors when running 95 | # on OSX which otherwise defaults to spawn. Starting in Python 3.14, fork 96 | # must be explicitly set if needed. 97 | # See: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods 98 | multiprocessing.set_start_method("fork") 99 | 100 | raw_pipe_in, raw_pipe_out = multiprocessing.Pipe() 101 | ac_pipe_in, ac_pipe_out = multiprocessing.Pipe() 102 | exception_queue = multiprocessing.Queue() 103 | stop_flag = multiprocessing.Value("b", False) 104 | 105 | if SOURCE == "net": 106 | source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE) 107 | elif SOURCE == "rtlsdr": 108 | source = RtlSdrSource() 109 | # elif SOURCE == "rtlsdr24": 110 | # source = RtlSdrSource24() 111 | 112 | recv_process = multiprocessing.Process( 113 | target=source.run, args=(raw_pipe_in, stop_flag, exception_queue) 114 | ) 115 | 116 | decode = Decode(latlon=LATLON, dumpto=DUMPTO) 117 | decode_process = multiprocessing.Process( 118 | target=decode.run, args=(raw_pipe_out, ac_pipe_in, exception_queue) 119 | ) 120 | 121 | screen = Screen(uncertainty=UNCERTAINTY) 122 | screen_process = multiprocessing.Process( 123 | target=screen.run, args=(ac_pipe_out, exception_queue) 124 | ) 125 | 126 | def shutdown(): 127 | stop_flag.value = True 128 | curses.endwin() 129 | sys.stdout = sys.__stdout__ 130 | recv_process.terminate() 131 | decode_process.terminate() 132 | screen_process.terminate() 133 | recv_process.join() 134 | decode_process.join() 135 | screen_process.join() 136 | 137 | def closeall(signal, frame): 138 | print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal)) 139 | shutdown() 140 | sys.exit(0) 141 | 142 | signal.signal(signal.SIGINT, closeall) 143 | 144 | recv_process.start() 145 | decode_process.start() 146 | screen_process.start() 147 | 148 | while True: 149 | if ( 150 | (not recv_process.is_alive()) 151 | or (not decode_process.is_alive()) 152 | or (not screen_process.is_alive()) 153 | ): 154 | shutdown() 155 | while not exception_queue.empty(): 156 | trackback = exception_queue.get() 157 | print(trackback) 158 | 159 | sys.exit(1) 160 | 161 | time.sleep(0.01) 162 | -------------------------------------------------------------------------------- /src/pyModeS/streamer/screen.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import numpy as np 3 | import time 4 | import threading 5 | import traceback 6 | 7 | COLUMNS = [ 8 | ("call", 10), 9 | ("lat", 10), 10 | ("lon", 10), 11 | ("alt", 7), 12 | ("gs", 5), 13 | ("tas", 5), 14 | ("ias", 5), 15 | ("mach", 7), 16 | ("roc", 7), 17 | ("trk", 10), 18 | ("hdg", 10), 19 | ("live", 6), 20 | ] 21 | 22 | UNCERTAINTY_COLUMNS = [ 23 | ("|", 5), 24 | ("ver", 4), 25 | ("HPL", 5), 26 | ("RCu", 5), 27 | ("RCv", 5), 28 | ("HVE", 5), 29 | ("VVE", 5), 30 | ("Rc", 4), 31 | ("VPL", 5), 32 | ("EPU", 5), 33 | ("VEPU", 6), 34 | ("HFOMr", 7), 35 | ("VFOMr", 7), 36 | ("PE_RCu", 8), 37 | ("PE_VPL", 8), 38 | ] 39 | 40 | 41 | class Screen(object): 42 | def __init__(self, uncertainty=False): 43 | super(Screen, self).__init__() 44 | self.screen = curses.initscr() 45 | curses.noecho() 46 | curses.mousemask(1) 47 | self.screen.keypad(True) 48 | self.y = 3 49 | self.x = 1 50 | self.offset = 0 51 | self.acs = {} 52 | self.lock_icao = None 53 | 54 | self.columns = COLUMNS 55 | if uncertainty: 56 | self.columns.extend(UNCERTAINTY_COLUMNS) 57 | 58 | def reset_cursor_pos(self): 59 | self.screen.move(self.y, self.x) 60 | 61 | def update_ac(self, acs): 62 | self.acs = acs 63 | 64 | def draw_frame(self): 65 | self.screen.border(0) 66 | self.screen.addstr( 67 | 0, 68 | 2, 69 | "Online aircraft [%d] ('Ctrl+C' to exit, 'Enter' to lock one)" 70 | % len(self.acs), 71 | ) 72 | 73 | 74 | def round_float(self, value, max_width): 75 | # Constrain a floating point number to a maximum string size. 76 | # Subtract 2 to account for decimal and column spacing. 77 | 78 | sign_size = 1 if value < 0 else 0 79 | int_size = len(str(int(value))) 80 | max_precision = max_width - int_size - sign_size - 2 81 | rounded_value = round(value, max_precision) 82 | 83 | return f"{rounded_value:.{max_precision}f}" 84 | 85 | def update(self): 86 | if len(self.acs) == 0: 87 | return 88 | 89 | resized = curses.is_term_resized(self.scr_h, self.scr_w) 90 | if resized is True: 91 | self.scr_h, self.scr_w = self.screen.getmaxyx() 92 | self.screen.clear() 93 | curses.resizeterm(self.scr_h, self.scr_w) 94 | 95 | self.screen.refresh() 96 | self.draw_frame() 97 | 98 | row = 1 99 | 100 | header = " icao" 101 | for c, cw in self.columns: 102 | header += (cw - len(c)) * " " + c 103 | 104 | # fill end with spaces 105 | header += (self.scr_w - 2 - len(header)) * " " 106 | 107 | if len(header) > self.scr_w - 2: 108 | header = header[: self.scr_w - 3] + ">" 109 | 110 | self.screen.addstr(row, 1, header) 111 | 112 | row += 1 113 | self.screen.addstr(row, 1, "-" * (self.scr_w - 2)) 114 | 115 | icaos = np.array(list(self.acs.keys())) 116 | icaos = np.sort(icaos) 117 | 118 | for row in range(3, self.scr_h - 3): 119 | icao = None 120 | idx = row + self.offset - 3 121 | 122 | if idx > len(icaos) - 1: 123 | line = " " * (self.scr_w - 2) 124 | 125 | else: 126 | line = "" 127 | 128 | icao = icaos[idx] 129 | ac = self.acs[icao] 130 | 131 | line += icao 132 | 133 | for c, cw in self.columns: 134 | if c == "|": 135 | val = "|" 136 | elif c == "live": 137 | val = str(ac[c] - int(time.time())) + "s" 138 | elif ac[c] is None: 139 | val = "" 140 | else: 141 | val = ac[c] 142 | 143 | if isinstance(val, float): 144 | val = self.round_float(val, cw) 145 | 146 | val_str = str(val) 147 | line += (cw - len(val_str)) * " " + val_str 148 | 149 | # fill end with spaces 150 | line += (self.scr_w - 2 - len(line)) * " " 151 | 152 | if len(line) > self.scr_w - 2: 153 | line = line[: self.scr_w - 3] + ">" 154 | 155 | if (icao is not None) and (self.lock_icao == icao): 156 | self.screen.addstr(row, 1, line, curses.A_STANDOUT) 157 | elif row == self.y: 158 | self.screen.addstr(row, 1, line, curses.A_BOLD) 159 | else: 160 | self.screen.addstr(row, 1, line) 161 | 162 | self.screen.addstr(self.scr_h - 3, 1, "-" * (self.scr_w - 2)) 163 | 164 | total_page = len(icaos) // (self.scr_h - 4) + 1 165 | current_page = self.offset // (self.scr_h - 4) + 1 166 | self.screen.addstr(self.scr_h - 2, 1, "(%d / %d)" % (current_page, total_page)) 167 | 168 | self.reset_cursor_pos() 169 | 170 | def kye_handling(self): 171 | self.draw_frame() 172 | self.scr_h, self.scr_w = self.screen.getmaxyx() 173 | 174 | while True: 175 | c = self.screen.getch() 176 | 177 | if c == curses.KEY_HOME: 178 | self.x = 1 179 | self.y = 1 180 | elif c == curses.KEY_NPAGE: 181 | offset_intent = self.offset + (self.scr_h - 4) 182 | if offset_intent < len(self.acs) - 5: 183 | self.offset = offset_intent 184 | elif c == curses.KEY_PPAGE: 185 | offset_intent = self.offset - (self.scr_h - 4) 186 | if offset_intent > 0: 187 | self.offset = offset_intent 188 | else: 189 | self.offset = 0 190 | elif c == curses.KEY_DOWN: 191 | y_intent = self.y + 1 192 | if y_intent < self.scr_h - 3: 193 | self.y = y_intent 194 | elif c == curses.KEY_UP: 195 | y_intent = self.y - 1 196 | if y_intent > 2: 197 | self.y = y_intent 198 | elif c == curses.KEY_ENTER or c == 10 or c == 13: 199 | self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode() 200 | elif c == 27: # escape key 201 | self.lock_icao = None 202 | elif c == curses.KEY_F5: 203 | self.screen.refresh() 204 | self.draw_frame() 205 | 206 | def run(self, ac_pipe_out, exception_queue): 207 | local_buffer = [] 208 | key_thread = threading.Thread(target=self.kye_handling) 209 | key_thread.daemon = True 210 | key_thread.start() 211 | 212 | while True: 213 | try: 214 | # raise RuntimeError("test exception") 215 | 216 | while ac_pipe_out.poll(): 217 | acs = ac_pipe_out.recv() 218 | local_buffer.append(acs) 219 | 220 | for acs in local_buffer: 221 | self.update_ac(acs) 222 | 223 | local_buffer = [] 224 | 225 | self.update() 226 | except curses.error: 227 | pass 228 | except Exception as e: 229 | tb = traceback.format_exc() 230 | exception_queue.put(tb) 231 | time.sleep(0.1) 232 | raise e 233 | 234 | time.sleep(0.001) 235 | -------------------------------------------------------------------------------- /src/pyModeS/streamer/source.py: -------------------------------------------------------------------------------- 1 | import pyModeS as pms 2 | from pyModeS.extra.tcpclient import TcpClient 3 | from pyModeS.extra.rtlreader import RtlReader 4 | 5 | 6 | class NetSource(TcpClient): 7 | def __init__(self, host, port, rawtype): 8 | super(NetSource, self).__init__(host, port, rawtype) 9 | self.reset_local_buffer() 10 | 11 | def reset_local_buffer(self): 12 | self.local_buffer_adsb_msg = [] 13 | self.local_buffer_adsb_ts = [] 14 | self.local_buffer_commb_msg = [] 15 | self.local_buffer_commb_ts = [] 16 | 17 | def handle_messages(self, messages): 18 | 19 | if self.stop_flag.value is True: 20 | self.stop() 21 | return 22 | 23 | for msg, t in messages: 24 | if len(msg) < 28: # only process long messages 25 | continue 26 | 27 | df = pms.df(msg) 28 | 29 | if df == 17 or df == 18: 30 | self.local_buffer_adsb_msg.append(msg) 31 | self.local_buffer_adsb_ts.append(t) 32 | elif df == 20 or df == 21: 33 | self.local_buffer_commb_msg.append(msg) 34 | self.local_buffer_commb_ts.append(t) 35 | else: 36 | continue 37 | 38 | if len(self.local_buffer_adsb_msg) > 1: 39 | self.raw_pipe_in.send( 40 | { 41 | "adsb_ts": self.local_buffer_adsb_ts, 42 | "adsb_msg": self.local_buffer_adsb_msg, 43 | "commb_ts": self.local_buffer_commb_ts, 44 | "commb_msg": self.local_buffer_commb_msg, 45 | } 46 | ) 47 | self.reset_local_buffer() 48 | 49 | 50 | class RtlSdrSource(RtlReader): 51 | def __init__(self): 52 | super(RtlSdrSource, self).__init__() 53 | self.reset_local_buffer() 54 | 55 | def reset_local_buffer(self): 56 | self.local_buffer_adsb_msg = [] 57 | self.local_buffer_adsb_ts = [] 58 | self.local_buffer_commb_msg = [] 59 | self.local_buffer_commb_ts = [] 60 | 61 | def handle_messages(self, messages): 62 | 63 | if self.stop_flag.value is True: 64 | self.stop() 65 | return 66 | 67 | for msg, t in messages: 68 | if len(msg) < 28: # only process long messages 69 | continue 70 | 71 | df = pms.df(msg) 72 | 73 | if df == 17 or df == 18: 74 | self.local_buffer_adsb_msg.append(msg) 75 | self.local_buffer_adsb_ts.append(t) 76 | elif df == 20 or df == 21: 77 | self.local_buffer_commb_msg.append(msg) 78 | self.local_buffer_commb_ts.append(t) 79 | else: 80 | continue 81 | 82 | if len(self.local_buffer_adsb_msg) > 1: 83 | self.raw_pipe_in.send( 84 | { 85 | "adsb_ts": self.local_buffer_adsb_ts, 86 | "adsb_msg": self.local_buffer_adsb_msg, 87 | "commb_ts": self.local_buffer_commb_ts, 88 | "commb_msg": self.local_buffer_commb_msg, 89 | } 90 | ) 91 | self.reset_local_buffer() 92 | -------------------------------------------------------------------------------- /tests/benchmark.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import pandas as pd 4 | from tqdm import tqdm 5 | from pyModeS.decoder import adsb 6 | 7 | fin = sys.argv[1] 8 | 9 | df = pd.read_csv(fin, names=["ts", "df", "icao", "msg"]) 10 | df_adsb = df[df["df"] == 17].copy() 11 | 12 | total = df_adsb.shape[0] 13 | 14 | 15 | def native(): 16 | 17 | from pyModeS.decoder import common 18 | 19 | # airborne position 20 | m_air_0 = None 21 | m_air_1 = None 22 | 23 | # surface position 24 | m_surf_0 = None 25 | m_surf_1 = None 26 | 27 | for i, r in tqdm(df_adsb.iterrows(), total=total): 28 | ts = r.ts 29 | m = r.msg 30 | 31 | downlink_format = common.df(m) 32 | crc = common.crc(m) 33 | icao = adsb.icao(m) 34 | tc = adsb.typecode(m) 35 | 36 | if 1 <= tc <= 4: 37 | category = adsb.category(m) 38 | callsign = adsb.callsign(m) 39 | if tc == 19: 40 | velocity = adsb.velocity(m) 41 | 42 | if 5 <= tc <= 8: 43 | if adsb.oe_flag(m): 44 | m_surf_1 = m 45 | t1 = ts 46 | else: 47 | m_surf_0 = m 48 | t0 = ts 49 | 50 | if m_surf_0 and m_surf_1: 51 | position = adsb.surface_position( 52 | m_surf_0, m_surf_1, t0, t1, 50.01, 4.35 53 | ) 54 | altitude = adsb.altitude(m) 55 | 56 | if 9 <= tc <= 18: 57 | if adsb.oe_flag(m): 58 | m_air_1 = m 59 | t1 = ts 60 | else: 61 | m_air_0 = m 62 | t0 = ts 63 | 64 | if m_air_0 and m_air_1: 65 | position = adsb.position(m_air_0, m_air_1, t0, t1) 66 | altitude = adsb.altitude(m) 67 | 68 | 69 | def cython(): 70 | 71 | from pyModeS.decoder import c_common as common 72 | 73 | # airborne position 74 | m_air_0 = None 75 | m_air_1 = None 76 | 77 | # surface position 78 | m_surf_0 = None 79 | m_surf_1 = None 80 | 81 | for i, r in tqdm(df_adsb.iterrows(), total=total): 82 | ts = r.ts 83 | m = r.msg 84 | 85 | downlink_format = common.df(m) 86 | crc = common.crc(m) 87 | icao = adsb.icao(m) 88 | tc = adsb.typecode(m) 89 | 90 | if 1 <= tc <= 4: 91 | category = adsb.category(m) 92 | callsign = adsb.callsign(m) 93 | if tc == 19: 94 | velocity = adsb.velocity(m) 95 | 96 | if 5 <= tc <= 8: 97 | if adsb.oe_flag(m): 98 | m_surf_1 = m 99 | t1 = ts 100 | else: 101 | m_surf_0 = m 102 | t0 = ts 103 | 104 | if m_surf_0 and m_surf_1: 105 | position = adsb.surface_position( 106 | m_surf_0, m_surf_1, t0, t1, 50.01, 4.35 107 | ) 108 | altitude = adsb.altitude(m) 109 | 110 | if 9 <= tc <= 18: 111 | if adsb.oe_flag(m): 112 | m_air_1 = m 113 | t1 = ts 114 | else: 115 | m_air_0 = m 116 | t0 = ts 117 | 118 | if m_air_0 and m_air_1: 119 | position = adsb.position(m_air_0, m_air_1, t0, t1) 120 | altitude = adsb.altitude(m) 121 | 122 | 123 | if __name__ == "__main__": 124 | t1 = time.time() 125 | native() 126 | dt1 = time.time() - t1 127 | 128 | t2 = time.time() 129 | cython() 130 | dt2 = time.time() - t2 131 | -------------------------------------------------------------------------------- /tests/sample_run_adsb.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import time 3 | 4 | from pyModeS.decoder import adsb 5 | 6 | print("===== Decode ADS-B sample data=====") 7 | 8 | f = open("tests/data/sample_data_adsb.csv", "rt") 9 | 10 | msg0 = None 11 | msg1 = None 12 | 13 | tstart = time.time() 14 | for i, r in enumerate(csv.reader(f)): 15 | 16 | ts = int(r[0]) 17 | m = r[1].encode() 18 | 19 | icao = adsb.icao(m) 20 | tc = adsb.typecode(m) 21 | 22 | if 1 <= tc <= 4: 23 | print(ts, m, icao, tc, adsb.category(m), adsb.callsign(m)) 24 | if tc == 19: 25 | print(ts, m, icao, tc, adsb.velocity(m)) 26 | if 5 <= tc <= 18: 27 | if adsb.oe_flag(m): 28 | msg1 = m 29 | t1 = ts 30 | else: 31 | msg0 = m 32 | t0 = ts 33 | 34 | if msg0 and msg1: 35 | pos = adsb.position(msg0, msg1, t0, t1) 36 | alt = adsb.altitude(m) 37 | print(ts, m, icao, tc, pos, alt) 38 | 39 | 40 | dt = time.time() - tstart 41 | 42 | print("Execution time: {} seconds".format(dt)) 43 | -------------------------------------------------------------------------------- /tests/sample_run_commb.py: -------------------------------------------------------------------------------- 1 | from pyModeS import commb, common, bds 2 | 3 | # === Decode sample data file === 4 | 5 | 6 | def bds_info(BDS, m): 7 | if BDS == "BDS10": 8 | info = [commb.ovc10(m)] 9 | 10 | elif BDS == "BDS17": 11 | info = [i[-2:] for i in commb.cap17(m)] 12 | 13 | elif BDS == "BDS20": 14 | info = [commb.cs20(m)] 15 | 16 | elif BDS == "BDS40": 17 | info = (commb.selalt40mcp(m), commb.selalt40fms(m), commb.p40baro(m)) 18 | 19 | elif BDS == "BDS44": 20 | info = (commb.wind44(m), commb.temp44(m), commb.p44(m), commb.hum44(m)) 21 | 22 | elif BDS == "BDS44REV": 23 | info = ( 24 | commb.wind44(m, rev=True), 25 | commb.temp44(m, rev=True), 26 | commb.p44(m, rev=True), 27 | commb.hum44(m, rev=True), 28 | ) 29 | 30 | elif BDS == "BDS50": 31 | info = ( 32 | commb.roll50(m), 33 | commb.trk50(m), 34 | commb.gs50(m), 35 | commb.rtrk50(m), 36 | commb.tas50(m), 37 | ) 38 | 39 | elif BDS == "BDS60": 40 | info = ( 41 | commb.hdg60(m), 42 | commb.ias60(m), 43 | commb.mach60(m), 44 | commb.vr60baro(m), 45 | commb.vr60ins(m), 46 | ) 47 | 48 | else: 49 | info = [] 50 | 51 | return info 52 | 53 | 54 | def commb_decode_all(df, n=None): 55 | import csv 56 | 57 | print("===== Decode Comm-B sample data (DF=%s)=====" % df) 58 | 59 | f = open("tests/data/sample_data_commb_df%s.csv" % df, "rt") 60 | 61 | for i, r in enumerate(csv.reader(f)): 62 | if n and i > n: 63 | break 64 | 65 | ts = r[0] 66 | m = r[2] 67 | 68 | df = common.df(m) 69 | icao = common.icao(m) 70 | BDS = bds.infer(m) 71 | code = common.altcode(m) if df == 20 else common.idcode(m) 72 | 73 | if not BDS: 74 | print(ts, m, icao, df, "%5s" % code, "UNKNOWN") 75 | continue 76 | 77 | if len(BDS.split(",")) > 1: 78 | print(ts, m, icao, df, "%5s" % code, end=" ") 79 | for i, _bds in enumerate(BDS.split(",")): 80 | if i == 0: 81 | print(_bds, *bds_info(_bds, m)) 82 | else: 83 | print(" " * 55, _bds, *bds_info(_bds, m)) 84 | 85 | else: 86 | print(ts, m, icao, df, "%5s" % code, BDS, *bds_info(BDS, m)) 87 | 88 | 89 | if __name__ == "__main__": 90 | commb_decode_all(df=20, n=500) 91 | commb_decode_all(df=21, n=500) 92 | -------------------------------------------------------------------------------- /tests/test_adsb.py: -------------------------------------------------------------------------------- 1 | from pyModeS import adsb 2 | from pytest import approx 3 | 4 | # === TEST ADS-B package === 5 | 6 | 7 | def test_adsb_icao(): 8 | assert adsb.icao("8D406B902015A678D4D220AA4BDA") == "406B90" 9 | 10 | 11 | def test_adsb_category(): 12 | assert adsb.category("8D406B902015A678D4D220AA4BDA") == 0 13 | 14 | 15 | def test_adsb_callsign(): 16 | assert adsb.callsign("8D406B902015A678D4D220AA4BDA") == "EZY85MH_" 17 | 18 | 19 | def test_adsb_position(): 20 | pos = adsb.position( 21 | "8D40058B58C901375147EFD09357", 22 | "8D40058B58C904A87F402D3B8C59", 23 | 1446332400, 24 | 1446332405, 25 | ) 26 | assert pos == (approx(49.81755, 0.001), approx(6.08442, 0.001)) 27 | 28 | 29 | def test_adsb_position_swap_odd_even(): 30 | pos = adsb.position( 31 | "8D40058B58C904A87F402D3B8C59", 32 | "8D40058B58C901375147EFD09357", 33 | 1446332405, 34 | 1446332400, 35 | ) 36 | assert pos == (approx(49.81755, 0.001), approx(6.08442, 0.001)) 37 | 38 | 39 | def test_adsb_position_with_ref(): 40 | pos = adsb.position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0) 41 | assert pos == (approx(49.82410, 0.001), approx(6.06785, 0.001)) 42 | pos = adsb.position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5) 43 | assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001)) 44 | 45 | 46 | def test_adsb_airborne_position_with_ref(): 47 | pos = adsb.airborne_position_with_ref( 48 | "8D40058B58C901375147EFD09357", 49.0, 6.0 49 | ) 50 | assert pos == (approx(49.82410, 0.001), approx(6.06785, 0.001)) 51 | pos = adsb.airborne_position_with_ref( 52 | "8D40058B58C904A87F402D3B8C59", 49.0, 6.0 53 | ) 54 | assert pos == (approx(49.81755, 0.001), approx(6.08442, 0.001)) 55 | 56 | 57 | def test_adsb_airborne_position_with_ref_numerical_challenge(): 58 | lat_ref = 30.508474576271183 # Close to (360.0/59.0)*5 59 | lon_ref = 7.2*5.0+3e-15 60 | pos = adsb.airborne_position_with_ref( 61 | "8D06A15358BF17FF7D4A84B47B95", lat_ref, lon_ref 62 | ) 63 | assert pos == (approx(30.50540, 0.001), approx(33.44787, 0.001)) 64 | 65 | 66 | def test_adsb_surface_position_with_ref(): 67 | pos = adsb.surface_position_with_ref( 68 | "8FC8200A3AB8F5F893096B000000", -43.5, 172.5 69 | ) 70 | assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001)) 71 | 72 | 73 | def test_adsb_surface_position(): 74 | pos = adsb.surface_position( 75 | "8CC8200A3AC8F009BCDEF2000000", 76 | "8FC8200A3AB8F5F893096B000000", 77 | 0, 78 | 2, 79 | -43.496, 80 | 172.558, 81 | ) 82 | assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001)) 83 | 84 | 85 | def test_adsb_alt(): 86 | assert adsb.altitude("8D40058B58C901375147EFD09357") == 39000 87 | 88 | 89 | def test_adsb_velocity(): 90 | vgs = adsb.velocity("8D485020994409940838175B284F") 91 | vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F") 92 | vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000") 93 | assert vgs == (159, approx(182.88, 0.1), -832, "GS") 94 | assert vas == (375, approx(243.98, 0.1), -2304, "TAS") 95 | assert vgs_surface == (19, approx(42.2, 0.1), 0, "GS") 96 | assert adsb.altitude_diff("8D485020994409940838175B284F") == 550 97 | 98 | 99 | def test_adsb_emergency(): 100 | assert not adsb.is_emergency("8DA2C1B6E112B600000000760759") 101 | assert adsb.emergency_state("8DA2C1B6E112B600000000760759") == 0 102 | assert adsb.emergency_squawk("8DA2C1B6E112B600000000760759") == "6513" 103 | 104 | 105 | def test_adsb_target_state_status(): 106 | sel_alt = adsb.selected_altitude("8DA05629EA21485CBF3F8CADAEEB") 107 | assert sel_alt == (16992, "MCP/FCU") 108 | assert adsb.baro_pressure_setting("8DA05629EA21485CBF3F8CADAEEB") == 1012.8 109 | assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB") == approx( 110 | 66.8, 0.1 111 | ) 112 | assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") is True 113 | assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True 114 | assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") is False 115 | assert adsb.approach_mode("8DA05629EA21485CBF3F8CADAEEB") is False 116 | assert adsb.tcas_operational("8DA05629EA21485CBF3F8CADAEEB") is True 117 | assert adsb.lnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True 118 | 119 | 120 | # def test_nic(): 121 | # assert adsb.nic('8D3C70A390AB11F55B8C57F65FE6') == 0 122 | # assert adsb.nic('8DE1C9738A4A430B427D219C8225') == 1 123 | # assert adsb.nic('8D44058880B50006B1773DC2A7E9') == 2 124 | # assert adsb.nic('8D44058881B50006B1773DC2A7E9') == 3 125 | # assert adsb.nic('8D4AB42A78000640000000FA0D0A') == 4 126 | # assert adsb.nic('8D4405887099F5D9772F37F86CB6') == 5 127 | # assert adsb.nic('8D4841A86841528E72D9B472DAC2') == 6 128 | # assert adsb.nic('8D44057560B9760C0B840A51C89F') == 7 129 | # assert adsb.nic('8D40621D58C382D690C8AC2863A7') == 8 130 | # assert adsb.nic('8F48511C598D04F12CCF82451642') == 9 131 | # assert adsb.nic('8DA4D53A50DBF8C6330F3B35458F') == 10 132 | # assert adsb.nic('8D3C4ACF4859F1736F8E8ADF4D67') == 11 133 | -------------------------------------------------------------------------------- /tests/test_allcall.py: -------------------------------------------------------------------------------- 1 | from pyModeS import allcall 2 | 3 | 4 | def test_icao(): 5 | assert allcall.icao("5D484FDEA248F5") == "484FDE" 6 | 7 | 8 | def test_interrogator(): 9 | assert allcall.interrogator("5D484FDEA248F5") == "SI6" 10 | 11 | 12 | def test_capability(): 13 | assert allcall.capability("5D484FDEA248F5")[0] == 5 14 | -------------------------------------------------------------------------------- /tests/test_bds_inference.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | from pyModeS import bds 5 | 6 | # this one fails on GitHub action for some unknown reason 7 | # it looks successful on other Windows instances though 8 | # TODO fix later 9 | @pytest.mark.skipif(sys.platform == "win32", reason="GitHub Action") 10 | def test_bds_infer(): 11 | assert bds.infer("8D406B902015A678D4D220AA4BDA") == "BDS08" 12 | assert bds.infer("8FC8200A3AB8F5F893096B000000") == "BDS06" 13 | assert bds.infer("8D40058B58C901375147EFD09357") == "BDS05" 14 | assert bds.infer("8D485020994409940838175B284F") == "BDS09" 15 | 16 | assert bds.infer("A800178D10010080F50000D5893C") == "BDS10" 17 | assert bds.infer("A0000638FA81C10000000081A92F") == "BDS17" 18 | assert bds.infer("A0001838201584F23468207CDFA5") == "BDS20" 19 | assert bds.infer("A0001839CA3800315800007448D9") == "BDS40" 20 | assert bds.infer("A000139381951536E024D4CCF6B5") == "BDS50" 21 | assert bds.infer("A00004128F39F91A7E27C46ADC21") == "BDS60" 22 | 23 | 24 | def test_bds_is50or60(): 25 | assert bds.is50or60("A0001838201584F23468207CDFA5", 0, 0, 0) == None 26 | assert bds.is50or60("A8001EBCFFFB23286004A73F6A5B", 320, 250, 14000) == "BDS50" 27 | assert bds.is50or60("A8001EBCFE1B29287FDCA807BCFC", 320, 250, 14000) == "BDS50" 28 | 29 | 30 | def test_surface_position(): 31 | msg0 = "8FE48C033A9FA184B934E744C6FD" 32 | msg1 = "8FE48C033A9FA68F7C3D39B1C2F0" 33 | 34 | t0 = 1565608663102 35 | t1 = 1565608666214 36 | 37 | lat_ref = -23.4265448 38 | lon_ref = -46.4816258 39 | 40 | lat, lon = bds.bds06.surface_position(msg0, msg1, t0, t1, lat_ref, lon_ref) 41 | 42 | assert abs(lon_ref - lon) < 0.05 43 | -------------------------------------------------------------------------------- /tests/test_c_common.py: -------------------------------------------------------------------------------- 1 | try: 2 | from pyModeS import c_common 3 | 4 | def test_conversions(): 5 | assert c_common.hex2bin("6E") == "01101110" 6 | assert c_common.bin2hex("01101110") == "6E" 7 | assert c_common.bin2hex("1101110") == "6E" 8 | 9 | def test_crc_decode(): 10 | 11 | assert c_common.crc("8D406B902015A678D4D220AA4BDA") == 0 12 | assert c_common.crc("8d8960ed58bf053cf11bc5932b7d") == 0 13 | assert c_common.crc("8d45cab390c39509496ca9a32912") == 0 14 | assert c_common.crc("8d74802958c904e6ef4ba0184d5c") == 0 15 | assert c_common.crc("8d4400cd9b0000b4f87000e71a10") == 0 16 | assert c_common.crc("8d4065de58a1054a7ef0218e226a") == 0 17 | 18 | assert c_common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924 19 | assert c_common.crc("a800089d8094e33a6004e4b8a522") == 4805588 20 | assert c_common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991 21 | assert c_common.crc("a0000410bc900010a40000f5f477") == 11727682 22 | assert c_common.crc("8d4ca251204994b1c36e60a5343d") == 16 23 | assert c_common.crc("b0001718c65632b0a82040715b65") == 353333 24 | 25 | def test_crc_encode(): 26 | parity = c_common.crc("8D406B902015A678D4D220AA4BDA", encode=True) 27 | assert parity == 11160538 28 | 29 | def test_icao(): 30 | assert c_common.icao("8D406B902015A678D4D220AA4BDA") == "406B90" 31 | assert c_common.icao("A0001839CA3800315800007448D9") == "400940" 32 | assert c_common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2" 33 | assert c_common.icao("A000029CFFBAA11E2004727281F1") == "4243D0" 34 | 35 | def test_modes_altcode(): 36 | assert c_common.altcode("A02014B400000000000000F9D514") == 32300 37 | 38 | def test_modes_idcode(): 39 | assert c_common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346" 40 | 41 | def test_graycode_to_altitude(): 42 | assert c_common.gray2alt("00000000010") == -1000 43 | assert c_common.gray2alt("00000001010") == -500 44 | assert c_common.gray2alt("00000011011") == -100 45 | assert c_common.gray2alt("00000011010") == 0 46 | assert c_common.gray2alt("00000011110") == 100 47 | assert c_common.gray2alt("00000010011") == 600 48 | assert c_common.gray2alt("00000110010") == 1000 49 | assert c_common.gray2alt("00001001001") == 5800 50 | assert c_common.gray2alt("00011100100") == 10300 51 | assert c_common.gray2alt("01100011010") == 32000 52 | assert c_common.gray2alt("01110000100") == 46300 53 | assert c_common.gray2alt("01010101100") == 50200 54 | assert c_common.gray2alt("11011110100") == 73200 55 | assert c_common.gray2alt("10000000011") == 126600 56 | assert c_common.gray2alt("10000000001") == 126700 57 | 58 | 59 | except: 60 | pass 61 | -------------------------------------------------------------------------------- /tests/test_commb.py: -------------------------------------------------------------------------------- 1 | from pyModeS import bds, commb 2 | from pytest import approx 3 | 4 | # from pyModeS import ehs, els # deprecated 5 | 6 | 7 | def test_bds20_callsign(): 8 | assert bds.bds20.cs20("A000083E202CC371C31DE0AA1CCF") == "KLM1017_" 9 | assert bds.bds20.cs20("A0001993202422F2E37CE038738E") == "IBK2873_" 10 | 11 | assert commb.cs20("A000083E202CC371C31DE0AA1CCF") == "KLM1017_" 12 | assert commb.cs20("A0001993202422F2E37CE038738E") == "IBK2873_" 13 | 14 | 15 | def test_bds40_functions(): 16 | assert bds.bds40.selalt40mcp("A000029C85E42F313000007047D3") == 3008 17 | assert bds.bds40.selalt40fms("A000029C85E42F313000007047D3") == 3008 18 | assert bds.bds40.p40baro("A000029C85E42F313000007047D3") == 1020.0 19 | 20 | assert commb.selalt40mcp("A000029C85E42F313000007047D3") == 3008 21 | assert commb.selalt40fms("A000029C85E42F313000007047D3") == 3008 22 | assert commb.p40baro("A000029C85E42F313000007047D3") == 1020.0 23 | 24 | 25 | def test_bds50_functions(): 26 | msg1 = "A000139381951536E024D4CCF6B5" 27 | msg2 = "A0001691FFD263377FFCE02B2BF9" 28 | 29 | for module in [bds.bds50, commb]: 30 | assert module.roll50(msg1) == approx(2.1, 0.01) 31 | assert module.roll50(msg2) == approx(-0.35, 0.01) # signed value 32 | assert module.trk50(msg1) == approx(114.258, 0.1) 33 | assert module.gs50(msg1) == 438 34 | assert module.rtrk50(msg1) == 0.125 35 | assert module.tas50(msg1) == 424 36 | 37 | 38 | def test_bds60_functions(): 39 | msg = "A00004128F39F91A7E27C46ADC21" 40 | 41 | for module in [bds.bds60, commb]: 42 | assert bds.bds60.hdg60(msg) == approx(42.71484) 43 | assert bds.bds60.ias60(msg) == 252 44 | assert bds.bds60.mach60(msg) == 0.42 45 | assert bds.bds60.vr60baro(msg) == -1920 46 | assert bds.bds60.vr60ins(msg) == -1920 47 | -------------------------------------------------------------------------------- /tests/test_py_common.py: -------------------------------------------------------------------------------- 1 | from pyModeS import py_common 2 | 3 | 4 | def test_conversions(): 5 | assert py_common.hex2bin("6E") == "01101110" 6 | assert py_common.bin2hex("01101110") == "6E" 7 | assert py_common.bin2hex("1101110") == "6E" 8 | 9 | 10 | def test_crc_decode(): 11 | assert py_common.crc_legacy("8D406B902015A678D4D220AA4BDA") == 0 12 | 13 | assert py_common.crc("8D406B902015A678D4D220AA4BDA") == 0 14 | assert py_common.crc("8d8960ed58bf053cf11bc5932b7d") == 0 15 | assert py_common.crc("8d45cab390c39509496ca9a32912") == 0 16 | assert py_common.crc("8d49d3d4e1089d00000000744c3b") == 0 17 | assert py_common.crc("8d74802958c904e6ef4ba0184d5c") == 0 18 | assert py_common.crc("8d4400cd9b0000b4f87000e71a10") == 0 19 | assert py_common.crc("8d4065de58a1054a7ef0218e226a") == 0 20 | 21 | assert py_common.crc("c80b2dca34aa21dd821a04cb64d4") == 10719924 22 | assert py_common.crc("a800089d8094e33a6004e4b8a522") == 4805588 23 | assert py_common.crc("a8000614a50b6d32bed000bbe0ed") == 5659991 24 | assert py_common.crc("a0000410bc900010a40000f5f477") == 11727682 25 | assert py_common.crc("8d4ca251204994b1c36e60a5343d") == 16 26 | assert py_common.crc("b0001718c65632b0a82040715b65") == 353333 27 | 28 | 29 | def test_crc_encode(): 30 | parity = py_common.crc("8D406B902015A678D4D220AA4BDA", encode=True) 31 | assert parity == 11160538 32 | 33 | 34 | def test_icao(): 35 | assert py_common.icao("8D406B902015A678D4D220AA4BDA") == "406B90" 36 | assert py_common.icao("A0001839CA3800315800007448D9") == "400940" 37 | assert py_common.icao("A000139381951536E024D4CCF6B5") == "3C4DD2" 38 | assert py_common.icao("A000029CFFBAA11E2004727281F1") == "4243D0" 39 | 40 | 41 | def test_modes_altcode(): 42 | assert py_common.altcode("A02014B400000000000000F9D514") == 32300 43 | 44 | 45 | def test_modes_idcode(): 46 | assert py_common.idcode("A800292DFFBBA9383FFCEB903D01") == "1346" 47 | 48 | 49 | def test_graycode_to_altitude(): 50 | assert py_common.gray2alt("00000000010") == -1000 51 | assert py_common.gray2alt("00000001010") == -500 52 | assert py_common.gray2alt("00000011011") == -100 53 | assert py_common.gray2alt("00000011010") == 0 54 | assert py_common.gray2alt("00000011110") == 100 55 | assert py_common.gray2alt("00000010011") == 600 56 | assert py_common.gray2alt("00000110010") == 1000 57 | assert py_common.gray2alt("00001001001") == 5800 58 | assert py_common.gray2alt("00011100100") == 10300 59 | assert py_common.gray2alt("01100011010") == 32000 60 | assert py_common.gray2alt("01110000100") == 46300 61 | assert py_common.gray2alt("01010101100") == 50200 62 | assert py_common.gray2alt("11011110100") == 73200 63 | assert py_common.gray2alt("10000000011") == 126600 64 | assert py_common.gray2alt("10000000001") == 126700 65 | -------------------------------------------------------------------------------- /tests/test_surv.py: -------------------------------------------------------------------------------- 1 | from pyModeS import surv 2 | 3 | 4 | def test_fs(): 5 | assert surv.fs("2A00516D492B80")[0] == 2 6 | 7 | 8 | def test_dr(): 9 | assert surv.dr("2A00516D492B80")[0] == 0 10 | 11 | 12 | def test_um(): 13 | assert surv.um("200CBE4ED80137")[0] == 9 14 | assert surv.um("200CBE4ED80137")[1] == 1 15 | 16 | 17 | def test_identity(): 18 | assert surv.identity("2A00516D492B80") == "0356" 19 | 20 | 21 | def test_altitude(): 22 | assert surv.altitude("20001718029FCD") == 36000 23 | -------------------------------------------------------------------------------- /tests/test_tell.py: -------------------------------------------------------------------------------- 1 | from pyModeS.decoder import tell 2 | 3 | messages = [ 4 | "8D406B902015A678D4D220AA4BDA", 5 | "8FC8200A3AB8F5F893096B000000", 6 | "8D40058B58C901375147EFD09357", 7 | "8D485020994409940838175B284F", 8 | "A000083E202CC371C31DE0AA1CCF", 9 | "A8001E2520053332C1A820363386", 10 | "A000029C85E42F313000007047D3", 11 | "A5DC282C2A0108372CA6DA9693B0", 12 | "A00015B8C26A00328400004242DA", 13 | "A000139381951536E024D4CCF6B5", 14 | "A00004128F39F91A7E27C46ADC21", 15 | ] 16 | 17 | print("-" * 70) 18 | for m in messages: 19 | tell(m) 20 | print("-" * 70) 21 | --------------------------------------------------------------------------------