Initial import: WebAisMap
Closes TG-4
This commit is contained in:
+178
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Формирование кадра VDL AIS (флаги HDLC, bit stuffing, CRC-16-CCITT) и NRZI.
|
||||
Опционально: выравнивание payload до октета и добор NRZ до N бит перед NRZI (аналог padd_frame в GNU Radio).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from bitarray import bitarray
|
||||
|
||||
FLAG = bitarray("01111110")
|
||||
|
||||
|
||||
def _bits_to_bytes_msb(bits: bitarray) -> bytes:
|
||||
out = bytearray()
|
||||
for i in range(0, len(bits), 8):
|
||||
chunk = bits[i : i + 8]
|
||||
if len(chunk) < 8:
|
||||
pad = bitarray(8 - len(chunk))
|
||||
pad.setall(0)
|
||||
chunk = chunk + pad
|
||||
v = 0
|
||||
for b in chunk:
|
||||
v = (v << 1) | int(b)
|
||||
out.append(v)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def crc16_ccitt_fcs(data_bits: bitarray) -> int:
|
||||
"""FCS по данным payload (без FCS), длина кратна 8 бит."""
|
||||
b = _bits_to_bytes_msb(data_bits)
|
||||
crc = 0xFFFF
|
||||
poly = 0x1021
|
||||
for byte in b:
|
||||
crc ^= byte << 8
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = ((crc << 1) ^ poly) & 0xFFFF
|
||||
else:
|
||||
crc = (crc << 1) & 0xFFFF
|
||||
return crc ^ 0xFFFF
|
||||
|
||||
|
||||
def _int_to_bits(val: int, n: int) -> bitarray:
|
||||
out = bitarray()
|
||||
for i in range(n - 1, -1, -1):
|
||||
out.append((val >> i) & 1)
|
||||
return out
|
||||
|
||||
|
||||
def bit_stuff(bits: bitarray) -> bitarray:
|
||||
out = bitarray()
|
||||
count = 0
|
||||
for b in bits:
|
||||
out.append(b)
|
||||
if b:
|
||||
count += 1
|
||||
if count == 5:
|
||||
out.append(0)
|
||||
count = 0
|
||||
else:
|
||||
count = 0
|
||||
return out
|
||||
|
||||
|
||||
def _pad_payload_to_octet_boundary(payload_bits: bitarray) -> bitarray:
|
||||
r = len(payload_bits) % 8
|
||||
if r == 0:
|
||||
return payload_bits
|
||||
z = bitarray(8 - r)
|
||||
z.setall(0)
|
||||
return payload_bits + z
|
||||
|
||||
|
||||
def _pad_nrz_to_min_bits(nrz_bits: bitarray, min_len: int) -> bitarray:
|
||||
if min_len <= 0 or len(nrz_bits) >= min_len:
|
||||
return nrz_bits
|
||||
tail = bitarray(min_len - len(nrz_bits))
|
||||
tail.setall(0)
|
||||
return nrz_bits + tail
|
||||
|
||||
|
||||
def build_hdlc_frame(
|
||||
payload_bits: bitarray, *, pad_payload_to_octet: bool = False
|
||||
) -> bitarray:
|
||||
"""payload_bits — двоичное тело AIS (как pyais Payload.to_bitarray())."""
|
||||
pl = payload_bits
|
||||
if len(pl) % 8 != 0:
|
||||
if pad_payload_to_octet:
|
||||
pl = _pad_payload_to_octet_boundary(pl)
|
||||
else:
|
||||
raise ValueError("AIS payload length must be multiple of 8 bits for this CRC path")
|
||||
fcs = crc16_ccitt_fcs(pl)
|
||||
fcs_bits = _int_to_bits(fcs, 16)
|
||||
data = pl + fcs_bits
|
||||
stuffed = bit_stuff(data)
|
||||
return FLAG + stuffed + FLAG
|
||||
|
||||
|
||||
def nrzi_encode(bits: bitarray) -> bitarray:
|
||||
"""NRZI: 1 — уровень без изменения, 0 — переключение."""
|
||||
level = 0
|
||||
out = bitarray()
|
||||
for b in bits:
|
||||
if not int(b):
|
||||
level = 1 - level
|
||||
out.append(level)
|
||||
return out
|
||||
|
||||
|
||||
def preamble_alternating(num_bits: int = 24) -> bitarray:
|
||||
"""
|
||||
ITU-style dotting: чередование 1/0 (101010…), как gr-aistx LEN_PREAMBLE и phy._preamble_bits.
|
||||
Вариант 010101… дал бы после NRZI (packed, старт уровня 0) байты 0xCC… вместо 0x66… — та же частота,
|
||||
другая фаза; к стаффингу это не относится.
|
||||
"""
|
||||
out = bitarray()
|
||||
for i in range(num_bits):
|
||||
out.append(1 - (i & 1))
|
||||
return out
|
||||
|
||||
|
||||
def hdlc_nrzi_bytes_no_preamble(
|
||||
payload_bits: bitarray,
|
||||
*,
|
||||
nrzi_byte_mode: str = "packed",
|
||||
pad_payload_to_octet: bool = False,
|
||||
pad_nrz_total_bits: Optional[int] = None,
|
||||
) -> bytes:
|
||||
"""Только HDLC+NRZI без преамбулы. pad_nrz_total_bits — добор нулей в NRZ до длины (как GNU Radio padd_frame)."""
|
||||
frame = build_hdlc_frame(payload_bits, pad_payload_to_octet=pad_payload_to_octet)
|
||||
frame = _pad_nrz_to_min_bits(frame, pad_nrz_total_bits or 0)
|
||||
nrz = nrzi_encode(frame)
|
||||
if nrzi_byte_mode == "expanded":
|
||||
return bytes(0xFF if int(b) else 0x00 for b in nrz)
|
||||
return _bits_to_bytes_msb(nrz)
|
||||
|
||||
|
||||
def phy_frame_bit_counts(
|
||||
payload_bits: bitarray, *, pad_payload_to_octet: bool = False
|
||||
) -> dict:
|
||||
"""Длины для отладки: payload, поле между флагами (со стаффингом), полный HDLC."""
|
||||
pl_in = len(payload_bits)
|
||||
pl = payload_bits
|
||||
if len(pl) % 8 != 0 and pad_payload_to_octet:
|
||||
pl = _pad_payload_to_octet_boundary(pl)
|
||||
frame = build_hdlc_frame(pl, pad_payload_to_octet=pad_payload_to_octet)
|
||||
inner = frame[8:-8]
|
||||
return {
|
||||
"payload_bits_input": pl_in,
|
||||
"payload_bits_after_pad": len(pl),
|
||||
"bits_between_flags_stuffed": len(inner),
|
||||
"hdlc_frame_bits": len(frame),
|
||||
}
|
||||
|
||||
|
||||
def ais_channel_to_nrzi_bytes(
|
||||
payload_bits: bitarray,
|
||||
*,
|
||||
preamble_bits: int = 24,
|
||||
nrzi_byte_mode: str = "packed",
|
||||
pad_payload_to_octet: bool = False,
|
||||
pad_nrz_total_bits: Optional[int] = None,
|
||||
) -> bytes:
|
||||
"""
|
||||
nrzi_byte_mode:
|
||||
'packed' — 8 NRZI-сэмплов в байт (MSB первый);
|
||||
'expanded' — один байт на NRZI-бит: 0x00 / 0xFF.
|
||||
pad_nrz_total_bits — после преамбулы+кадра дописать нули в NRZ до этой длины (типично 200 для GNU Radio).
|
||||
"""
|
||||
frame = build_hdlc_frame(payload_bits, pad_payload_to_octet=pad_payload_to_octet)
|
||||
pre = preamble_alternating(preamble_bits) if preamble_bits > 0 else bitarray()
|
||||
combined = pre + frame
|
||||
combined = _pad_nrz_to_min_bits(combined, pad_nrz_total_bits or 0)
|
||||
nrzi = nrzi_encode(combined)
|
||||
if nrzi_byte_mode == "expanded":
|
||||
return bytes(0xFF if int(b) else 0x00 for b in nrzi)
|
||||
return _bits_to_bytes_msb(nrzi)
|
||||
Reference in New Issue
Block a user