6 Commits

Author SHA1 Message Date
Xavier Olive
c62b3b48fc remove cache for windows 2022-12-29 18:53:11 +01:00
Xavier Olive
ae01f95ff5 fix typing 2022-12-29 18:46:43 +01:00
Xavier Olive
c3839d861c minimal attempt for 2.4M demodulation 2022-12-28 00:13:03 +01:00
Xavier Olive
a8f3b9c811 attempt to fix poetry windows cache issue 2022-12-28 00:11:52 +01:00
Xavier Olive
8f098671a0 Update .gitignore 2022-12-28 00:05:07 +01:00
Xavier Olive
4cf99e8927 switch to poetry (#135) 2022-12-27 23:58:28 +01:00
26 changed files with 2222 additions and 584 deletions

3
.flake8 Normal file
View File

@@ -0,0 +1,3 @@
[flake8]
max-line-length = 80
extend-ignore = E203, E302

View File

@@ -5,13 +5,16 @@ on:
pull_request_target:
workflow_dispatch:
env:
POETRY_VERSION: "1.3.1"
jobs:
deploy:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
env:
PYTHON_VERSION: ${{ matrix.python-version }}
@@ -24,30 +27,45 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
# Poetry cache depends on OS, Python version and Poetry version.
- name: Cache Poetry cache
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry
key: poetry-cache-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ env.POETRY_VERSION }}
# virtualenv cache should depends on OS, Python version and `poetry.lock` (and optionally workflow files).
- name: Cache Packages
uses: actions/cache@v3
if: matrix.os != 'windows-latest'
with:
path: ~/.local
key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }}
- name: Add poetry to windows path
if: "startsWith(runner.os, 'windows')"
run: |
echo "C:\Users\runneradmin\.local\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install and configure Poetry
uses: snok/install-poetry@v1.3.3
with:
version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install dependencies
run: |
pip install -U pip numpy cython mypy
pip install -U pytest codecov pytest-cov
pip install .
poetry install
- name: Type checking
if: ${{ env.PYTHON_VERSION != '3.7' }}
run: |
mypy pyModeS
poetry run mypy pyModeS
- name: Run tests (without Cython)
- name: Run tests
run: |
pytest tests --cov --cov-report term-missing
- name: Install with Cython
run: |
pip install -U cython
pip uninstall -y pymodes
pip install .
- name: Run tests (with Cython)
run: |
pytest tests
poetry run pytest tests --cov --cov-report term-missing
- name: Upload coverage to Codecov
if: ${{ github.event_name != 'pull_request_target' && env.PYTHON_VERSION == '3.10' }}

6
.gitignore vendored
View File

@@ -5,6 +5,11 @@ __pycache__/
*.py[cod]
.pytest_cache/
# Cython
pyModeS/decoder/flarm/decode.c
pyModeS/extra/demod2400/core.c
pyModeS/c_common.c
# C extensions
*.so
@@ -64,3 +69,4 @@ target/
.venv
env/
venv/

View File

@@ -94,13 +94,13 @@ If you want to make use of the (faster) c module, install ``pyModeS`` as follows
# conda (compiled) version
conda install -c conda-forge pymodes
# stable version (to be compiled on your side)
pip install pyModeS[fast]
# stable version
pip install pyModeS
# development version
git clone https://github.com/junzis/pyModeS
cd pyModeS
pip install .[fast]
poetry install -E rtlsdr
View live traffic (modeslive)

77
build.py Normal file
View File

@@ -0,0 +1,77 @@
import os
import shutil
import sys
from distutils.core import Distribution, Extension
from distutils.command import build_ext
from Cython.Build import cythonize
def build() -> None:
compile_args = []
if sys.platform == "linux":
compile_args += [
"-march=native",
"-O3",
"-msse",
"-msse2",
"-mfma",
"-mfpmath=sse",
"-Wno-pointer-sign",
"-Wno-unused-variable",
]
extensions = [
Extension(
"pyModeS.c_common",
["pyModeS/c_common.pyx"],
extra_compile_args=compile_args,
),
Extension(
"pyModeS.decoder.flarm.decode",
[
"pyModeS/decoder/flarm/decode.pyx",
"pyModeS/decoder/flarm/core.c",
],
extra_compile_args=compile_args,
include_dirs=["pyModeS/decoder/flarm"],
),
Extension(
"pyModeS.extra.demod2400.core",
[
"pyModeS/extra/demod2400/core.pyx",
"pyModeS/extra/demod2400/demod2400.c",
],
extra_compile_args=compile_args,
include_dirs=["pyModeS/extra/demod2400"],
libraries=["m"],
),
]
ext_modules = cythonize(
extensions,
compiler_directives={"binding": True, "language_level": 3},
)
distribution = Distribution(
{"name": "extended", "ext_modules": ext_modules}
)
distribution.package_dir = "extended" # type: ignore
cmd = build_ext.build_ext(distribution)
cmd.verbose = True # type: ignore
cmd.ensure_finalized() # type: ignore
cmd.run()
# Copy built extensions back to the project
for output in cmd.get_outputs():
relative_extension = os.path.relpath(output, cmd.build_lib)
shutil.copyfile(output, relative_extension)
mode = os.stat(relative_extension).st_mode
mode |= (mode & 0o444) >> 2
os.chmod(relative_extension, mode)
if __name__ == "__main__":
build()

