closed TG-1; git was inited;

This commit is contained in:
2026-05-04 08:13:38 +03:00
commit bcf20fcb04
105 changed files with 6592 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
"""Binary decoder tests for AIS-catcher Mini UDP telemetry.
The expected byte layouts come from ais-mini's UDP comment block; any
layout drift (field order, endianness, sizes) must fail here before the
ingest runs on real hardware.
"""
from __future__ import annotations
import struct
import pytest
from ais_hub.parser.aiscatcher import (
decode_rssi_iq,
decode_signal_events,
decode_slot_bitmap,
decode_slot_detail,
)
# ---------------------------------------------------------------------------
# Slot bitmap (315 bytes)
# ---------------------------------------------------------------------------
def _build_bitmap(
*,
channel: bytes = b"A",
utc_minute: int = 28_279_310,
slots_total: int = 2250,
occupied: int = 437,
noise: float = 0.0,
threshold: float = 0.0,
slot0_ms: int = 28_279_310 * 60_000,
first_occ_ms: int = 28_279_310 * 60_000 + 123,
bitmap: bytes | None = None,
) -> bytes:
if bitmap is None:
bitmap = bytes(282)
assert len(channel) == 1
assert len(bitmap) == 282
head = struct.pack(
">BIHHffQQ",
channel[0], utc_minute, slots_total, occupied,
noise, threshold, slot0_ms, first_occ_ms,
)
assert len(head) == 33
return head + bitmap
def test_bitmap_decodes_roundtrip():
bm = bytes(range(256)) + bytes(282 - 256)
data = _build_bitmap(channel=b"B", occupied=1234, bitmap=bm)
snap = decode_slot_bitmap(data)
assert snap is not None
assert snap.channel == "B"
assert snap.utc_minute == 28_279_310
assert snap.slots_total == 2250
assert snap.occupied_count == 1234
assert snap.slot0_unix_ms == 28_279_310 * 60_000
assert snap.first_occupied_unix_ms == 28_279_310 * 60_000 + 123
assert snap.bitmap == bm
# occupied_fraction is derived
assert snap.occupied_fraction() == pytest.approx(1234 / 2250)
def test_bitmap_rejects_wrong_size():
data = _build_bitmap()
assert decode_slot_bitmap(data[:-1]) is None
assert decode_slot_bitmap(data + b"\x00") is None
def test_bitmap_unknown_channel_byte_marked_questionmark():
data = _build_bitmap(channel=b"X")
snap = decode_slot_bitmap(data)
assert snap is not None
assert snap.channel == "?"
# ---------------------------------------------------------------------------
# Slot detail
# ---------------------------------------------------------------------------
def test_slot_detail_roundtrip():
entries = [(0, -85.5), (100, -72.125), (2249, -99.0)]
head = struct.pack(">BIQH", ord(b"A"), 28_279_310, 28_279_310 * 60_000, len(entries))
body = b"".join(struct.pack(">Hf", slot, lvl) for slot, lvl in entries)
detail = decode_slot_detail(head + body)
assert detail is not None
assert detail.channel == "A"
assert detail.utc_minute == 28_279_310
assert detail.slot0_unix_ms == 28_279_310 * 60_000
assert [(e.slot, round(e.level_db, 4)) for e in detail.entries] == [
(0, -85.5),
(100, -72.125),
(2249, -99.0),
]
def test_slot_detail_rejects_short_body():
head = struct.pack(">BIQH", ord(b"A"), 1, 2, 3)
# only 1 full entry provided where 3 claimed
body = struct.pack(">Hf", 10, -70.0)
assert decode_slot_detail(head + body) is None
def test_slot_detail_empty_is_valid():
head = struct.pack(">BIQH", ord(b"B"), 1, 2, 0)
detail = decode_slot_detail(head)
assert detail is not None
assert detail.entries == []
# ---------------------------------------------------------------------------
# RSSI IQ (8 bytes)
# ---------------------------------------------------------------------------
def test_rssi_decodes_pair():
data = struct.pack(">ff", -42.25, -55.75)
rssi = decode_rssi_iq(data)
assert rssi is not None
assert rssi.power_a_db == pytest.approx(-42.25)
assert rssi.power_b_db == pytest.approx(-55.75)
def test_rssi_wrong_size():
assert decode_rssi_iq(b"\x00" * 7) is None
assert decode_rssi_iq(b"\x00" * 9) is None
# ---------------------------------------------------------------------------
# Signal events (decode markers)
# ---------------------------------------------------------------------------
def test_signal_events_roundtrip():
events = [
(1_700_000_000_000, 42, 257_123_456, -72.0),
(1_700_000_000_100, 1500, 227_000_001, -88.5),
]
head = struct.pack(">BH", ord(b"A"), len(events))
body = b"".join(struct.pack(">QHIf", ts, slot, mmsi, lvl)
for ts, slot, mmsi, lvl in events)
batch = decode_signal_events(head + body)
assert batch is not None
assert batch.channel == "A"
assert len(batch.events) == 2
assert batch.events[0].unix_ms == 1_700_000_000_000
assert batch.events[0].slot == 42
assert batch.events[0].mmsi == 257_123_456
assert batch.events[0].level_db == pytest.approx(-72.0)
assert batch.events[1].mmsi == 227_000_001
def test_signal_events_empty():
data = struct.pack(">BH", ord(b"A"), 0)
batch = decode_signal_events(data)
assert batch is not None
assert batch.events == []
def test_signal_events_rejects_short_body():
head = struct.pack(">BH", ord(b"A"), 5)
assert decode_signal_events(head + b"\x00" * 10) is None