3 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
39 changed files with 1773 additions and 890 deletions

3
.flake8 Normal file
View File

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

View File

@@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -1,16 +0,0 @@
name: build and publish
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and publish to pypi
uses: JRubics/poetry-publish@v2.0
with:
poetry_version: "==1.8.2"
pypi_token: ${{ secrets.PYPI_API_TOKEN_PYMODES }}

29
.github/workflows/pypi-publish.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: PyPI Publish
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

View File

@@ -6,7 +6,7 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
POETRY_VERSION: "1.6.1" POETRY_VERSION: "1.3.1"
jobs: jobs:
deploy: deploy:
@@ -14,27 +14,32 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"] python-version: ["3.8", "3.9", "3.10", "3.11"]
env: env:
PYTHON_VERSION: ${{ matrix.python-version }} PYTHON_VERSION: ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} 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). # virtualenv cache should depends on OS, Python version and `poetry.lock` (and optionally workflow files).
- name: Cache Packages - name: Cache Packages
uses: actions/cache@v4 uses: actions/cache@v3
if: ${{ !startsWith(runner.os, 'windows') }} if: matrix.os != 'windows-latest'
with: with:
path: | path: ~/.local
~/.local
.venv
key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }} key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }}
- name: Add poetry to windows path - name: Add poetry to windows path
@@ -43,20 +48,18 @@ jobs:
echo "C:\Users\runneradmin\.local\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append echo "C:\Users\runneradmin\.local\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install and configure Poetry - name: Install and configure Poetry
uses: snok/install-poetry@v1.4.0 uses: snok/install-poetry@v1.3.3
with: with:
version: ${{ env.POETRY_VERSION }} version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true virtualenvs-create: true
virtualenvs-in-project: true virtualenvs-in-project: true
- name: Display Python version
run: poetry run python -c "import sys; print(sys.version)"
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install poetry install
- name: Type checking - name: Type checking
if: ${{ env.PYTHON_VERSION != '3.7' }}
run: | run: |
poetry run mypy pyModeS poetry run mypy pyModeS
@@ -65,6 +68,7 @@ jobs:
poetry run pytest tests --cov --cov-report term-missing poetry run pytest tests --cov --cov-report term-missing
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 if: ${{ github.event_name != 'pull_request_target' && env.PYTHON_VERSION == '3.10' }}
uses: codecov/codecov-action@v2
with: with:
env_vars: PYTHON_VERSION env_vars: PYTHON_VERSION

6
.gitignore vendored
View File

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

View File

@@ -1,18 +1,26 @@
import os import os
import shutil import shutil
import sys import sys
from distutils.core import Distribution, Extension
from distutils.command import build_ext
# import pip
from Cython.Build import cythonize from Cython.Build import cythonize
from setuptools import Distribution, Extension
from setuptools.command import build_ext
def build() -> None: def build() -> None:
compile_args = [] compile_args = []
if sys.platform == "linux": if sys.platform == "linux":
compile_args += ["-Wno-pointer-sign", "-Wno-unused-variable"] compile_args += [
"-march=native",
"-O3",
"-msse",
"-msse2",
"-mfma",
"-mfpmath=sse",
"-Wno-pointer-sign",
"-Wno-unused-variable",
]
extensions = [ extensions = [
Extension( Extension(
@@ -29,16 +37,16 @@ def build() -> None:
extra_compile_args=compile_args, extra_compile_args=compile_args,
include_dirs=["pyModeS/decoder/flarm"], include_dirs=["pyModeS/decoder/flarm"],
), ),
# Extension( Extension(
# "pyModeS.extra.demod2400.core", "pyModeS.extra.demod2400.core",
# [ [
# "pyModeS/extra/demod2400/core.pyx", "pyModeS/extra/demod2400/core.pyx",
# "pyModeS/extra/demod2400/demod2400.c", "pyModeS/extra/demod2400/demod2400.c",
# ], ],
# extra_compile_args=compile_args, extra_compile_args=compile_args,
# include_dirs=["pyModeS/extra/demod2400"], include_dirs=["pyModeS/extra/demod2400"],
# libraries=["m"], libraries=["m"],
# ), ),
] ]
ext_modules = cythonize( ext_modules = cythonize(
@@ -46,7 +54,9 @@ def build() -> None:
compiler_directives={"binding": True, "language_level": 3}, compiler_directives={"binding": True, "language_level": 3},
) )
distribution = Distribution({"name": "extended", "ext_modules": ext_modules}) distribution = Distribution(
{"name": "extended", "ext_modules": ext_modules}
)
distribution.package_dir = "extended" # type: ignore distribution.package_dir = "extended" # type: ignore
cmd = build_ext.build_ext(distribution) cmd = build_ext.build_ext(distribution)
@@ -55,7 +65,7 @@ def build() -> None:
cmd.run() cmd.run()
# Copy built extensions back to the project # Copy built extensions back to the project
for output in cmd.get_output_mapping(): for output in cmd.get_outputs():
relative_extension = os.path.relpath(output, cmd.build_lib) relative_extension = os.path.relpath(output, cmd.build_lib)
shutil.copyfile(output, relative_extension) shutil.copyfile(output, relative_extension)
mode = os.stat(relative_extension).st_mode mode = os.stat(relative_extension).st_mode

1635
poetry.lock generated

File diff suppressed because it is too large Load Diff

3
pyModeS/.gitignore vendored
View File

@@ -1,3 +0,0 @@
decoder/flarm/decode.c
extra/demod2400/core.c
c_common.c

View File

