├── .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 |
--------------------------------------------------------------------------------