1364
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@ except Exception:
from .decoder import tell
from .decoder import adsb
from .decoder import acas
from .decoder import commb
from .decoder import allcall
from .decoder import surv

View File

@@ -1,158 +1,5 @@
"""
Decoding Air-Air Surveillance (ACAS) DF=0/16
[To be implemented]
"""
from __future__ import annotations
from .. import common
import warnings
warnings.simplefilter("always", UserWarning)
def isACAS(msg: str) -> bool:
"""Check if the message is an ACAS coordination message.
:param msg: 28 hexdigits string
:return: if VDS is 3,1
"""
mv = common.hex2bin(common.data(msg))
vds = mv[0:8]
if vds == "00110000":
return True
else:
return False
def rac(msg: str) -> None | str:
"""Resolution Advisory Complement.
:param msg: 28 hexdigits string
:return: RACs
"""
if not isACAS(msg):
warnings.warn("Not an ACAS coordination message.")
return None
RAC = []
mv = common.hex2bin(common.data(msg))
if mv[22] == "1":
RAC.append("do not pass below")
if mv[23] == "1":
RAC.append("do not pass above")
if mv[24] == "1":
RAC.append("do not pass left")
if mv[25] == "1":
RAC.append("do not pass right")
return "; ".join(RAC)
def rat(msg: str) -> None | int:
"""RA terminated indicator
Mode S transponder is still required to report RA 18 seconds after
it is terminated by ACAS. Hence, the RAT filed is used.
:param msg: 28 hexdigits string
:return: if RA has been terminated
"""
if not isACAS(msg):
warnings.warn("Not an ACAS coordination message.")
return None
mv = common.hex2bin(common.data(msg))
mte = int(mv[26])
return mte
def mte(msg: str) -> None | int:
"""Multiple threat encounter.
:param msg: 28 hexdigits string
:return: if there are multiple threats
"""
if not isACAS(msg):
warnings.warn("Not an ACAS coordination message.")
return None
mv = common.hex2bin(common.data(msg))
mte = int(mv[27])
return mte
def ara(msg: str) -> None | str:
"""Decode active resolution advisory.
:param msg: 28 bytes hexadecimal message string
:return: RA charactristics
"""
if not isACAS(msg):
warnings.warn("Not an ACAS coordination message.")
return None
mv = common.hex2bin(common.data(msg))
mte = int(mv[27])
ara_b1 = int(mv[8])
ara_b2 = int(mv[9])
ara_b3 = int(mv[10])
ara_b4 = int(mv[11])
ara_b5 = int(mv[12])
ara_b6 = int(mv[14])
ara_b7 = int(mv[15])
# ACAS III are bits 15-22
RA = []
if ara_b1 == 1:
if ara_b2:
RA.append("corrective")
else:
RA.append("preventive")
if ara_b3:
RA.append("downward sense")
else:
RA.append("upward sense")
if ara_b4:
RA.append("increased rate")
if ara_b5:
RA.append("sense reversal")
if ara_b6:
RA.append("altitude crossing")
if ara_b7:
RA.append("positive")
else:
RA.append("vertical speed limit")
if ara_b1 == 0 and mte == 1:
if ara_b2:
RA.append("requires a correction in the upward sense")
if ara_b3:
RA.append("requires a positive climb")
if ara_b4:
RA.append("requires a correction in downward sense")
if ara_b5:
RA.append("requires a positive descent")
if ara_b6:
RA.append("requires a crossing")
if ara_b7:
RA.append("requires a sense reversal")
return "; ".join(RA)

View File

