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
30 changed files with 1439 additions and 393 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,67 +1,29 @@
# This workflows will upload a Python Package using Twine when a release is created # 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 # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: publish name: PyPI Publish
on: on:
release: release:
types: [created] types: [created]
workflow_dispatch:
env:
POETRY_VERSION: "1.3.1"
jobs: jobs:
deploy: deploy:
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: "3.x"
- name: Add poetry to windows path
if: "startsWith(runner.os, 'windows')"
run: |
echo "C:\Users\runneradmin\.local\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install and configure Poetry
uses: snok/install-poetry@v1.3.3
with:
version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true
virtualenvs-in-project: true
- name: Display Python version
run: poetry run python -c "import sys; print(sys.version)"
- name: Build packages
run: poetry build
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry run pip install --upgrade pip python -m pip install --upgrade pip
poetry run pip install twine pip install setuptools wheel twine
- name: Build and publish - name: Build and publish
env: env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: | run: |
poetry run twine upload dist/*.whl python setup.py sdist bdist_wheel
twine upload dist/*
- name: Build and publish (source)
if: ${{ startsWith(runner.os, 'windows') && matrix.python-version == '3.11' }}
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
poetry run twine upload dist/*.tar.gz

View File

@@ -20,21 +20,26 @@ jobs:
PYTHON_VERSION: ${{ matrix.python-version }} PYTHON_VERSION: ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 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@v3 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
@@ -49,14 +54,12 @@ jobs:
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@v3 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,10 +1,9 @@
import os import os
import shutil import shutil
import sys import sys
from distutils.command import build_ext
from distutils.core import Distribution, Extension from distutils.core import Distribution, Extension
from distutils.command import build_ext
# import pip
from Cython.Build import cythonize from Cython.Build import cythonize
@@ -12,7 +11,16 @@ 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(

1015
poetry.lock generated

File diff suppressed because it is too large Load Diff

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:

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,12 +130,12 @@ 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,

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

@@ -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

@@ -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.16" 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"
@@ -22,7 +22,7 @@ include = [
"*.pxd", "*.pxd",
"*.pyi", "*.pyi",
"py.typed", "py.typed",
{ path = "pyModeS/**/*.so", format = "wheel" } { path = "src/pyModeS/**/*.so", format = "wheel" }
] ]
[tool.poetry.build] [tool.poetry.build]
@@ -39,6 +39,7 @@ 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]
Cython = "^0.29.32"
mypy = "^0.991" mypy = "^0.991"
flake8 = "^5.0.0" flake8 = "^5.0.0"
black = "^22.12.0" black = "^22.12.0"
@@ -46,6 +47,7 @@ isort = "^5.11.4"
pytest = "^7.2.0" pytest = "^7.2.0"
pytest-cov = "^4.0.0" pytest-cov = "^4.0.0"
codecov = "^2.1.12" codecov = "^2.1.12"
ipykernel = "^6.20.0"
[tool.poetry.extras] [tool.poetry.extras]
rtlsdr = ["pyrtlsdr"] rtlsdr = ["pyrtlsdr"]

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