196 строки
5.9 KiB
Python
Исполняемый файл
196 строки
5.9 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:
|
|
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}'")
|