@@ -25,7 +25,7 @@ from .bds.bds06 import (
)
from .bds.bds08 import callsign, category
from .bds.bds09 import airborne_velocity, altitude_diff
from .bds.bds61_st1 import emergency_squawk, emergency_state, is_emergency
from .bds.bds61 import emergency_squawk, emergency_state, is_emergency
from .bds.bds62 import (
altitude_hold_mode,
approach_mode,
@@ -161,7 +161,9 @@ def position(
raise RuntimeError("Incorrect or inconsistent message types")
def position_with_ref(msg: str, lat_ref: float, lon_ref: float) -> tuple[float, float]:
def position_with_ref(
msg: str, lat_ref: float, lon_ref: float
) -> tuple[float, float]:
"""Decode position with only one message.
A reference position is required, which can be previously

View File

@@ -34,12 +34,14 @@ from . import ( # noqa: F401
bds45,
bds50,
bds60,
bds61_st1,
bds61,
bds62,
)
def is50or60(msg: str, spd_ref: float, trk_ref: float, alt_ref: float) -> Optional[str]:
def is50or60(
msg: str, spd_ref: float, trk_ref: float, alt_ref: float
) -> Optional[str]:
"""Use reference ground speed and trk to determine BDS50 and DBS60.
Args:

View File

@@ -2,7 +2,6 @@
# BDS 6,1
# ADS-B TC=28
# Aircraft Airborne status
# (Subtype 1)
# ------------------------------------------
from ... import common

View File

@@ -1,102 +0,0 @@
# ------------------------------------------
# BDS 6,1
# ADS-B TC=28
# Aircraft Airborne status
# (Subtype 1)
# ------------------------------------------
from __future__ import annotations
from ... import common
from .. import acas
def threat_type(msg: str) -> int:
"""Determine the threat type indicator.
Value Meaning
----- ---------------------------------------
0 No identity data in TID
1 TID has Mode S address (ICAO)
2 TID has altitude, range, and bearing
3 Not assigned
:param msg: 28 hexdigits string
:return: indicator of threat type
"""
mb = common.hex2bin(common.data(msg))
tti = common.bin2int(mb[28:30])
return tti
def threat_identity(msg: str) -> str:
mb = common.hex2bin(common.data(msg))
tti = threat_type(msg)
# The ICAO of the threat is announced
if tti == 1:
return common.icao(mb[30:55])
else:
raise RuntimeError("%s: Missing threat identity (ICAO)")
def threat_location(msg: str) -> None | tuple[int, int, int]:
"""Get the altitude, range, and bearing of the threat.
Altitude is the Mode C altitude
:param msg: 28 hexdigits string
:return: tuple of the Mode C altitude, range, and bearing
"""
mb = common.hex2bin(common.data(msg))
tti = threat_type(msg)
# Altitude, range, and bearing of threat
if tti == 2:
altitude = common.altitude(mb[31:44])
distance = common.bin2int(mb[44:51])
bearing = common.bin2int(mb[51:57])
return altitude, distance, bearing
return None
def has_multiple_threats(msg: str) -> bool:
"""Indicate if the ACAS is processing multiple threats simultaneously.
:param msg: 28 hexdigits string
:return: if there are multiple threats
"""
return acas.mte(msg) == 1
def active_resolution_advisories(msg: str) -> None | str:
"""Decode active resolution advisory.
Uses ARA decoding function from ACAS module.
:param msg: 28 bytes hexadecimal message string
:return: RA charactristics
"""
return acas.ara(msg)
def is_ra_terminated(msg: str) -> bool:
"""Indicate if the threat is still being generated.
Mode S transponder is still required to report RA 18 seconds after
it is terminated by ACAS. Hence, the RAT filed is used.
:param msg: 28 hexdigits string
:return: if the threat is terminated
"""
return acas.rat(msg) == 1
def ra_complement(msg: str) -> None | str:
"""Resolution Advisory Complement.
:param msg: 28 hexdigits string
:return: RACs
"""
return acas.rac(msg)

View File

@@ -1,6 +1,7 @@
from core cimport make_key as c_make_key, btea as c_btea
from cpython cimport array
from .core cimport make_key as c_make_key, btea as c_btea
import array
import math
from ctypes import c_byte

View File

@@ -0,0 +1,3 @@
from .core import demod2400
__all__ = ["demod2400"]

View File

@@ -0,0 +1,4 @@
import numpy as np
import numpy.typing as npt
def demod2400(data: npt.NDArray[np.uint16], timestamp: float): ...

View File

@@ -0,0 +1,49 @@
from libc.stdint cimport uint16_t, uint8_t
import numpy as np
from ...c_common cimport crc, df
cdef extern from "demod2400.h":
int demodulate2400(uint16_t *data, uint8_t *msg, int len_data, int* len_msg)
def demod2400(uint16_t[:] data, float timestamp):
cdef uint8_t[:] msg_bin
cdef int i = 0, j, length, crc_msg = 1
cdef long size = data.shape[0]
msg_bin = np.zeros(14, dtype=np.uint8)
while i < size:
j = demodulate2400(&data[i], &msg_bin[0], size-i, &length)
if j == 0:
yield dict(
# 1 sample data = 2 IQ samples (hence 2*)
timestamp=timestamp + 2.*i/2400000.,
payload=None,
crc=None,
index=i,
)
return
i += j
msg_clip = np.asarray(msg_bin)[:length]
msg = "".join(f"{elt:02X}" for elt in msg_clip)
crc_msg = crc(msg)
# if df(msg) != 17 or crc_msg == 0:
if crc_msg == 0:
yield dict(
# 1 sample data = 2 IQ samples (hence 2*)
timestamp=timestamp + 2.*i/2400000.,
payload=msg,
crc=crc_msg,
index=i,
)
yield dict(
# 1 sample data = 2 IQ samples (hence 2*)
timestamp=timestamp + 2.*i/2400000.,
payload=None,
crc=None,
index=i,
)
return

View File

@@ -0,0 +1,256 @@
#include "demod2400.h"
static inline int slice_phase0(uint16_t *m)
{
return 5 * m[0] - 3 * m[1] - 2 * m[2];
}
static inline int slice_phase1(uint16_t *m)
{
return 4 * m[0] - m[1] - 3 * m[2];
}
static inline int slice_phase2(uint16_t *m)
{
return 3 * m[0] + m[1] - 4 * m[2];
}
static inline int slice_phase3(uint16_t *m)
{
return 2 * m[0] + 3 * m[1] - 5 * m[2];
}
static inline int slice_phase4(uint16_t *m)
{
return m[0] + 5 * m[1] - 5 * m[2] - m[3];
}
int demodulate2400(uint16_t *mag, uint8_t *msg, int len_mag, int *len_msg)
{
uint32_t j;
for (j = 0; j < len_mag / 2 - 300; j++)
{ // SALE
uint16_t *preamble = &mag[j];
int high;
uint32_t base_signal, base_noise;
// quick check: we must have a rising edge 0->1 and a falling edge 12->13
if (!(preamble[0] < preamble[1] && preamble[12] > preamble[13]))
continue;
if (preamble[1] > preamble[2] && // 1
preamble[2] < preamble[3] && preamble[3] > preamble[4] && // 3
preamble[8] < preamble[9] && preamble[9] > preamble[10] && // 9
preamble[10] < preamble[11])
{ // 11-12
// peaks at 1,3,9,11-12: phase 3
high = (preamble[1] + preamble[3] + preamble[9] + preamble[11] + preamble[12]) / 4;
base_signal = preamble[1] + preamble[3] + preamble[9];
base_noise = preamble[5] + preamble[6] + preamble[7];
}
else if (preamble[1] > preamble[2] && // 1
preamble[2] < preamble[3] && preamble[3] > preamble[4] && // 3
preamble[8] < preamble[9] && preamble[9] > preamble[10] && // 9
preamble[11] < preamble[12])
{ // 12
// peaks at 1,3,9,12: phase 4
high = (preamble[1] + preamble[3] + preamble[9] + preamble[12]) / 4;
base_signal = preamble[1] + preamble[3] + preamble[9] + preamble[12];
base_noise = preamble[5] + preamble[6] + preamble[7] + preamble[8];
}
else if (preamble[1] > preamble[2] && // 1
preamble[2] < preamble[3] && preamble[4] > preamble[5] && // 3-4
preamble[8] < preamble[9] && preamble[10] > preamble[11] && // 9-10
preamble[11] < preamble[12])
{ // 12
// peaks at 1,3-4,9-10,12: phase 5
high = (preamble[1] + preamble[3] + preamble[4] + preamble[9] + preamble[10] + preamble[12]) / 4;
base_signal = preamble[1] + preamble[12];
base_noise = preamble[6] + preamble[7];
}
else if (preamble[1] > preamble[2] && // 1
preamble[3] < preamble[4] && preamble[4] > preamble[5] && // 4
preamble[9] < preamble[10] && preamble[10] > preamble[11] && // 10
preamble[11] < preamble[12])
{ // 12
// peaks at 1,4,10,12: phase 6
high = (preamble[1] + preamble[4] + preamble[10] + preamble[12]) / 4;
base_signal = preamble[1] + preamble[4] + preamble[10] + preamble[12];
base_noise = preamble[5] + preamble[6] + preamble[7] + preamble[8];
}
else if (preamble[2] > preamble[3] && // 1-2
preamble[3] < preamble[4] && preamble[4] > preamble[5] && // 4
preamble[9] < preamble[10] && preamble[10] > preamble[11] && // 10
preamble[11] < preamble[12])
{ // 12
// peaks at 1-2,4,10,12: phase 7
high = (preamble[1] + preamble[2] + preamble[4] + preamble[10] + preamble[12]) / 4;
base_signal = preamble[4] + preamble[10] + preamble[12];
base_noise = preamble[6] + preamble[7] + preamble[8];
}
else
{
// no suitable peaks
continue;
}
// Check for enough signal
if (base_signal * 2 < 3 * base_noise) // about 3.5dB SNR
continue;
// Check that the "quiet" bits 6,7,15,16,17 are actually quiet
if (preamble[5] >= high ||
preamble[6] >= high ||
preamble[7] >= high ||
preamble[8] >= high ||
preamble[14] >= high ||
preamble[15] >= high ||
preamble[16] >= high ||
preamble[17] >= high ||
preamble[18] >= high)
{
continue;
}
// // try all phases
// Modes.stats_current.demod_preambles++;
// bestmsg = NULL; bestscore = -2; bestphase = -1;
for (int try_phase = 4; try_phase <= 8; ++try_phase)
{
uint16_t *pPtr;
int phase, i, bytelen;
// Decode all the next 112 bits, regardless of the actual message
// size. We'll check the actual message type later
pPtr = &mag[j + 19] + (try_phase / 5);
phase = try_phase % 5;
bytelen = MODES_LONG_MSG_BYTES;
for (i = 0; i < bytelen; ++i)
{
uint8_t theByte = 0;
switch (phase)
{
case 0:
theByte =
(slice_phase0(pPtr) > 0 ? 0x80 : 0) |
(slice_phase2(pPtr + 2) > 0 ? 0x40 : 0) |
(slice_phase4(pPtr + 4) > 0 ? 0x20 : 0) |
(slice_phase1(pPtr + 7) > 0 ? 0x10 : 0) |
(slice_phase3(pPtr + 9) > 0 ? 0x08 : 0) |
(slice_phase0(pPtr + 12) > 0 ? 0x04 : 0) |
(slice_phase2(pPtr + 14) > 0 ? 0x02 : 0) |
(slice_phase4(pPtr + 16) > 0 ? 0x01 : 0);
phase = 1;
pPtr += 19;
break;
case 1:
theByte =
(slice_phase1(pPtr) > 0 ? 0x80 : 0) |
(slice_phase3(pPtr + 2) > 0 ? 0x40 : 0) |
(slice_phase0(pPtr + 5) > 0 ? 0x20 : 0) |
(slice_phase2(pPtr + 7) > 0 ? 0x10 : 0) |
(slice_phase4(pPtr + 9) > 0 ? 0x08 : 0) |
(slice_phase1(pPtr + 12) > 0 ? 0x04 : 0) |
(slice_phase3(pPtr + 14) > 0 ? 0x02 : 0) |
(slice_phase0(pPtr + 17) > 0 ? 0x01 : 0);
phase = 2;
pPtr += 19;
break;
case 2:
theByte =
(slice_phase2(pPtr) > 0 ? 0x80 : 0) |
(slice_phase4(pPtr + 2) > 0 ? 0x40 : 0) |
(slice_phase1(pPtr + 5) > 0 ? 0x20 : 0) |
(slice_phase3(pPtr + 7) > 0 ? 0x10 : 0) |
(slice_phase0(pPtr + 10) > 0 ? 0x08 : 0) |
(slice_phase2(pPtr + 12) > 0 ? 0x04 : 0) |
(slice_phase4(pPtr + 14) > 0 ? 0x02 : 0) |
(slice_phase1(pPtr + 17) > 0 ? 0x01 : 0);
phase = 3;
pPtr += 19;
break;
case 3:
theByte =
(slice_phase3(pPtr) > 0 ? 0x80 : 0) |
(slice_phase0(pPtr + 3) > 0 ? 0x40 : 0) |
(slice_phase2(pPtr + 5) > 0 ? 0x20 : 0) |
(slice_phase4(pPtr + 7) > 0 ? 0x10 : 0) |
(slice_phase1(pPtr + 10) > 0 ? 0x08 : 0) |
(slice_phase3(pPtr + 12) > 0 ? 0x04 : 0) |
(slice_phase0(pPtr + 15) > 0 ? 0x02 : 0) |
(slice_phase2(pPtr + 17) > 0 ? 0x01 : 0);
phase = 4;
pPtr += 19;
break;
case 4:
theByte =
(slice_phase4(pPtr) > 0 ? 0x80 : 0) |
(slice_phase1(pPtr + 3) > 0 ? 0x40 : 0) |
(slice_phase3(pPtr + 5) > 0 ? 0x20 : 0) |
(slice_phase0(pPtr + 8) > 0 ? 0x10 : 0) |
(slice_phase2(pPtr + 10) > 0 ? 0x08 : 0) |
(slice_phase4(pPtr + 12) > 0 ? 0x04 : 0) |
(slice_phase1(pPtr + 15) > 0 ? 0x02 : 0) |
(slice_phase3(pPtr + 17) > 0 ? 0x01 : 0);
phase = 0;
pPtr += 20;
break;
}
msg[i] = theByte;
if (i == 0)
{
switch (msg[0] >> 3)
{
case 0:
case 4:
case 5:
case 11:
bytelen = MODES_SHORT_MSG_BYTES;
*len_msg = MODES_SHORT_MSG_BYTES;
break;
case 16:
case 17:
case 18:
case 20:
case 21:
case 24:
*len_msg = MODES_LONG_MSG_BYTES;
break;
default:
bytelen = 1; // unknown DF, give up immediately
break;
}
}
}
return j + 1;
}
// Score the mode S message and see if it's any good.
// score = scoreModesMessage(msg, i*8);
// if (score > bestscore) {
// // new high score!
// bestmsg = msg;
// bestscore = score;
// bestphase = try_phase;
// // swap to using the other buffer so we don't clobber our demodulated data
// // (if we find a better result then we'll swap back, but that's OK because
// // we no longer need this copy if we found a better one)
// msg = (msg == msg1) ? msg2 : msg1;
// }
}
return 0;
}

View File

@@ -0,0 +1,11 @@
#ifndef __DEMOD_2400_H__
#define __DEMOD_2400_H__
#define MODES_LONG_MSG_BYTES 14
#define MODES_SHORT_MSG_BYTES 7
#include <stdint.h>
int demodulate2400(uint16_t *mag, uint8_t *msg, int len_mag, int* len_msg);
#endif

View File

@@ -0,0 +1,133 @@
import time
import traceback
import numpy as np
import pyModeS as pms
from pyModeS.extra.demod2400 import demod2400
try:
import rtlsdr # type: ignore
except ImportError:
print(
"------------------------------------------------------------------------"
)
print(
"! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !"
)
print(
"------------------------------------------------------------------------"
)
modes_frequency = 1090e6
sampling_rate = 2.4e6
buffer_size = 16 * 16384
read_size = buffer_size / 2
class RtlReader(object):
def __init__(self, **kwargs):
super(RtlReader, self).__init__()
self.signal_buffer = [] # amplitude of the sample only
self.sdr = rtlsdr.RtlSdr()
self.sdr.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency
self.sdr.gain = "auto"
self.debug = kwargs.get("debug", False)
self.raw_pipe_in = None
self.stop_flag = False
self.exception_queue = None
def _process_buffer(self):
"""process raw IQ data in the buffer"""
# Mode S messages
messages = []
data = (np.array(self.signal_buffer) * 65535).astype(np.uint16)
for s in demod2400(data, self.timestamp):
if s["payload"] is None:
idx = s["index"]
# reset the buffer
self.signal_buffer = self.signal_buffer[idx:]
self.timestamp = s["timestamp"]
break
if self._check_msg(s["payload"]):
messages.append([s["payload"], time.time()]) # s["timestamp"]])
if self.debug:
self._debug_msg(s["payload"])
self.timestamp = s["timestamp"]
return messages
def _check_msg(self, msg):
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
if pms.crc(msg) == 0:
return True
elif df in [20, 21] and msglen == 28:
return True
elif df in [4, 5, 11] and msglen == 14:
return True
def _debug_msg(self, msg):
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
print(msg, pms.icao(msg), df, pms.crc(msg))
print(pms.tell(msg))
elif df in [20, 21] and msglen == 28:
print(msg, pms.icao(msg), df)
elif df in [4, 5, 11] and msglen == 14:
print(msg, pms.icao(msg), df)
else:
# print("[*]", msg)
pass
def _read_callback(self, data, rtlsdr_obj):
amp = np.absolute(data)
self.signal_buffer.extend(amp.tolist())
if len(self.signal_buffer) >= buffer_size:
messages = self._process_buffer()
self.handle_messages(messages)
def handle_messages(self, messages):
"""re-implement this method to handle the messages"""
for msg, t in messages:
# print("%15.9f %s" % (t, msg))
pass
def stop(self, *args, **kwargs):
self.sdr.close()
def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self.raw_pipe_in = raw_pipe_in
self.exception_queue = exception_queue
self.stop_flag = stop_flag
try:
# raise RuntimeError("test exception")
self.timestamp = time.time()
while True:
data = self.sdr.read_samples(read_size)
self._read_callback(data, None)
except Exception as e:
tb = traceback.format_exc()
if self.exception_queue is not None:
self.exception_queue.put(tb)
raise e
if __name__ == "__main__":
import signal
rtl = RtlReader()
signal.signal(signal.SIGINT, rtl.stop)
rtl.debug = True
rtl.run()

View File

@@ -1,156 +0,0 @@
#!/usr/bin/env python
import os
import sys
import time
import argparse
import curses
import signal
import multiprocessing
from pyModeS.streamer.decode import Decode
from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource
support_rawtypes = ["raw", "beast", "skysense"]
parser = argparse.ArgumentParser()
parser.add_argument(
"--source",
help='Choose data source, "rtlsdr" or "net"',
required=True,
default="net",
)
parser.add_argument(
"--connect",
help="Define server, port and data type. Supported data types are: {}".format(
support_rawtypes
),
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
required=False,
)
parser.add_argument(
"--latlon",
help="Receiver latitude and longitude, needed for the surface position, default none",
nargs=2,
metavar=("LAT", "LON"),
default=None,
required=False,
)
parser.add_argument(
"--show-uncertainty",
dest="uncertainty",
help="Display uncertainty values, default off",
action="store_true",
required=False,
default=False,
)
parser.add_argument(
"--dumpto",
help="Folder to dump decoded output, default none",
required=False,
default=None,
)
args = parser.parse_args()
SOURCE = args.source
LATLON = args.latlon
UNCERTAINTY = args.uncertainty
DUMPTO = args.dumpto
if SOURCE == "rtlsdr":
pass
elif SOURCE == "net":
if args.connect is None:
print("Error: --connect argument must not be empty.")
else:
SERVER, PORT, DATATYPE = args.connect
if DATATYPE not in support_rawtypes:
print("Data type not supported, available ones are %s" % support_rawtypes)
else:
print('Source must be "rtlsdr" or "net".')
sys.exit(1)
if DUMPTO is not None:
# append to current folder except root is given
if DUMPTO[0] != "/":
DUMPTO = os.getcwd() + "/" + DUMPTO
if not os.path.isdir(DUMPTO):
print("Error: dump folder (%s) does not exist" % DUMPTO)
sys.exit(1)
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, "w")
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
exception_queue = multiprocessing.Queue()
stop_flag = multiprocessing.Value("b", False)
if SOURCE == "net":
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
elif SOURCE == "rtlsdr":
source = RtlSdrSource()
recv_process = multiprocessing.Process(
target=source.run, args=(raw_pipe_in, stop_flag, exception_queue)
)
decode = Decode(latlon=LATLON, dumpto=DUMPTO)
decode_process = multiprocessing.Process(
target=decode.run, args=(raw_pipe_out, ac_pipe_in, exception_queue)
)
screen = Screen(uncertainty=UNCERTAINTY)
screen_process = multiprocessing.Process(
target=screen.run, args=(ac_pipe_out, exception_queue)
)
def shutdown():
stop_flag.value = True
curses.endwin()
sys.stdout = sys.__stdout__
recv_process.terminate()
decode_process.terminate()
screen_process.terminate()
recv_process.join()
decode_process.join()
screen_process.join()
def closeall(signal, frame):
print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal))
shutdown()
sys.exit(0)
signal.signal(signal.SIGINT, closeall)
recv_process.start()
decode_process.start()
screen_process.start()
while True:
if (
(not recv_process.is_alive())
or (not decode_process.is_alive())
or (not screen_process.is_alive())
):
shutdown()
while not exception_queue.empty():
trackback = exception_queue.get()
print(trackback)
sys.exit(1)
time.sleep(0.01)

