Files

198 строки
6.0 KiB
Python
Исполняемый файл

from __future__ import annotations
import enum
from abc import ABC, abstractmethod
from bitarray import bitarray
from dmr_utils3.bptc import decode_full_lc
from typing import NoReturn
from .base_fields import DMRPFieldInt
from .enums import CallType
from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPL2BadDataException
from .exceptions import DMRPUnknownLCTypeException
from .factory import AbstractFactory, IFactoryProduced
class DMRPL2Base(ABC):
"""
Base decoder of DMRD packet payload (layer2, 33 bytes)
"""
DataTypes = enum.StrEnum('DataTypes', ['FULL_LC', 'VOICE_BURST'])
def __init__(self, data: bytes) -> None:
self.set_data(data)
def set_data(self, data: bytes) -> None:
if len(data) != 33:
DMRPL2BadDataException("L2 data must be 33 bytes long")
self.bitdata: bitarray = bitarray(data, endian='big')
def get_data(self) -> bytes:
return self.bitdata.tobytes()
@abstractmethod
def get_data_type(self) -> DMRPL2Base.DataTypes: ...
class DMRPL2FullLC(DMRPL2Base):
def get_data_type(self) -> DMRPL2Base.DataTypes:
return DMRPL2Base.DataTypes.FULL_LC
def get_full_lc(self) -> bytes:
return decode_full_lc(self.bitdata[:98] + self.bitdata[-98:]).tobytes()
class DMRPL2VoiceBurst(DMRPL2Base):
def get_data_type(self) -> DMRPL2Base.DataTypes:
return DMRPL2Base.DataTypes.VOICE_BURST
def get_voice_bits(self) -> tuple[bitarray, bitarray]:
return (self.bitdata[:108], self.bitdata[-108:])
def get_emb_lc(self) -> bytes:
return self.bitdata[116:148].tobytes()
class LCBase(IFactoryProduced, ABC):
DATA_SIZE: int = 9
FLCOS: list[int] = []
flco = DMRPFieldInt('flco', 0, 1)
def __init__(self, data: bytes|None) -> None:
if data is None:
self.create()
else:
self.from_data(data)
def create(self, flco: int|None = None):
self._data: bytearray = bytearray(b'\x00' * self.DATA_SIZE)
if flco == None and len(self.FLCOS) > 0:
self.flco = self.FLCOS[0]
def from_data(self, data: bytes) -> None:
if len(data) != self.DATA_SIZE:
raise DMRPL2BadDataException(
f"LC data must be {self.DATA_SIZE} bytes long")
self._data = bytearray(data)
@classmethod
def detect_by_data(cls, data: bytes) -> bool:
return len(data) == cls.DATA_SIZE and data[0] in cls.FLCOS
class LCCall(LCBase):
FLCOS = [0x00, 0x03]
CALL_TYPE_FLCO_MAP = { 0x00: CallType.GROUP, 0x03: CallType.UNIT }
@property
def call_type(self) -> CallType:
flco = self._data[0] if len(self._data) > 0 else 0
if flco not in self.CALL_TYPE_FLCO_MAP:
return CallType.GROUP
return self.CALL_TYPE_FLCO_MAP[flco]
dst_id = DMRPFieldInt('dst_id', 3, 3)
src_id = DMRPFieldInt('src_id', 6, 3)
def __str__(self) -> str:
return (f"LC Call {self.flco} {self.call_type.name} "
f"dst:{self.dst_id} src:{self.src_id}")
class LCLocation(LCBase):
FLCOS = [0x08]
# lat
@property
def lat(self) -> float:
"""Decode 24-bit signed latitude integer to decimal degrees."""
lat_24bit = int.from_bytes(self._data[6:9], byteorder="big")
if lat_24bit & (1 << 23): # Check sign bit
lat_24bit -= (1 << 24) # Convert from two's complement
return lat_24bit * (180.0 / (1 << 24)) # Map to range [-90, +90]
# lon
@property
def lon(self) -> float:
"""Decode 25-bit signed longitude integer to decimal degrees."""
lon_25bit = int.from_bytes(self._data[2:6], byteorder="big")
lon_25bit &= 0x1FFFFFF
if lon_25bit & (1 << 24): # Check sign bit
lon_25bit -= (1 << 25) # Convert from two's complement
return lon_25bit * (360.0 / (1 << 25)) # Map to range [-180, +180]
def __str__(self) -> str:
return f"LC GPS {self.flco} {self.lat} {self.lon}"
class LCTalkerAlias(LCBase):
FLCOS = [0x04, 0x05, 0x06, 0x07]
class Format(enum.Enum):
_7BIT = 0b00
ISO8 = 0b01
UTF8 = 0b10
UTF16BE = 0b11
# format
def get_format(self) -> Format|None:
if self.flco != 0x04:
return None
return LCTalkerAlias.Format((self._data[2] & 0xC0) >> 6)
def set_format(self, format: Format) -> None:
if self.flco != 0x04:
self.__format = format
self._data[2] = (self._data[2] & ~0xC0) | ((format.value << 6) & 0xC0)
format = property(get_format, set_format)
# len
def get_len(self) -> int|None:
if self.flco != 0x04:
return None
return int((self._data[2] & 0x3E) >> 1)
def set_len(self, len: int) -> None:
if self.flco != 0x04:
return
if not (0 <= len < 1<<5):
raise DMRPFieldOutOfRangeException("len", "uint5")
if self._data is not None:
self._data[2] = (self._data[2] & ~0x3E) | ((len << 1) & 0x3E)
len = property(get_len, set_len)
@property
def ta_data(self) -> bytes:
if self.flco == 0x04:
if self.format == LCTalkerAlias.Format._7BIT:
data = self._data[2:9]
data[0] &= 1
return bytes(data)
return bytes(self._data[3:9])
return bytes(self._data[2:9])
def __str__(self) -> str:
if self.flco != 0x04:
return (f"LC TA {self.flco} data:{self.ta_data.hex()}")
return (f"LC TA {self.flco} fmt:{self.format.name} "
f"len:{self.len} data:{self.ta_data.hex()}")
class LCFactory(AbstractFactory[LCBase]):
def __init__(self) -> None:
"""
Initializes the lc analyzer factory with a list of all lc
analyzer classes.
"""
super().__init__([LCCall, LCLocation, LCTalkerAlias])
def not_found(self, data: bytes) -> NoReturn:
lctype = data[0:1].hex() if len(data) > 0 else ''
raise DMRPUnknownLCTypeException(f"Unknown lc type '{lctype}'")