Этот коммит содержится в:
Alexander UR6LKW
2025-05-31 19:11:26 +03:00
родитель a3b486071d
Коммит 07aa4efb2d
16 изменённых файлов: 280 добавлений и 155 удалений
+3 -2
Просмотреть файл
@@ -1,4 +1,5 @@
from .mmdvm_l1 import *
from .enums import *
from .etsi_l2 import *
from .util import *
from .exceptions import *
from .lc_util import *
from .mmdvm_l1 import *
+2 -4
Просмотреть файл
@@ -19,12 +19,10 @@ class DMRPFieldBase(ABC):
obj._data[self.offset:self.eoffset] = value
@abstractmethod
def __get__(self, obj, cls = None) -> Any:
pass
def __get__(self, obj, cls = None) -> Any: ...
@abstractmethod
def __set__(self, obj, value: Any) -> None:
pass
def __set__(self, obj, value: Any) -> None: ...
class DMRPFieldBytes(DMRPFieldBase):
+22 -25
Просмотреть файл
@@ -12,20 +12,21 @@ from .enums import CallType
from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPL2BadDataException
from .exceptions import DMRPUnknownLCTypeException
from .factory import BaseFactory, IFactoryProduced
from .factory import AbstractFactory, IFactoryProduced
class DMRPL2Base(ABC):
DataTypes = enum.StrEnum('DataTypes', [
'UNKNOWN', 'FULL_LC', 'VOICE_BURST'
])
"""
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("Data must be 33 bytes long")
DMRPL2BadDataException("L2 data must be 33 bytes long")
self.bitdata: bitarray = bitarray(data, endian='big')
@@ -33,8 +34,7 @@ class DMRPL2Base(ABC):
return self.bitdata.tobytes()
@abstractmethod
def get_data_type(self) -> DMRPL2Base.DataTypes:
pass
def get_data_type(self) -> DMRPL2Base.DataTypes: ...
class DMRPL2FullLC(DMRPL2Base):
@@ -136,18 +136,12 @@ class LCTalkerAlias(LCBase):
_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
UTF16BE = 0b11
# format
def get_format(self) -> Format:
def get_format(self) -> Format|None:
if self.flco != 0x04:
return self.__format
return None
return LCTalkerAlias.Format((self._data[2] & 0xC0) >> 6)
def set_format(self, format: Format) -> None:
@@ -158,10 +152,10 @@ class LCTalkerAlias(LCBase):
format = property(get_format, set_format)
# len
def get_len(self) -> int:
def get_len(self) -> int|None:
if self.flco != 0x04:
return 0
return (self._data[2] & 0x3E) >> 1
return None
return int((self._data[2] & 0x3E) >> 1)
def set_len(self, len: int) -> None:
if self.flco != 0x04:
@@ -174,18 +168,21 @@ class LCTalkerAlias(LCBase):
len = property(get_len, set_len)
@property
def ta_str(self) -> str:
# TODO: decode different fmts
def ta_data(self) -> bytes:
if self.flco == 0x04:
return self._data[3:9].decode(encoding='ascii')
return self._data[2:9].decode(encoding='ascii')
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} str:{self.ta_str}")
f"len:{self.len} data:{self.ta_data.hex()}")
class LCFactory(BaseFactory):
class LCFactory(AbstractFactory[LCBase]):
def __init__(self) -> None:
"""
Initializes the lc analyzer factory with a list of all lc
+33 -6
Просмотреть файл
@@ -1,3 +1,6 @@
from abc import ABC, abstractmethod
class DMRPFieldOutOfRangeException(Exception):
"""
Exception raised when a packet field value is out of the allowed or expected range.
@@ -14,18 +17,17 @@ class DMRPBadPacketException(Exception):
"""
Exception raised when a packet is structurally invalid or corrupted
"""
pass
...
class DMRPL2BadDataException(Exception):
"""
Exception raised when L2 data is invalid or corrupted.
"""
pass
...
class FactoryException(Exception):
pass
...
class DMRPUnknownPacketTypeException(FactoryException):
@@ -33,7 +35,7 @@ class DMRPUnknownPacketTypeException(FactoryException):
Exception raised when the packet factory cannot recognize the packet type
from the given input data.
"""
pass
...
class DMRPUnknownLCTypeException(FactoryException):
@@ -41,6 +43,31 @@ class DMRPUnknownLCTypeException(FactoryException):
Exception raised when the lc analyzer factory cannot recognize the
lc type from the given input data.
"""
pass
...
class CustomMessageException(Exception, ABC):
@classmethod
@abstractmethod
def create_message(cls, msg: str) -> str: ...
def __init__(self, msg: str):
super().__init__(self.__class__.create_message(msg))
class EmbLCAssemblerException(CustomMessageException):
"""
Exception raised on EmbLCAssembler error
"""
@classmethod
def create_message(cls, msg: str) -> str:
return f"Embedded LC failed: {msg}"
class CallLCDecoderException(CustomMessageException):
"""
Exception raised on CallLCDecoder error
"""
@classmethod
def create_message(cls, msg: str) -> str:
return f"In-call LC decoder failed: {msg}"
+20 -16
Просмотреть файл
@@ -1,44 +1,48 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Self, Type, NoReturn
from typing import TypeVar, Generic, Type, ClassVar, NoReturn, Any
from .exceptions import FactoryException
class IFactoryProduced(ABC):
@abstractmethod
def __init__(self, data: bytes) -> None:
pass
def __init__(self, data: bytes) -> None: ...
@classmethod
@abstractmethod
def detect_by_data(cls, data: bytes) -> bool:
pass
def detect_by_data(cls, data: bytes) -> bool: ...
class BaseFactory(ABC):
__instance: Self|None = None
TProduced = TypeVar("TProduced", bound=IFactoryProduced, covariant=True)
class AbstractFactory(Generic[TProduced], ABC):
_instance: ClassVar[AbstractFactory[Any]|None] = None
@classmethod
def fd(cls, data: bytes) -> IFactoryProduced:
def fd(cls: Type[AbstractFactory[TProduced]],
data: bytes) -> TProduced:
"""
Short singleton version of from_data method
"""
if cls.__instance is None:
cls.__instance = cls()
return cls.__instance.from_data(data)
if cls._instance is None:
cls._instance = cls()
return cls._instance.from_data(data)
def __init__(self,
classes: list[Type[IFactoryProduced]]|None = None) -> None:
self._classes: list[Type[IFactoryProduced]] = (
classes: list[Type[TProduced]]|None = None) -> None:
self._classes: list[Type[TProduced]] = (
classes if classes is not None else [])
def register(self, cls: Type[IFactoryProduced]) -> None:
def register(self, cls: Type[TProduced]) -> None:
self._classes.append(cls)
def from_data(self, data: bytes) -> IFactoryProduced:
def from_data(self, data: bytes) -> TProduced:
"""
Attempts to create an instance of corresponding
IFactoryProduced-implementing class based on data
TProduced-implementing class based on data
"""
for cls in self._classes:
if cls.detect_by_data(data):
+144
Просмотреть файл
@@ -0,0 +1,144 @@
import logging
from bitarray import bitarray
from dmr_utils3.bptc import decode_emblc
from .enums import CallType, VoiceType
from .etsi_l2 import DMRPL2FullLC, DMRPL2VoiceBurst
from .etsi_l2 import LCFactory, LCBase
from .etsi_l2 import LCLocation, LCCall, LCTalkerAlias
from .exceptions import CallLCDecoderException, EmbLCAssemblerException
from .mmdvm_l1 import DMRPPacketData
class EmbLCAssembler:
"""
This is auxiliary class to assemble a few voice packets and
assemble embedded LC from them.
How to use: pass DMRD packets from the same stream id to process_voicedata, if it returns true, then embedded LC can be decoded with decode()
"""
VTYPE_N_MAP: dict[VoiceType, int] = {
VoiceType.BURST_B: 0,
VoiceType.BURST_C: 1,
VoiceType.BURST_D: 2,
VoiceType.BURST_E: 3
}
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
self.lcs: list[bytes] = []
self.vseq: int = 0
def process_voicedata(self, p: DMRPPacketData) -> bool:
if p.voice_type not in EmbLCAssembler.VTYPE_N_MAP:
return False
burst_n = EmbLCAssembler.VTYPE_N_MAP[p.voice_type]
# check voice sequence for next packets
if burst_n > 0 and p.vseq != (self.vseq + 1) & 0xFF:
err_msg = f"Wrong vseq ({p.vseq}, expected {self.vseq + 1})"
self.reset()
raise EmbLCAssemblerException(err_msg)
# check, if burst sequence matches collected
if len(self.lcs) != burst_n:
err_msg = f"Wrong burst N ({burst_n}, expected {len(self.lcs)})"
self.reset()
raise EmbLCAssemblerException(err_msg)
# try getting LC and collect
emblc = p.get_emb_lc()
if emblc is None:
self.reset()
raise EmbLCAssemblerException("Can't get data")
self.lcs.append(emblc)
self.vseq = p.vseq
return burst_n == 3
def decode(self) -> bytes|None:
if len(self.lcs) != 4:
return None
emblc_data: bitarray = bitarray(b"".join(self.lcs), endian='big')
return decode_emblc(emblc_data)
class CallLCDecoder:
"""
Decode and collect both full lc and embedded lc in the same call
"""
def __init__(self, stream_id: int) -> None:
self.stream_id: int = stream_id
self.lcs: dict[int, LCBase] = {} # flco -> child(LCBase)
self._assembler: EmbLCAssembler = EmbLCAssembler()
def process_voicedata(self, p: DMRPPacketData) -> LCBase|None:
if p.stream_id != self.stream_id:
raise CallLCDecoderException("Wrong stream_id")
if (full_lc := p.get_full_lc()) is not None:
return self._add_lc(full_lc)
else:
try:
if (self._assembler.process_voicedata(p)
and (lc_data := self._assembler.decode()) is not None):
self._assembler.reset()
return self._add_lc(lc_data)
except Exception as e:
logging.debug(f"Exception while processing lc data: {e}")
return None
def _add_lc(self, lc_data: bytes) -> LCBase:
lc = LCFactory.fd(lc_data)
self.lcs[lc.flco] = lc
return lc
@property
def call(self) -> LCCall|None:
for flco in LCCall.FLCOS:
if (flco in self.lcs and
type(lc := self.lcs[flco]) is LCCall):
return lc
return None
@property
def location(self) -> LCLocation|None:
if (LCLocation.FLCOS[0] in self.lcs and
type(lc := self.lcs[LCLocation.FLCOS[0]]) is LCLocation):
return lc
return None
@property
def ta(self) -> str|None:
# check if all parts are collected and are instances of LCTalkerAlias
if any((flco not in self.lcs or
type(self.lcs[flco]) is not LCTalkerAlias)
for flco in LCTalkerAlias.FLCOS):
return None
lcta0: LCTalkerAlias = self.lcs[LCTalkerAlias.FLCOS[0]] # type: ignore[assignment]
ta_data: bytes = b""
for flco in LCTalkerAlias.FLCOS:
if type(lc := self.lcs[flco]) is LCTalkerAlias:
ta_data += lc.ta_data
ta_len = lcta0.len
match lcta0.format:
case LCTalkerAlias.Format._7BIT:
return "" # Not supported; !!TODO: decode 7-bit
case LCTalkerAlias.Format.ISO8:
return ta_data.decode("iso-8859-1", errors='replace')[:ta_len]
case LCTalkerAlias.Format.UTF8:
return ta_data.decode("utf-8", errors='replace')[:ta_len]
case LCTalkerAlias.Format.UTF16BE:
return ta_data.decode("utf-16-be", errors='replace')[:ta_len]
return None
+2 -2
Просмотреть файл
@@ -12,7 +12,7 @@ from .etsi_l2 import DMRPL2Base, DMRPL2FullLC, DMRPL2VoiceBurst
from .exceptions import DMRPBadPacketException
from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPUnknownPacketTypeException
from .factory import BaseFactory, IFactoryProduced
from .factory import AbstractFactory, IFactoryProduced
#############################
@@ -437,7 +437,7 @@ class DMRPPacketData(DMRPBasePeerPacket):
f"vt:{'T' if self.is_voice_term else 'f'}")
class DMRPPacketFactory(BaseFactory):
class DMRPPacketFactory(AbstractFactory[DMRPBasePacket]):
"""
A factory class responsible for creating instances of DMRP packet classes
based on packet data. This class supports both predefined packet types
-68
Просмотреть файл
@@ -1,68 +0,0 @@
import logging
from bitarray import bitarray
from dmr_utils3.bptc import decode_emblc
from .mmdvm_l1 import DMRPPacketData
class EmbLCAssembler:
"""
This is auxiliary class to assemble a few voice packets and
assemble embedded LC from them.
How to use: pass DMRD packets from the same stream id to process_voicedata, if it returns true, then embedded LC can be decoded with decode()
"""
VTYPE_N_MAP: dict[DMRPPacketData.VoiceType, int] = {
DMRPPacketData.VoiceType.BURST_B: 0,
DMRPPacketData.VoiceType.BURST_C: 1,
DMRPPacketData.VoiceType.BURST_D: 2,
DMRPPacketData.VoiceType.BURST_E: 3
}
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
self.lcs: list[bytes] = list()
self.vseq: int = 0
def process_voicedata(self, p: DMRPPacketData) -> bool:
if p.voice_type not in EmbLCAssembler.VTYPE_N_MAP:
return False
burst_n = EmbLCAssembler.VTYPE_N_MAP[p.voice_type]
# check voice sequence for next packets
if burst_n > 0 and p.vseq != (self.vseq + 1) % 0x100:
logging.error("Embedded LC failed: wrong vseq "
f"({p.vseq}, expected {self.vseq + 1})")
self.reset()
return False
# check, if burst sequence matches collected
if len(self.lcs) != burst_n:
logging.error("Embedded LC failed: wrong burst N "
f"({burst_n}, expected {len(self.lcs)})")
self.reset()
return False
# try getting LC and collect
emblc = p.get_emb_lc()
if emblc is None:
logging.error("Embedded LC failed: can't get")
self.reset()
return False
self.lcs.append(emblc)
self.vseq = p.vseq
# logging.debug(f"assembled emblc's: {self.lcs}")
return burst_n == 3
def decode(self) -> bytes|None:
if len(self.lcs) != 4:
return None
emblc_data: bitarray = bitarray(b"".join(self.lcs), endian='big')
return decode_emblc(emblc_data)