155
pyModeS/streamer/modeslive.py Executable file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python
import os
import sys
import time
import argparse
import curses
import signal
import multiprocessing
from pyModeS.streamer.decode import Decode
from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource, RtlSdrSource24
def main():
support_rawtypes = ["raw", "beast", "skysense"]
parser = argparse.ArgumentParser()
parser.add_argument(
"--source",
help='Choose data source, "rtlsdr", "rtlsdr24" or "net"',
required=True,
default="net",
)
parser.add_argument(
"--connect",
help="Define server, port and data type. Supported data types are: {}".format(
support_rawtypes
),
nargs=3,
metavar=("SERVER", "PORT", "DATATYPE"),
default=None,
required=False,
)
parser.add_argument(
"--latlon",
help="Receiver latitude and longitude, needed for the surface position, default none",
nargs=2,
metavar=("LAT", "LON"),
default=None,
required=False,
)
parser.add_argument(
"--show-uncertainty",
dest="uncertainty",
help="Display uncertainty values, default off",
action="store_true",
required=False,
default=False,
)
parser.add_argument(
"--dumpto",
help="Folder to dump decoded output, default none",
required=False,
default=None,
)
args = parser.parse_args()
SOURCE = args.source
LATLON = args.latlon
UNCERTAINTY = args.uncertainty
DUMPTO = args.dumpto
if SOURCE in ["rtlsdr", "rtlsdr24"]:
pass
elif SOURCE == "net":
if args.connect is None:
print("Error: --connect argument must not be empty.")
else:
SERVER, PORT, DATATYPE = args.connect
if DATATYPE not in support_rawtypes:
print(
"Data type not supported, available ones are %s"
% support_rawtypes
)
else:
print('Source must be "rtlsdr" or "net".')
sys.exit(1)
if DUMPTO is not None:
# append to current folder except root is given
if DUMPTO[0] != "/":
DUMPTO = os.getcwd() + "/" + DUMPTO
if not os.path.isdir(DUMPTO):
print("Error: dump folder (%s) does not exist" % DUMPTO)
sys.exit(1)
# redirect all stdout to null, avoiding messing up with the screen
sys.stdout = open(os.devnull, "w")
raw_pipe_in, raw_pipe_out = multiprocessing.Pipe()
ac_pipe_in, ac_pipe_out = multiprocessing.Pipe()
exception_queue = multiprocessing.Queue()
stop_flag = multiprocessing.Value("b", False)
if SOURCE == "net":
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
elif SOURCE == "rtlsdr":
source = RtlSdrSource()
elif SOURCE == "rtlsdr24":
source = RtlSdrSource24()
recv_process = multiprocessing.Process(
target=source.run, args=(raw_pipe_in, stop_flag, exception_queue)
)
decode = Decode(latlon=LATLON, dumpto=DUMPTO)
decode_process = multiprocessing.Process(
target=decode.run, args=(raw_pipe_out, ac_pipe_in, exception_queue)
)
screen = Screen(uncertainty=UNCERTAINTY)
screen_process = multiprocessing.Process(
target=screen.run, args=(ac_pipe_out, exception_queue)
)
def shutdown():
stop_flag.value = True
curses.endwin()
sys.stdout = sys.__stdout__
recv_process.terminate()
decode_process.terminate()
screen_process.terminate()
recv_process.join()
decode_process.join()
screen_process.join()
def closeall(signal, frame):
print("KeyboardInterrupt (ID: {}). Cleaning up...".format(signal))
shutdown()
sys.exit(0)
signal.signal(signal.SIGINT, closeall)
recv_process.start()
decode_process.start()
screen_process.start()
while True:
if (
(not recv_process.is_alive())
or (not decode_process.is_alive())
or (not screen_process.is_alive())
):
shutdown()
while not exception_queue.empty():
trackback = exception_queue.get()
print(trackback)
sys.exit(1)
time.sleep(0.01)