@@ -311,7 +311,7 @@ cpdef int altitude(str binstr):
if bin2int(binstr) == 0: if bin2int(binstr) == 0:
# altitude unknown or invalid # altitude unknown or invalid
alt = -999999 alt = -9999
elif Mbit == 48: # unit in ft, "0" -> 48 elif Mbit == 48: # unit in ft, "0" -> 48
if Qbit == 49: # 25ft interval, "1" -> 49 if Qbit == 49: # 25ft interval, "1" -> 49

View File

@@ -211,7 +211,7 @@ def tell(msg: str) -> None:
_print("True airspeed", commb.tas50(msg), "knots") _print("True airspeed", commb.tas50(msg), "knots")
if BDS == "BDS60": if BDS == "BDS60":
_print("Magnetic Heading", commb.hdg60(msg), "degrees") _print("Megnatic Heading", commb.hdg60(msg), "degrees")
_print("Indicated airspeed", commb.ias60(msg), "knots") _print("Indicated airspeed", commb.ias60(msg), "knots")
_print("Mach number", commb.mach60(msg)) _print("Mach number", commb.mach60(msg))
_print("Vertical rate (Baro)", commb.vr60baro(msg), "feet/minute") _print("Vertical rate (Baro)", commb.vr60baro(msg), "feet/minute")

View File

