ETSI Layer2 implementation; stage2: decode

Этот коммит содержится в:
Alexander UR6LKW
2025-05-28 21:50:31 +03:00
родитель 4db12d074c
Коммит a3b486071d
3 изменённых файлов: 158 добавлений и 1 удалений
+148
Просмотреть файл
@@ -5,8 +5,14 @@ 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 BaseFactory, IFactoryProduced
class DMRPL2Base(ABC):
@@ -48,3 +54,145 @@ class DMRPL2VoiceBurst(DMRPL2Base):
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
UTF16BF = 0b11
def __init__(
self, data: bytes|None,
format: Format = Format.ISO8) -> None:
super().__init__(data)
self.__format: LCTalkerAlias.Format = format
# format
def get_format(self) -> Format:
if self.flco != 0x04:
return self.__format
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:
if self.flco != 0x04:
return 0
return (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_str(self) -> str:
# TODO: decode different fmts
if self.flco == 0x04:
return self._data[3:9].decode(encoding='ascii')
return self._data[2:9].decode(encoding='ascii')
def __str__(self) -> str:
return (f"LC TA {self.flco} fmt:{self.format.name} "
f"len:{self.len} str:{self.ta_str}")
class LCFactory(BaseFactory):
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}'")
+10
Просмотреть файл
@@ -34,3 +34,13 @@ class DMRPUnknownPacketTypeException(FactoryException):
from the given input data.
"""
pass
class DMRPUnknownLCTypeException(FactoryException):
"""
Exception raised when the lc analyzer factory cannot recognize the
lc type from the given input data.
"""
pass
-1
Просмотреть файл
@@ -58,7 +58,6 @@ class EmbLCAssembler:
self.vseq = p.vseq
# logging.debug(f"assembled emblc's: {self.lcs}")
return burst_n == 3
def decode(self) -> bytes|None: