Compare commits

..

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:
env:
POETRY_VERSION: "1.6.1"
POETRY_VERSION: "1.3.1"
jobs:
deploy:
@ -14,27 +14,32 @@ jobs:
strategy:
matrix:
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:
PYTHON_VERSION: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v2
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@v4
if: ${{ !startsWith(runner.os, 'windows') }}
uses: actions/cache@v3
if: matrix.os != 'windows-latest'
with:
path: |
~/.local
.venv
path: ~/.local
key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }}
- 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
- name: Install and configure Poetry
uses: snok/install-poetry@v1.4.0
uses: snok/install-poetry@v1.3.3
with:
version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true
virtualenvs-in-project: true
- name: Display Python version
run: poetry run python -c "import sys; print(sys.version)"
- name: Install dependencies
run: |
poetry install
- name: Type checking
if: ${{ env.PYTHON_VERSION != '3.7' }}
run: |
poetry run mypy pyModeS
@ -65,6 +68,7 @@ jobs:
poetry run pytest tests --cov --cov-report term-missing
- 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:
env_vars: PYTHON_VERSION

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

@ -1,18 +1,26 @@
import os
import shutil
import sys
from distutils.core import Distribution, Extension
from distutils.command import build_ext
# import pip
from Cython.Build import cythonize
from setuptools import Distribution, Extension
from setuptools.command import build_ext
def build() -> None:
compile_args = []
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 = [
Extension(
@ -29,16 +37,16 @@ def build() -> None:
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"],
# ),
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(
@ -46,7 +54,9 @@ def build() -> None:
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
cmd = build_ext.build_ext(distribution)
@ -55,7 +65,7 @@ def build() -> None:
cmd.run()
# 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)
shutil.copyfile(output, relative_extension)
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:
# altitude unknown or invalid
alt = -999999
alt = -9999
elif Mbit == 48: # unit in ft, "0" -> 48
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")
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("Mach number", commb.mach60(msg))
_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 datetime import datetime
from .. import common
@ -162,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
@ -424,7 +425,7 @@ def nic_v1(msg: str, NICs: int) -> tuple[int, None | float, None | float]:
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
Args:
@ -455,9 +456,10 @@ def nic_v2(msg: str, NICa: int, NICbc: int) -> tuple[int | None, int | None]:
try:
if isinstance(NIC, dict):
NIC = NIC[NICs]
Rc = uncertainty.NICv2[NIC][NICs]["Rc"]
except KeyError:
return None, None
Rc = uncertainty.NA
return NIC, Rc # type: ignore

View File

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

View File

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

View File

@ -19,6 +19,7 @@ def surface_position(
lat_ref: float,
lon_ref: float,
) -> None | tuple[float, float]:
"""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.
@ -91,7 +92,7 @@ def surface_position(
imin = min(range(4), key=dls.__getitem__)
lon = lons[imin]
return lat, lon
return round(lat, 5), round(lon, 5)
def surface_position_with_ref(
@ -138,12 +139,12 @@ def surface_position_with_ref(
lon = d_lon * (m + cprlon)
return lat, lon
return round(lat, 5), round(lon, 5)
def surface_velocity(
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
Args:
@ -172,6 +173,7 @@ def surface_velocity(
trk_status = int(mb[12])
if trk_status == 1:
trk = common.bin2int(mb[13:20]) * 360 / 128
trk = round(trk, 1)
else:
trk = None

View File

@ -48,6 +48,7 @@ def airborne_velocity(
spd: None | float
if subtype in (1, 2):
v_ew = common.bin2int(mb[14:24])
v_ns = common.bin2int(mb[25:35])
@ -76,7 +77,7 @@ def airborne_velocity(
trk = math.degrees(trk) # convert to degrees
trk = trk if trk >= 0 else trk + 360 # no negative val
trk_or_hdg = trk
trk_or_hdg = round(trk, 2)
spd_type = "GS"
dir_type = "TRUE_NORTH"
@ -86,6 +87,7 @@ def airborne_velocity(
hdg = None
else:
hdg = common.bin2int(mb[14:24]) / 1024 * 360.0
hdg = round(hdg, 2)
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
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]:
@ -96,8 +96,10 @@ def temp44(msg: str) -> Tuple[float, float]:
value = value - 1024
temp = value * 0.25 # celsius
temp = round(temp, 2)
temp_alternative = value * 0.125 # celsius
temp_alternative = round(temp_alternative, 3)
return temp, temp_alternative
@ -138,7 +140,7 @@ def hum44(msg: str) -> Optional[float]:
hm = common.bin2int(d[50:56]) * 100 / 64 # %
return hm
return round(hm, 1)
def turb44(msg: str) -> Optional[int]:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -131,7 +131,7 @@ def lockout(msg):
if uf(msg) in {4, 5, 20, 21}:
lockout = False
di = mbytes[1] & 0x7
if (di == 1 or di == 7):
if di == 7:
# LOS
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
@ -187,16 +187,10 @@ def uplink_fields(msg):
if RR > 15:
BDS1 = RR - 16
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 = (mbytes[2] >> 4) & 0xF
IC = "II" + str(II)
if ((mbytes[3] & 0x40) >> 6) == 1:
lockout = True
elif di == 7:
# LOS
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 traceback
import numpy as np
import pyModeS as pms
from typing import Any
import_msg = """
---------------------------------------------------------------------
Warning: pyrtlsdr not installed (required for using RTL-SDR devices)!
---------------------------------------------------------------------"""
try:
import rtlsdr # type: ignore
except ImportError:
print(import_msg)
except:
print("------------------------------------------------------------------------")
print("! Warning: pyrtlsdr not installed (required for using RTL-SDR devices) !")
print("------------------------------------------------------------------------")
sampling_rate = 2e6
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):
def __init__(self, **kwargs) -> None:
def __init__(self, **kwargs):
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.sample_rate = sampling_rate
self.sdr.center_freq = modes_frequency
@ -47,7 +39,7 @@ class RtlReader(object):
self.exception_queue = None
def _calc_noise(self) -> float:
def _calc_noise(self):
"""Calculate noise floor"""
window = smaples_per_microsec * 100
total_len = len(self.signal_buffer)
@ -58,7 +50,7 @@ class RtlReader(object):
)
return min(means)
def _process_buffer(self) -> list[list[Any]]:
def _process_buffer(self):
"""process raw IQ data in the buffer"""
# update noise floor
@ -78,18 +70,17 @@ class RtlReader(object):
i += 1
continue
if self._check_preamble(self.signal_buffer[i : i + pbits * 2]):
frame_start = i + pbits * 2
if self._check_preamble(self.signal_buffer[i:frame_start]):
frame_end = i + pbits * 2 + (fbits + 1) * 2
frame_length = (fbits + 1) * 2
frame_end = frame_start + frame_length
frame_pulses = self.signal_buffer[frame_start:frame_end]
threshold = max(frame_pulses) * 0.2
msgbin: list[int] = []
msgbin = []
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:
break
@ -126,7 +117,7 @@ class RtlReader(object):
return messages
def _check_preamble(self, pulses) -> bool:
def _check_preamble(self, pulses):
if len(pulses) != 16:
return False
@ -136,7 +127,7 @@ class RtlReader(object):
return True
def _check_msg(self, msg) -> bool:
def _check_msg(self, msg):
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
@ -146,9 +137,8 @@ class RtlReader(object):
return True
elif df in [4, 5, 11] and msglen == 14:
return True
return False
def _debug_msg(self, msg) -> None:
def _debug_msg(self, msg):
df = pms.df(msg)
msglen = len(msg)
if df == 17 and msglen == 28:
@ -161,7 +151,7 @@ class RtlReader(object):
# print("[*]", msg)
pass
def _read_callback(self, data, rtlsdr_obj) -> None:
def _read_callback(self, data, rtlsdr_obj):
amp = np.absolute(data)
self.signal_buffer.extend(amp.tolist())
@ -169,18 +159,16 @@ class RtlReader(object):
messages = self._process_buffer()
self.handle_messages(messages)
def handle_messages(self, messages) -> None:
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) -> None:
def stop(self, *args, **kwargs):
self.sdr.close()
def run(
self, raw_pipe_in=None, stop_flag=None, exception_queue=None
) -> None:
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

View File

@ -6,7 +6,6 @@ import time
import pyModeS as pms
import traceback
import zmq
import math
class TcpClient(object):
@ -150,103 +149,6 @@ class TcpClient(object):
messages.append([msg, ts])
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):
"""Skysense stream format.

View File

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

View File

@ -73,15 +73,8 @@ class Decode:
"VFOMr": None,
"PE_RCu": 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]["live"] = int(t)
@ -162,7 +155,7 @@ class Decode:
ac["nic_bc"] = pms.adsb.nic_b(msg)
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()):
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_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:
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]:
ac["NACv"], ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg)
ac["HFOMr"], ac["VFOMr"] = pms.adsb.nac_v(msg)
if tc == 29:
ac["PE_RCu"], ac["PE_VPL"], ac["base"] = pms.adsb.sil(msg, ac["ver"])
ac["NACp"], ac["HEPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
ac["EPU"], ac["VEPU"] = pms.adsb.nac_p(msg)
if tc == 31:
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(
msg, ac["ver"]
)
@ -201,8 +194,6 @@ class Decode:
if icao not in self.acs:
continue
self.acs[icao]["icao"] = icao
self.acs[icao]["t"] = t
self.acs[icao]["live"] = int(t)
bds = pms.bds.infer(msg)
@ -226,10 +217,8 @@ class Decode:
output_buffer.append([t, icao, "rtrk50", rtrk50])
if trk50:
self.acs[icao]["trk50"] = trk50
output_buffer.append([t, icao, "trk50", trk50])
if gs50:
self.acs[icao]["gs50"] = gs50
output_buffer.append([t, icao, "gs50", gs50])
elif bds == "BDS60":
@ -252,20 +241,10 @@ class Decode:
output_buffer.append([t, icao, "mach60", mach60])
if roc60baro:
self.acs[icao]["roc60baro"] = roc60baro
output_buffer.append([t, icao, "roc60baro", roc60baro])
if roc60ins:
self.acs[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
for icao in list(self.acs.keys()):
if self.t - self.acs[icao]["live"] > self.cache_timeout:

View File

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

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()

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "pyModeS"
version = "2.18"
version = "2.11"
description = "Python Mode-S and ADS-B Decoder"
authors = ["Junzi Sun <j.sun-1@tudelft.nl>"]
license = "GNU GPL v3"
@ -13,42 +13,48 @@ classifiers = [
"Programming Language :: Python :: 3",
"Typing :: Typed",
]
packages = [{ include = "pyModeS", from = "." }]
packages = [
{ include = "pyModeS", from = "." },
]
include = [
"LICENSE",
"*.pyx",
"*.pxd",
"*.pyi",
"py.typed",
{ path = "pyModeS/**/*.so", format = "wheel" },
{ path = "pyModeS/**/*.pyd", format = "wheel" },
{ path = "src/pyModeS/**/*.so", format = "wheel" }
]
build = "build.py"
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
[tool.poetry.scripts]
modeslive = "pyModeS.streamer.modeslive:main"
[tool.poetry.dependencies]
python = ">=3.9"
numpy = ">=1.26"
pyzmq = ">=24.0"
pyrtlsdr = { version = ">=0.2.93", optional = true }
python = "^3.8"
numpy = "^1.24"
pyzmq = "^24.0"
pyrtlsdr = {version = "^0.2.93", optional = true}
[tool.poetry.group.dev.dependencies]
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"
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 = ['py39', 'py310', 'py311', 'py312']
target_version = ['py38', 'py39', 'py310', 'py311']
include = '\.pyi?$'
[tool.isort]
@ -56,5 +62,5 @@ line_length = 80
profile = "black"
[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"

View File

@ -1,5 +1,4 @@
from pyModeS import adsb
from pytest import approx
# === TEST ADS-B package ===
@ -23,7 +22,7 @@ def test_adsb_position():
1446332400,
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():
@ -33,32 +32,32 @@ def test_adsb_position_swap_odd_even():
1446332405,
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():
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)
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():
pos = adsb.airborne_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.airborne_position_with_ref(
"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():
pos = adsb.surface_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_surface_position():
@ -70,7 +69,7 @@ def test_adsb_surface_position():
-43.496,
172.558,
)
assert pos == (approx(-43.48564, 0.001), approx(172.53942, 0.001))
assert pos == (-43.48564, 172.53942)
def test_adsb_alt():
@ -81,9 +80,9 @@ def test_adsb_velocity():
vgs = adsb.velocity("8D485020994409940838175B284F")
vas = adsb.velocity("8DA05F219B06B6AF189400CBC33F")
vgs_surface = adsb.velocity("8FC8200A3AB8F5F893096B000000")
assert vgs == (159, approx(182.88, 0.1), -832, "GS")
assert vas == (375, approx(243.98, 0.1), -2304, "TAS")
assert vgs_surface == (19, approx(42.2, 0.1), 0, "GS")
assert vgs == (159, 182.88, -832, "GS")
assert vas == (375, 243.98, -2304, "TAS")
assert vgs_surface == (19, 42.2, 0, "GS")
assert adsb.altitude_diff("8D485020994409940838175B284F") == 550
@ -97,9 +96,7 @@ def test_adsb_target_state_status():
sel_alt = adsb.selected_altitude("8DA05629EA21485CBF3F8CADAEEB")
assert sel_alt == (16992, "MCP/FCU")
assert adsb.baro_pressure_setting("8DA05629EA21485CBF3F8CADAEEB") == 1012.8
assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB") == approx(
66.8, 0.1
)
assert adsb.selected_heading("8DA05629EA21485CBF3F8CADAEEB") == 66.8
assert adsb.autopilot("8DA05629EA21485CBF3F8CADAEEB") is True
assert adsb.vnav_mode("8DA05629EA21485CBF3F8CADAEEB") is True
assert adsb.altitude_hold_mode("8DA05629EA21485CBF3F8CADAEEB") is False

View File

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