@@ -12,7 +12,6 @@ The ADS-B module also imports functions from the following modules:
""" """
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from .. import common from .. import common
@@ -162,7 +161,9 @@ def position(
raise RuntimeError("Incorrect or inconsistent message types") 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. """Decode position with only one message.
A reference position is required, which can be previously A reference position is required, which can be previously
@@ -424,7 +425,7 @@ def nic_v1(msg: str, NICs: int) -> tuple[int, None | float, None | float]:
return NIC, Rc, VPL return NIC, Rc, VPL
def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int | None, int | None]: def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int, int]:
"""Calculate NIC, navigation integrity category, for ADS-B version 2 """Calculate NIC, navigation integrity category, for ADS-B version 2
Args: Args:
@@ -455,9 +456,10 @@ def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int | None, int | None]:
try: try:
if isinstance(NIC, dict): if isinstance(NIC, dict):
NIC = NIC[NICs] NIC = NIC[NICs]
Rc = uncertainty.NICv2[NIC][NICs]["Rc"] Rc = uncertainty.NICv2[NIC][NICs]["Rc"]
except KeyError: except KeyError:
return None, None Rc = uncertainty.NA
return NIC, Rc # type: ignore return NIC, Rc # type: ignore

View File

@@ -88,9 +88,9 @@ def capability(msg: str) -> tuple[int, None | str]:
) )
elif ca == 7: elif ca == 7:
text = ( text = (
"Downlink Request value is not 0, " "Downlink Request value is 0, "
"or the Flight Status is 2, 3, 4 or 5, " "or the Flight Status is 2, 3, 4 or 5, "
"and either airborne or on the ground" "either airborne or on the ground"
) )
else: else:
text = None text = None

View File

@@ -82,7 +82,7 @@ def airborne_position(
if lon > 180: if lon > 180:
lon = lon - 360 lon = lon - 360
return lat, lon return round(lat, 5), round(lon, 5)
def airborne_position_with_ref( def airborne_position_with_ref(
@@ -129,7 +129,7 @@ def airborne_position_with_ref(
lon = d_lon * (m + cprlon) lon = d_lon * (m + cprlon)
return lat, lon return round(lat, 5), round(lon, 5)
def altitude(msg: str) -> None | int: def altitude(msg: str) -> None | int:
@@ -152,11 +152,6 @@ def altitude(msg: str) -> None | int:
if tc < 19: if tc < 19:
altcode = altbin[0:6] + "0" + altbin[6:] altcode = altbin[0:6] + "0" + altbin[6:]
alt = common.altitude(altcode) return common.altitude(altcode)
if alt != -999999:
return alt
else:
# return None if altitude is invalid
return None
else: else:
return common.bin2int(altbin) * 3.28084 # type: ignore return common.bin2int(altbin) * 3.28084 # type: ignore

View File

@@ -19,6 +19,7 @@ def surface_position(
lat_ref: float, lat_ref: float,
lon_ref: float, lon_ref: float,
) -> None | tuple[float, float]: ) -> None | tuple[float, float]:
"""Decode surface position from a pair of even and odd position message, """Decode surface position from a pair of even and odd position message,
the lat/lon of receiver must be provided to yield the correct solution. the lat/lon of receiver must be provided to yield the correct solution.
@@ -91,7 +92,7 @@ def surface_position(
imin = min(range(4), key=dls.__getitem__) imin = min(range(4), key=dls.__getitem__)
lon = lons[imin] lon = lons[imin]
return lat, lon return round(lat, 5), round(lon, 5)
def surface_position_with_ref( def surface_position_with_ref(
@@ -138,12 +139,12 @@ def surface_position_with_ref(
lon = d_lon * (m + cprlon) lon = d_lon * (m + cprlon)
return lat, lon return round(lat, 5), round(lon, 5)
def surface_velocity( def surface_velocity(
msg: str, source: bool = False msg: str, source: bool = False
) -> tuple[None | float, None | float, int, str]: ) -> tuple[None | float, float, int, str]:
"""Decode surface velocity from a surface position message """Decode surface velocity from a surface position message
Args: Args:
@@ -172,6 +173,7 @@ def surface_velocity(
trk_status = int(mb[12]) trk_status = int(mb[12])
if trk_status == 1: if trk_status == 1:
trk = common.bin2int(mb[13:20]) * 360 / 128 trk = common.bin2int(mb[13:20]) * 360 / 128
trk = round(trk, 1)
else: else:
trk = None trk = None

View File

@@ -48,6 +48,7 @@ def airborne_velocity(
spd: None | float spd: None | float
if subtype in (1, 2): if subtype in (1, 2):
v_ew = common.bin2int(mb[14:24]) v_ew = common.bin2int(mb[14:24])
v_ns = common.bin2int(mb[25:35]) v_ns = common.bin2int(mb[25:35])
@@ -76,7 +77,7 @@ def airborne_velocity(
trk = math.degrees(trk) # convert to degrees trk = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val trk = trk if trk >= 0 else trk + 360 # no negative val
trk_or_hdg = trk trk_or_hdg = round(trk, 2)
spd_type = "GS" spd_type = "GS"
dir_type = "TRUE_NORTH" dir_type = "TRUE_NORTH"
@@ -86,6 +87,7 @@ def airborne_velocity(
hdg = None hdg = None
else: else:
hdg = common.bin2int(mb[14:24]) / 1024 * 360.0 hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
hdg = round(hdg, 2)
trk_or_hdg = hdg trk_or_hdg = hdg

View File

@@ -72,7 +72,7 @@ def wind44(msg: str) -> Tuple[Optional[int], Optional[float]]:
speed = common.bin2int(d[5:14]) # knots speed = common.bin2int(d[5:14]) # knots
direction = common.bin2int(d[14:23]) * 180 / 256 # degree direction = common.bin2int(d[14:23]) * 180 / 256 # degree
return speed, direction return round(speed, 0), round(direction, 1)
def temp44(msg: str) -> Tuple[float, float]: def temp44(msg: str) -> Tuple[float, float]:
@@ -96,8 +96,10 @@ def temp44(msg: str) -> Tuple[float, float]:
value = value - 1024 value = value - 1024
temp = value * 0.25 # celsius temp = value * 0.25 # celsius
temp = round(temp, 2)
temp_alternative = value * 0.125 # celsius temp_alternative = value * 0.125 # celsius
temp_alternative = round(temp_alternative, 3)
return temp, temp_alternative return temp, temp_alternative
@@ -138,7 +140,7 @@ def hum44(msg: str) -> Optional[float]:
hm = common.bin2int(d[50:56]) * 100 / 64 # % hm = common.bin2int(d[50:56]) * 100 / 64 # %
return hm return round(hm, 1)
def turb44(msg: str) -> Optional[int]: def turb44(msg: str) -> Optional[int]:

View File

@@ -171,6 +171,7 @@ def temp45(msg: str) -> Optional[float]:
value = value - 512 value = value - 512
temp = value * 0.25 # celsius temp = value * 0.25 # celsius
temp = round(temp, 1)
return temp return temp

View File

@@ -81,7 +81,7 @@ def roll50(msg: str) -> Optional[float]:
value = value - 512 value = value - 512
angle = value * 45 / 256 # degree angle = value * 45 / 256 # degree
return angle return round(angle, 1)
def trk50(msg: str) -> Optional[float]: def trk50(msg: str) -> Optional[float]:
@@ -110,7 +110,7 @@ def trk50(msg: str) -> Optional[float]:
if trk < 0: if trk < 0:
trk = 360 + trk trk = 360 + trk
return trk return round(trk, 3)
def gs50(msg: str) -> Optional[float]: def gs50(msg: str) -> Optional[float]:
@@ -154,7 +154,7 @@ def rtrk50(msg: str) -> Optional[float]:
value = value - 512 value = value - 512
angle = value * 8 / 256 # degree / sec angle = value * 8 / 256 # degree / sec
return angle return round(angle, 3)
def tas50(msg: str) -> Optional[float]: def tas50(msg: str) -> Optional[float]:

View File

@@ -86,7 +86,7 @@ def hdg53(msg: str) -> Optional[float]:
if hdg < 0: if hdg < 0:
hdg = 360 + hdg hdg = 360 + hdg
return hdg return round(hdg, 3)
def ias53(msg: str) -> Optional[float]: def ias53(msg: str) -> Optional[float]:
@@ -122,7 +122,7 @@ def mach53(msg: str) -> Optional[float]:
return None return None
mach = common.bin2int(d[24:33]) * 0.008 mach = common.bin2int(d[24:33]) * 0.008
return mach return round(mach, 3)
def tas53(msg: str) -> Optional[float]: def tas53(msg: str) -> Optional[float]:
@@ -140,7 +140,7 @@ def tas53(msg: str) -> Optional[float]:
return None return None
tas = common.bin2int(d[34:46]) * 0.5 # kts tas = common.bin2int(d[34:46]) * 0.5 # kts
return tas return round(tas, 1)
def vr53(msg: str) -> Optional[int]: def vr53(msg: str) -> Optional[int]:

View File

@@ -200,6 +200,7 @@ def selected_heading(msg: str) -> None | float:
else: else:
hdg_sign = int(mb[30]) hdg_sign = int(mb[30])
hdg = (hdg_sign + 1) * common.bin2int(mb[31:39]) * (180 / 256) hdg = (hdg_sign + 1) * common.bin2int(mb[31:39]) * (180 / 256)
hdg = round(hdg, 2)
return hdg return hdg

View File

@@ -1,5 +1,4 @@
from typing import TypedDict from typing import TypedDict
from typing_extensions import Annotated
from .decode import flarm as flarm_decode from .decode import flarm as flarm_decode
@@ -11,8 +10,8 @@ class DecodedMessage(TypedDict):
icao24: str icao24: str
latitude: float latitude: float
longitude: float longitude: float
altitude: Annotated[int, "m"] altitude: int
vertical_speed: Annotated[float, "m/s"] vertical_speed: float
groundspeed: int groundspeed: int
track: int track: int
type: str type: str

View File

@@ -130,18 +130,17 @@ def flarm(long timestamp, str msg, float refLat, float refLon, **kwargs):
return dict( return dict(
timestamp=timestamp, timestamp=timestamp,
icao24=icao24, icao24=icao24,
latitude=lat * 1e-7, latitude=round(lat * 1e-7, 6),
longitude=lon * 1e-7, longitude=round(lon * 1e-7, 6),
geoaltitude=altitude, geoaltitude=altitude,
vertical_speed=raw_vs * mult_factor / 10, vertical_speed=raw_vs * mult_factor / 10,
groundspeed=speed, groundspeed=round(speed),
track=heading4 - 4 * turningRate(heading4, heading8) / 4, track=round(heading4 - 4 * turningRate(heading4, heading8) / 4),
type=AIRCRAFT_TYPES[aircraft_type], type=AIRCRAFT_TYPES[aircraft_type],
sensorLatitude=refLat, sensorLatitude=refLat,
sensorLongitude=refLon, sensorLongitude=refLon,
isIcao24=magic==0x10, isIcao24=magic==0x10,
noTrack=noTrack, noTrack=noTrack,
stealth=stealth, stealth=stealth,
gps=gps,
**kwargs **kwargs
) )

View File

@@ -131,7 +131,7 @@ def lockout(msg):
if uf(msg) in {4, 5, 20, 21}: if uf(msg) in {4, 5, 20, 21}:
lockout = False lockout = False
di = mbytes[1] & 0x7 di = mbytes[1] & 0x7
if (di == 1 or di == 7): if di == 7:
# LOS # LOS
if ((mbytes[3] & 0x40) >> 6) == 1: if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True lockout = True
@@ -187,16 +187,10 @@ def uplink_fields(msg):
if RR > 15: if RR > 15:
BDS1 = RR - 16 BDS1 = RR - 16
BDS2 = 0 BDS2 = 0
if di == 0: if di == 0 or di == 1:
# II
II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
elif di == 1:
# II # II
II = (mbytes[2] >> 4) & 0xF II = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II) IC = "II" + str(II)
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
elif di == 7: elif di == 7:
# LOS # LOS
if ((mbytes[3] & 0x40) >> 6) == 1: if ((mbytes[3] & 0x40) >> 6) == 1:

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,22 +1,14 @@
from __future__ import annotations
import time import time
import traceback import traceback
import numpy as np import numpy as np
import pyModeS as pms import pyModeS as pms
from typing import Any
import_msg = """
---------------------------------------------------------------------
Warning: pyrtlsdr not installed (required for using RTL-SDR devices)!
---------------------------------------------------------------------"""
try: try:
import rtlsdr # type: ignore import rtlsdr # type: ignore
except ImportError: except:
print(import_msg) print("------------------------------------------------------------------------")
print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !")
print("------------------------------------------------------------------------")
sampling_rate = 2e6 sampling_rate = 2e6
smaples_per_microsec = 2 smaples_per_microsec = 2
@@ -32,9 +24,9 @@ th_amp_diff = 0.8 # signal amplitude threshold difference between 0 and 1 bit
class RtlReader(object): class RtlReader(object):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs):
super(RtlReader, self).__init__() super(RtlReader, self).__init__()
self.signal_buffer: list[float] = [] # amplitude of the sample only self.signal_buffer = [] # amplitude of the sample only
self.sdr = rtlsdr.RtlSdr() self.sdr = rtlsdr.RtlSdr()
self.sdr.sample_rate = sampling_rate self.sdr.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency self.sdr.center_freq = modes_frequency
@@ -47,7 +39,7 @@ class RtlReader(object):
self.exception_queue = None self.exception_queue = None
def _calc_noise(self) -> float: def _calc_noise(self):
"""Calculate noise floor""" """Calculate noise floor"""
window = smaples_per_microsec * 100 window = smaples_per_microsec * 100
total_len = len(self.signal_buffer) total_len = len(self.signal_buffer)
@@ -58,7 +50,7 @@ class RtlReader(object):
) )
return min(means) return min(means)
def _process_buffer(self) -> list[list[Any]]: def _process_buffer(self):
"""process raw IQ data in the buffer""" """process raw IQ data in the buffer"""
# update noise floor # update noise floor
@@ -78,18 +70,17 @@ class RtlReader(object):
i += 1 i += 1
continue continue
frame_start = i + pbits * 2 if self._check_preamble(self.signal_buffer[i : i + pbits * 2]):
if self._check_preamble(self.signal_buffer[i:frame_start]): frame_start = i + pbits * 2
frame_end = i + pbits * 2 + (fbits + 1) * 2
frame_length = (fbits + 1) * 2 frame_length = (fbits + 1) * 2
frame_end = frame_start + frame_length
frame_pulses = self.signal_buffer[frame_start:frame_end] frame_pulses = self.signal_buffer[frame_start:frame_end]
threshold = max(frame_pulses) * 0.2 threshold = max(frame_pulses) * 0.2
msgbin: list[int] = [] msgbin = []
for j in range(0, frame_length, 2): for j in range(0, frame_length, 2):
j_2 = j + 2 p2 = frame_pulses[j : j + 2]
p2 = frame_pulses[j:j_2]
if len(p2) < 2: if len(p2) < 2:
break break
@@ -126,7 +117,7 @@ class RtlReader(object):
return messages return messages
def _check_preamble(self, pulses) -> bool: def _check_preamble(self, pulses):
if len(pulses) != 16: if len(pulses) != 16:
return False return False
@@ -136,7 +127,7 @@ class RtlReader(object):
return True return True
def _check_msg(self, msg) -> bool: def _check_msg(self, msg):
df = pms.df(msg) df = pms.df(msg)
msglen = len(msg) msglen = len(msg)
if df == 17 and msglen == 28: if df == 17 and msglen == 28:
@@ -146,9 +137,8 @@ class RtlReader(object):
return True return True
elif df in [4, 5, 11] and msglen == 14: elif df in [4, 5, 11] and msglen == 14:
return True return True
return False
def _debug_msg(self, msg) -> None: def _debug_msg(self, msg):
df = pms.df(msg) df = pms.df(msg)
msglen = len(msg) msglen = len(msg)
if df == 17 and msglen == 28: if df == 17 and msglen == 28:
@@ -161,7 +151,7 @@ class RtlReader(object):
# print("[*]", msg) # print("[*]", msg)
pass pass
def _read_callback(self, data, rtlsdr_obj) -> None: def _read_callback(self, data, rtlsdr_obj):
amp = np.absolute(data) amp = np.absolute(data)
self.signal_buffer.extend(amp.tolist()) self.signal_buffer.extend(amp.tolist())
@@ -169,18 +159,16 @@ class RtlReader(object):
messages = self._process_buffer() messages = self._process_buffer()
self.handle_messages(messages) self.handle_messages(messages)
def handle_messages(self, messages) -> None: def handle_messages(self, messages):
"""re-implement this method to handle the messages""" """re-implement this method to handle the messages"""
for msg, t in messages: for msg, t in messages:
# print("%15.9f %s" % (t, msg)) # print("%15.9f %s" % (t, msg))
pass pass
def stop(self, *args, **kwargs) -> None: def stop(self, *args, **kwargs):
self.sdr.close() self.sdr.close()
def run( def run(self, raw_pipe_in=None, stop_flag=None, exception_queue=None):
self, raw_pipe_in=None, stop_flag=None, exception_queue=None
) -> None:
self.raw_pipe_in = raw_pipe_in self.raw_pipe_in = raw_pipe_in
self.exception_queue = exception_queue self.exception_queue = exception_queue
self.stop_flag = stop_flag self.stop_flag = stop_flag

View File

@@ -6,7 +6,6 @@ import time
import pyModeS as pms import pyModeS as pms
import traceback import traceback
import zmq import zmq
import math
class TcpClient(object): class TcpClient(object):
@@ -150,103 +149,6 @@ class TcpClient(object):
messages.append([msg, ts]) messages.append([msg, ts])
return messages return messages
def read_beast_buffer_rssi_piaware(self):
"""Handle mode-s beast data type.
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
2 byte Mode-AC
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
7 byte Mode-S short frame
<esc> "3" : 6 byte MLAT timestamp, 1 byte signal level,
14 byte Mode-S long frame
<esc> "4" : 6 byte MLAT timestamp, status data, DIP switch
configuration settings (not on Mode-S Beast classic)
<esc><esc>: true 0x1a
<esc> is 0x1a, and "1", "2" and "3" are 0x31, 0x32 and 0x33
timestamp:
wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp
"""
messages_mlat = []
msg = []
i = 0
# process the buffer until the last divider <esc> 0x1a
# then, reset the self.buffer with the remainder
while i < len(self.buffer):
if self.buffer[i : i + 2] == [0x1A, 0x1A]:
msg.append(0x1A)
i += 1
elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1A):
# special case where the last bit is 0x1a
msg.append(0x1A)
elif self.buffer[i] == 0x1A:
if i == len(self.buffer) - 1:
# special case where the last bit is 0x1a
msg.append(0x1A)
elif len(msg) > 0:
messages_mlat.append(msg)
msg = []
else:
msg.append(self.buffer[i])
i += 1
# save the reminder for next reading cycle, if not empty
if len(msg) > 0:
reminder = []
for i, m in enumerate(msg):
if (m == 0x1A) and (i < len(msg) - 1):
# rewind 0x1a, except when it is at the last bit
reminder.extend([m, m])
else:
reminder.append(m)
self.buffer = [0x1A] + msg
else:
self.buffer = []
# extract messages
messages = []
for mm in messages_mlat:
ts = time.time()
msgtype = mm[0]
# print(''.join('%02X' % i for i in mm))
if msgtype == 0x32:
# Mode-S Short Message, 7 byte, 14-len hexstr
msg = "".join("%02X" % i for i in mm[8:15])
elif msgtype == 0x33:
# Mode-S Long Message, 14 byte, 28-len hexstr
msg = "".join("%02X" % i for i in mm[8:22])
else:
# Other message tupe
continue
if len(msg) not in [14, 28]:
continue
'''
we get the raw 0-255 byte value (raw_rssi = mm[7])
we scale it to 0.0 - 1.0 (voltage = raw_rssi / 255)
we convert it to a dBFS power value (rolling the squaring of the voltage into the dB calculation)
'''
df = pms.df(msg)
raw_rssi = mm[7] # eighth byte of Mode-S message should contain RSSI value
rssi_ratio = raw_rssi / 255
signalLevel = rssi_ratio ** 2
dbfs_rssi = 10 * math.log10(signalLevel)
# skip incomplete message
if df in [0, 4, 5, 11] and len(msg) != 14:
continue
if df in [16, 17, 18, 19, 20, 21, 24] and len(msg) != 28:
continue
messages.append([msg, dbfs_rssi, ts])
return messages
def read_skysense_buffer(self): def read_skysense_buffer(self):
"""Skysense stream format. """Skysense stream format.

View File

@@ -231,7 +231,7 @@ def squawk(binstr: str) -> str:
binstr (String): 13 bits binary string binstr (String): 13 bits binary string
Returns: Returns:
string: squawk code int: altitude in ft
""" """
if len(binstr) != 13 or not set(binstr).issubset(set("01")): if len(binstr) != 13 or not set(binstr).issubset(set("01")):

View File

@@ -73,15 +73,8 @@ class Decode:
"VFOMr": None, "VFOMr": None,
"PE_RCu": None, "PE_RCu": None,
"PE_VPL": None, "PE_VPL": None,
"hum44" : None,
"p44" : None,
"temp44" : None,
"turb44" : None,
"wind44" : None,
} }
self.acs[icao]["tc"] = tc
self.acs[icao]["icao"] = icao
self.acs[icao]["t"] = t self.acs[icao]["t"] = t
self.acs[icao]["live"] = int(t) self.acs[icao]["live"] = int(t)
@@ -162,7 +155,7 @@ class Decode:
ac["nic_bc"] = pms.adsb.nic_b(msg) ac["nic_bc"] = pms.adsb.nic_b(msg)
if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22): if (5 <= tc <= 8) or (9 <= tc <= 18) or (20 <= tc <= 22):
ac["NUCp"], ac["HPL"], ac["RCu"], ac["RCv"] = pms.adsb.nuc_p(msg) ac["HPL"], ac["RCu"], ac["RCv"] = pms.adsb.nuc_p(msg)
if (ac["ver"] == 1) and ("nic_s" in ac.keys()): if (ac["ver"] == 1) and ("nic_s" in ac.keys()):
ac["Rc"], ac["VPL"] = pms.adsb.nic_v1(msg, ac["nic_s"]) ac["Rc"], ac["VPL"] = pms.adsb.nic_v1(msg, ac["nic_s"])
@@ -171,20 +164,20 @@ class Decode:
and ("nic_a" in ac.keys()) and ("nic_a" in ac.keys())
and ("nic_bc" in ac.keys()) and ("nic_bc" in ac.keys())
): ):
ac["NIC"], ac["Rc"] = pms.adsb.nic_v2(msg, ac["nic_a"], ac["nic_bc"]) ac["Rc"] = pms.adsb.nic_v2(msg, ac["nic_a"], ac["nic_bc"])
if tc == 19: if tc == 19:
ac["NUCv"], ac["HVE"], ac["VVE"] = pms.adsb.nuc_v(msg) ac["HVE"], ac["VVE"] = pms.adsb.nuc_v(msg)
if ac["ver"] in [1, 2]: if ac["ver"] in [1, 2]:
ac["NACv"], ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg) ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg)
if tc == 29: if tc == 29:
ac["PE_RCu"], ac["PE_VPL"], ac["base"] = pms.adsb.sil(msg, ac["ver"]) ac["PE_RCu"], ac["PE_VPL"], ac["base"] = pms.adsb.sil(msg, ac["ver"])
ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg) ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
if tc == 31: if tc == 31:
ac["ver"] = pms.adsb.version(msg) ac["ver"] = pms.adsb.version(msg)
ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg) ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
ac["PE_RCu"], ac["PE_VPL"], ac["sil_base"] = pms.adsb.sil( ac["PE_RCu"], ac["PE_VPL"], ac["sil_base"] = pms.adsb.sil(
msg, ac["ver"] msg, ac["ver"]
) )
@@ -201,8 +194,6 @@ class Decode:
if icao not in self.acs: if icao not in self.acs:
continue continue
self.acs[icao]["icao"] = icao
self.acs[icao]["t"] = t
self.acs[icao]["live"] = int(t) self.acs[icao]["live"] = int(t)
bds = pms.bds.infer(msg) bds = pms.bds.infer(msg)
@@ -226,10 +217,8 @@ class Decode:
output_buffer.append([t, icao, "rtrk50", rtrk50]) output_buffer.append([t, icao, "rtrk50", rtrk50])
if trk50: if trk50:
self.acs[icao]["trk50"] = trk50
output_buffer.append([t, icao, "trk50", trk50]) output_buffer.append([t, icao, "trk50", trk50])
if gs50: if gs50:
self.acs[icao]["gs50"] = gs50
output_buffer.append([t, icao, "gs50", gs50]) output_buffer.append([t, icao, "gs50", gs50])
elif bds == "BDS60": elif bds == "BDS60":
@@ -252,20 +241,10 @@ class Decode:
output_buffer.append([t, icao, "mach60", mach60]) output_buffer.append([t, icao, "mach60", mach60])
if roc60baro: if roc60baro:
self.acs[icao]["roc60baro"] = roc60baro
output_buffer.append([t, icao, "roc60baro", roc60baro]) output_buffer.append([t, icao, "roc60baro", roc60baro])
if roc60ins: if roc60ins:
self.acs[icao]["roc60ins"] = roc60ins
output_buffer.append([t, icao, "roc60ins", roc60ins]) output_buffer.append([t, icao, "roc60ins", roc60ins])
elif bds == "BDS44":
if(pms.commb.is44(msg)):
self.acs[icao]["hum44"] = pms.commb.hum44(msg)
self.acs[icao]["p44"] = pms.commb.p44(msg)
self.acs[icao]["temp44"] = pms.commb.temp44(msg)
self.acs[icao]["turb44"] = pms.commb.turb44(msg)
self.acs[icao]["wind44"] = pms.commb.wind44(msg)
# clear up old data # clear up old data
for icao in list(self.acs.keys()): for icao in list(self.acs.keys()):
if self.t - self.acs[icao]["live"] > self.cache_timeout: if self.t - self.acs[icao]["live"] > self.cache_timeout:

View File

@@ -9,7 +9,7 @@ import signal
import multiprocessing import multiprocessing
from pyModeS.streamer.decode import Decode from pyModeS.streamer.decode import Decode
from pyModeS.streamer.screen import Screen from pyModeS.streamer.screen import Screen
from pyModeS.streamer.source import NetSource, RtlSdrSource # , RtlSdrSource24 from pyModeS.streamer.source import NetSource, RtlSdrSource, RtlSdrSource24
def main(): def main():
@@ -100,8 +100,8 @@ def main():
source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE) source = NetSource(host=SERVER, port=PORT, rawtype=DATATYPE)
elif SOURCE == "rtlsdr": elif SOURCE == "rtlsdr":
source = RtlSdrSource() source = RtlSdrSource()
# elif SOURCE == "rtlsdr24": elif SOURCE == "rtlsdr24":
# source = RtlSdrSource24() source = RtlSdrSource24()
recv_process = multiprocessing.Process( recv_process = multiprocessing.Process(
target=source.run, args=(raw_pipe_in, stop_flag, exception_queue) target=source.run, args=(raw_pipe_in, stop_flag, exception_queue)

View File

@@ -1,6 +1,7 @@
import pyModeS as pms import pyModeS as pms
from pyModeS.extra.tcpclient import TcpClient from pyModeS.extra.tcpclient import TcpClient
from pyModeS.extra.rtlreader import RtlReader from pyModeS.extra.rtlreader import RtlReader
from pyModeS.extra.demod2400.rtlreader import RtlReader as RtlReader24
class NetSource(TcpClient): class NetSource(TcpClient):
@@ -89,3 +90,47 @@ class RtlSdrSource(RtlReader):
} }
) )
self.reset_local_buffer() 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()

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyModeS" name = "pyModeS"
version = "2.18" version = "2.11"
description = "Python Mode-S and ADS-B Decoder" description = "Python Mode-S and ADS-B Decoder"
authors = ["Junzi Sun <j.sun-1@tudelft.nl>"] authors = ["Junzi Sun <j.sun-1@tudelft.nl>"]
license = "GNU GPL v3" license = "GNU GPL v3"
@@ -13,42 +13,48 @@ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Typing :: Typed", "Typing :: Typed",
] ]
packages = [{ include = "pyModeS", from = "." }] packages = [
{ include = "pyModeS", from = "." },
]
include = [ include = [
"LICENSE", "LICENSE",
"*.pyx", "*.pyx",
"*.pxd", "*.pxd",
"*.pyi", "*.pyi",
"py.typed", "py.typed",
{ path = "pyModeS/**/*.so", format = "wheel" }, { path = "src/pyModeS/**/*.so", format = "wheel" }
{ path = "pyModeS/**/*.pyd", format = "wheel" },
] ]
build = "build.py"
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
[tool.poetry.scripts] [tool.poetry.scripts]
modeslive = "pyModeS.streamer.modeslive:main" modeslive = "pyModeS.streamer.modeslive:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9" python = "^3.8"
numpy = ">=1.26" numpy = "^1.24"
pyzmq = ">=24.0" pyzmq = "^24.0"
pyrtlsdr = { version = ">=0.2.93", optional = true } pyrtlsdr = {version = "^0.2.93", optional = true}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
mypy = ">=0.991" Cython = "^0.29.32"
flake8 = ">=5.0.0" mypy = "^0.991"
black = ">=22.12.0" flake8 = "^5.0.0"
isort = ">=5.11.4" black = "^22.12.0"
pytest = ">=7.2.0" isort = "^5.11.4"
pytest-cov = ">=4.0.0" pytest = "^7.2.0"
codecov = ">=2.1.12" pytest-cov = "^4.0.0"
codecov = "^2.1.12"
ipykernel = "^6.20.0"
[tool.poetry.extras] [tool.poetry.extras]
rtlsdr = ["pyrtlsdr"] rtlsdr = ["pyrtlsdr"]
[tool.black] [tool.black]
line-length = 80 line-length = 80
target_version = ['py39', 'py310', 'py311', 'py312'] target_version = ['py38', 'py39', 'py310', 'py311']
include = '\.pyi?$' include = '\.pyi?$'
[tool.isort] [tool.isort]
@@ -56,5 +62,5 @@ line_length = 80
profile = "black" profile = "black"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0", "Cython>=0.29.32", "setuptools>=69.1.1"] requires = ["poetry-core>=1.0.0", "Cython>=0.29.32"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View File

@@ -1,5 +1,4 @@
from pyModeS import adsb from pyModeS import adsb
from pytest import approx
# === TEST ADS-B package === # === TEST ADS-B package ===
@@ -23,7 +22,7 @@ def test_adsb_position():
1446332400, 1446332400,
1446332405, 1446332405,
) )
assert pos == (approx(49.81755, 0.001), approx(6.08442, 0.001)) assert pos == (49.81755, 6.08442)
def test_adsb_position_swap_odd_even(): def test_adsb_position_swap_odd_even():
@@ -33,32 +32,32 @@ def test_adsb_position_swap_odd_even():
1446332405, 1446332405,
1446332400, 1446332400,
) )
assert pos == (approx(49.81755, 0.001), approx(6.08442, 0.001)) assert pos == (49.81755, 6.08442)
def test_adsb_position_with_ref(): def test_adsb_position_with_ref():
pos = adsb.position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0) pos = adsb.position_with_ref("8D40058B58C901375147EFD09357", 49.0, 6.0)
assert pos == (approx(49.82410, 0.001), approx(6.06785, 0.001)) assert pos == (49.82410, 6.06785)
pos = adsb.position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5) pos = adsb.position_with_ref("8FC8200A3AB8F5F893096B000000", -43.5, 172.5)
assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001)) assert pos == (-43.48564, 172.53942)
def test_adsb_airborne_position_with_ref(): def test_adsb_airborne_position_with_ref():
pos = adsb.airborne_position_with_ref( pos = adsb.airborne_position_with_ref(
"8D40058B58C901375147EFD09357", 49.0, 6.0 "8D40058B58C901375147EFD09357", 49.0, 6.0
) )
assert pos == (approx(49.82410, 0.001), approx(6.06785, 0.001)) assert pos == (49.82410, 6.06785)
pos = adsb.airborne_position_with_ref( pos = adsb.airborne_position_with_ref(
"8D40058B58C904A87F402D3B8C59", 49.0, 6.0 "8D40058B58C904A87F402D3B8C59", 49.0, 6.0
) )
assert pos == (approx(49.81755, 0.001), approx(6.08442, 0.001)) assert pos == (49.81755, 6.08442)
def test_adsb_surface_position_with_ref(): def test_adsb_surface_position_with_ref():
pos = adsb.surface_position_with_ref( pos = adsb.surface_position_with_ref(
"8FC8200A3AB8F5F893096B000000", -43.5, 172.5 "8FC8200A3AB8F5F893096B000000", -43.5, 172.5
) )
assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001)) assert pos == (-43.48564, 172.53942)
def test_adsb_surface_position(): def test_adsb_surface_position():
@@ -70,7 +69,7 @@ def test_adsb_surface_position():
-43.496, -43.496,
172.558, 172.558,
) )
assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001)) assert pos == (-43.48564, 172.53942)
def test_adsb_alt(): def test_adsb_alt():
@@ -81,9 +80,9 @@ def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F") vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F") vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000") vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, approx(182.88, 0.1), -832, "GS") assert vgs == (159, 182.88, -832, "GS")
assert vas == (375, approx(243.98, 0.1), -2304, "TAS") assert vas == (375, 243.98, -2304, "TAS")
assert vgs_surface == (19, approx(42.2, 0.1), 0, "GS") assert vgs_surface == (19, 42.2, 0, "GS")
assert adsb.altitude_diff("8D485020994409940838175B284F") == 550 assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
@@ -97,9 +96,7 @@ def test_adsb_target_state_status():
sel_alt = adsb.selected_altitude("8DA05629EA21485CBF3F8CADAEEB") sel_alt = adsb.selected_altitude("8DA05629EA21485CBF3F8CADAEEB")
assert sel_alt == (16992, "MCP/FCU") assert sel_alt == (16992, "MCP/FCU")
assert adsb.baro_pressure_setting("8DA05629EA21485CBF3F8CADAEEB") == 1012.8 assert adsb.baro_pressure_setting("8DA05629EA21485CBF3F8CADAEEB") == 1012.8
assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB") == approx( assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB") == 66.8
66.8, 0.1
)
assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") is True assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") is True
assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True
assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") is False assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") is False

View File

@@ -1,5 +1,5 @@
from pyModeS import bds, commb from pyModeS import bds, commb
from pytest import approx import pytest
# from pyModeS import ehs, els # deprecated # from pyModeS import ehs, els # deprecated
@@ -23,24 +23,32 @@ def test_bds40_functions():
def test_bds50_functions(): def test_bds50_functions():
msg1 = "A000139381951536E024D4CCF6B5" assert bds.bds50.roll50("A000139381951536E024D4CCF6B5") == 2.1
msg2 = "A0001691FFD263377FFCE02B2BF9" assert bds.bds50.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4
assert bds.bds50.trk50("A000139381951536E024D4CCF6B5") == 114.258
assert bds.bds50.gs50("A000139381951536E024D4CCF6B5") == 438
assert bds.bds50.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
assert bds.bds50.tas50("A000139381951536E024D4CCF6B5") == 424
for module in [bds.bds50, commb]: assert commb.roll50("A000139381951536E024D4CCF6B5") == 2.1
assert module.roll50(msg1) == approx(2.1, 0.01) assert commb.roll50("A0001691FFD263377FFCE02B2BF9") == -0.4 # signed value
assert module.roll50(msg2) == approx(-0.35, 0.01) # signed value assert commb.trk50("A000139381951536E024D4CCF6B5") == 114.258
assert module.trk50(msg1) == approx(114.258, 0.1) assert commb.gs50("A000139381951536E024D4CCF6B5") == 438
assert module.gs50(msg1) == 438 assert commb.rtrk50("A000139381951536E024D4CCF6B5") == 0.125
assert module.rtrk50(msg1) == 0.125 assert commb.tas50("A000139381951536E024D4CCF6B5") == 424
assert module.tas50(msg1) == 424
def test_bds60_functions(): def test_bds60_functions():
msg = "A00004128F39F91A7E27C46ADC21" msg = "A00004128F39F91A7E27C46ADC21"
for module in [bds.bds60, commb]: assert bds.bds60.hdg60(msg) == pytest.approx(42.71484)
assert bds.bds60.hdg60(msg) == approx(42.71484) assert bds.bds60.ias60(msg) == 252
assert bds.bds60.ias60(msg) == 252 assert bds.bds60.mach60(msg) == 0.42
assert bds.bds60.mach60(msg) == 0.42 assert bds.bds60.vr60baro(msg) == -1920
assert bds.bds60.vr60baro(msg) == -1920 assert bds.bds60.vr60ins(msg) == -1920
assert bds.bds60.vr60ins(msg) == -1920
assert commb.hdg60(msg) == pytest.approx(42.71484)
assert commb.ias60(msg) == 252
assert commb.mach60(msg) == 0.42
assert commb.vr60baro(msg) == -1920
assert commb.vr60ins(msg) == -1920