View File

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

66
pyproject.toml Normal file
View File

@@ -0,0 +1,66 @@
[tool.poetry]
name = "pyModeS"
version = "2.11"
description = "Python Mode-S and ADS-B Decoder"
authors = ["Junzi Sun <j.sun-1@tudelft.nl>"]
license = "GNU GPL v3"
readme = "README.rst"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3",
"Typing :: Typed",
]
packages = [
{ include = "pyModeS", from = "." },
]
include = [
"LICENSE",
"*.pyx",
"*.pxd",
"*.pyi",
"py.typed",
{ path = "src/pyModeS/**/*.so", format = "wheel" }
]
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
[tool.poetry.scripts]
modeslive = "pyModeS.streamer.modeslive:main"
[tool.poetry.dependencies]
python = "^3.8"
numpy = "^1.24"
pyzmq = "^24.0"
pyrtlsdr = {version = "^0.2.93", optional = true}
[tool.poetry.group.dev.dependencies]
Cython = "^0.29.32"
mypy = "^0.991"
flake8 = "^5.0.0"
black = "^22.12.0"
isort = "^5.11.4"
pytest = "^7.2.0"
pytest-cov = "^4.0.0"
codecov = "^2.1.12"
ipykernel = "^6.20.0"
[tool.poetry.extras]
rtlsdr = ["pyrtlsdr"]
[tool.black]
line-length = 80
target_version = ['py38', 'py39', 'py310', 'py311']
include = '\.pyi?$'
[tool.isort]
line_length = 80
profile = "black"
[build-system]
requires = ["poetry-core>=1.0.0", "Cython>=0.29.32"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,3 +0,0 @@
# https://github.com/embray/setup.cfg
[metadata]
license_file = LICENSE

101
setup.py
View File

@@ -1,101 +0,0 @@
"""A setuptools based setup module.
See:
https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject
Steps for deploying a new version:
1. Increase the version number
2. remove the old deployment under [dist] and [build] folder
3. run: python setup.py sdist
4. twine upload dist/*
"""
import sys
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# To use a consistent encoding
from codecs import open
from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(here, "README.rst"), encoding="utf-8") as f:
long_description = f.read()
details = dict(
name="pyModeS",
version="2.11",
description="Python Mode-S and ADS-B Decoder",
long_description=long_description,
url="https://github.com/junzis/pyModeS",
author="Junzi Sun",
author_email="j.sun-1@tudelft.nl",
license="GNU GPL v3",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3",
],
keywords="Mode-S ADS-B EHS ELS Comm-B",
packages=find_packages(exclude=["contrib", "docs", "tests"]),
# typing_extensions are no longer necessary after Python 3.8 (TypedDict)
install_requires=["numpy", "pyzmq", "typing_extensions"],
extras_require={"fast": ["Cython"]},
package_data={
"pyModeS": ["*.pyx", "*.pxd", "py.typed"],
"pyModeS.decoder.flarm": ["*.pyx", "*.pxd", "*.pyi"],
},
scripts=["pyModeS/streamer/modeslive"],
)
try:
from distutils.core import Extension
from Cython.Build import cythonize
compile_args = []
include_dirs = ["pyModeS/decoder/flarm"]
if sys.platform == "linux":
compile_args += [
"-march=native",
"-O3",
"-msse",
"-msse2",
"-mfma",
"-mfpmath=sse",
"-Wno-pointer-sign",
]
extensions = [
Extension("pyModeS.c_common", ["pyModeS/c_common.pyx"]),
Extension(
"pyModeS.decoder.flarm.decode",
[
"pyModeS/decoder/flarm/decode.pyx",
"pyModeS/decoder/flarm/core.c",
],
extra_compile_args=compile_args,
include_dirs=include_dirs,
),
]
setup(
**dict(
details,
ext_modules=cythonize(
extensions,
include_path=include_dirs,
compiler_directives={"binding": True, "language_level": 3},
),
)
)
except ImportError:
setup(**details)

View File

@@ -1,40 +0,0 @@
import pyModeS as pms
msgs = [
"80E1983958C392E8710C642D0BAD",
"8609F8E19E90653083FDE096BE26",
"80011B41F40017D0000012E06B45",
"808182365813667446EB715E253D",
"80E1953058AB029BE8F4E60D989E",
"8667ECFA8FE06B30E59A124D5AEF",
"80030AA000042C0AD05D6205DB2C",
"8631A80000373C463B6E7A00008B",
"80E19131588B146108DE703F3C47",
"825CC4A0001595C4600030A40000",
"808182365813667438EB710EB6D8",
"80A18393581D3655E90354A664B2",
"8081823658136309B4F22BCB5F8D",
"80E1953058AB06516E8602756DD8",
"80E1991058C9063AB6A3E4744DC3",
"8073EC91840AFCED8F300BE765C5",
"8571F54AA814BF8130066A19A31D",
"864070E1990D3B1E78048BAE6987",
"80E1991058C9063AB6A3E4744DC3",
"80818298581581F7CAF8C3C1EEA4",
"80004E98BC90000FF01010951298",
"8526D57E4D963C92CDEE1B6C7C49",
"802A613C93F65A7FF803A51B5ADB",
"85B6C54279B67BE2A0001998FFDA",
"851944F15881648338D1AF4B7A27",
"8321014858208000787905B0E800",
"866DD078EDEBD330404FFFAE9BA5",
"80E196905AB503260E835D849E35",
]
for msg in msgs:
print("-" * 80)
print(msg)
print("ARA", pms.acas.ara(msg))
print("RAC", pms.acas.rac(msg))
print("MTE", pms.acas.mte(msg))
print("RAT", pms.acas.rat(msg))