ETSI Layer2 implementation; stage1: link control analysis
Этот коммит содержится в:
@@ -1,2 +1,3 @@
|
|||||||
from .mmdvm_l1 import *
|
from .mmdvm_l1 import *
|
||||||
|
from .etsi_l2 import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from bitarray import bitarray
|
||||||
|
from dmr_utils3.bptc import decode_full_lc
|
||||||
|
|
||||||
|
from .exceptions import DMRPL2BadDataException
|
||||||
|
|
||||||
|
|
||||||
|
class DMRPL2Base(ABC):
|
||||||
|
DataTypes = enum.StrEnum('DataTypes', [
|
||||||
|
'UNKNOWN', '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")
|
||||||
|
|
||||||
|
self.bitdata: bitarray = bitarray(data, endian='big')
|
||||||
|
|
||||||
|
def get_data(self) -> bytes:
|
||||||
|
return self.bitdata.tobytes()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_data_type(self) -> DMRPL2Base.DataTypes:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -18,8 +18,15 @@ class DMRPUnknownPacketTypeException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DMRPBadPacket(Exception):
|
class DMRPBadPacketException(Exception):
|
||||||
"""
|
"""
|
||||||
Exception raised when a packet is structurally invalid or corrupted.
|
Exception raised when a packet is structurally invalid or corrupted
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DMRPL2BadDataException(Exception):
|
||||||
|
"""
|
||||||
|
Exception raised when L2 data is invalid or corrupted.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ from hashlib import sha256
|
|||||||
from typing import Type, Self
|
from typing import Type, Self
|
||||||
|
|
||||||
from .base_fields import DMRPFieldInt, DMRPFieldBytes, DMRPFieldStr
|
from .base_fields import DMRPFieldInt, DMRPFieldBytes, DMRPFieldStr
|
||||||
from .exceptions import DMRPBadPacket
|
from .exceptions import DMRPBadPacketException
|
||||||
from .exceptions import DMRPFieldOutOfRangeException
|
from .exceptions import DMRPFieldOutOfRangeException
|
||||||
from .exceptions import DMRPUnknownPacketTypeException
|
from .exceptions import DMRPUnknownPacketTypeException
|
||||||
|
from .etsi_l2 import DMRPL2Base, DMRPL2FullLC, DMRPL2VoiceBurst
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@@ -66,14 +67,14 @@ class DMRPBasePacket(ABC):
|
|||||||
data (bytes): The raw data from which to create a packet.
|
data (bytes): The raw data from which to create a packet.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
DMRPBadPacket: if size or type doesn't match
|
DMRPBadPacketException: if size or type doesn't match
|
||||||
"""
|
"""
|
||||||
self._data = None
|
self._data = None
|
||||||
if not data.startswith(self.PKT_TYPE):
|
if not data.startswith(self.PKT_TYPE):
|
||||||
raise DMRPBadPacket(
|
raise DMRPBadPacketException(
|
||||||
f"Bad packet type: '{data[0:4]!r}', expected '{self.PKT_TYPE!r}'")
|
f"Bad packet type: '{data[0:4]!r}', expected '{self.PKT_TYPE!r}'")
|
||||||
if len(data) != self.PKT_SIZE:
|
if len(data) != self.PKT_SIZE:
|
||||||
raise DMRPBadPacket(
|
raise DMRPBadPacketException(
|
||||||
f"Bad packet size: {len(data)}, expected {self.PKT_SIZE}")
|
f"Bad packet size: {len(data)}, expected {self.PKT_SIZE}")
|
||||||
self._data = bytearray(data)
|
self._data = bytearray(data)
|
||||||
|
|
||||||
@@ -296,6 +297,10 @@ class DMRPPacketData(DMRPBasePeerPacket):
|
|||||||
ber = DMRPFieldInt('ber', 53, 1)
|
ber = DMRPFieldInt('ber', 53, 1)
|
||||||
rssi = DMRPFieldInt('rssi', 54, 1)
|
rssi = DMRPFieldInt('rssi', 54, 1)
|
||||||
|
|
||||||
|
def __init__(self, data: bytes|None = None) -> None:
|
||||||
|
self.__l2: DMRPL2Base|None = None
|
||||||
|
super().__init__(data)
|
||||||
|
|
||||||
def set_random_stream_id(self) -> None:
|
def set_random_stream_id(self) -> None:
|
||||||
self.stream_id = int.from_bytes(random.randbytes(4), byteorder="big")
|
self.stream_id = int.from_bytes(random.randbytes(4), byteorder="big")
|
||||||
|
|
||||||
@@ -375,6 +380,42 @@ class DMRPPacketData(DMRPBasePeerPacket):
|
|||||||
def is_voice_term(self) -> bool:
|
def is_voice_term(self) -> bool:
|
||||||
return self.voice_type == DMRPPacketData.VoiceType.TERM
|
return self.voice_type == DMRPPacketData.VoiceType.TERM
|
||||||
|
|
||||||
|
def get_l2(self) -> DMRPL2Base|None:
|
||||||
|
if self.__l2 is not None:
|
||||||
|
return self.__l2
|
||||||
|
|
||||||
|
if self.voice_type in (DMRPPacketData.VoiceType.HEAD,
|
||||||
|
DMRPPacketData.VoiceType.TERM):
|
||||||
|
self.__l2 = DMRPL2FullLC(self.dmr_data)
|
||||||
|
|
||||||
|
if self.voice_type in (DMRPPacketData.VoiceType.BURST_A,
|
||||||
|
DMRPPacketData.VoiceType.BURST_B,
|
||||||
|
DMRPPacketData.VoiceType.BURST_C,
|
||||||
|
DMRPPacketData.VoiceType.BURST_D,
|
||||||
|
DMRPPacketData.VoiceType.BURST_E,
|
||||||
|
DMRPPacketData.VoiceType.BURST_F):
|
||||||
|
self.__l2 = DMRPL2VoiceBurst(self.dmr_data)
|
||||||
|
|
||||||
|
return self.__l2
|
||||||
|
|
||||||
|
def get_full_lc(self) -> bytes|None:
|
||||||
|
l2 = self.get_l2()
|
||||||
|
if type(l2) is DMRPL2FullLC:
|
||||||
|
return l2.get_full_lc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_emb_lc(self) -> bytes|None:
|
||||||
|
l2 = self.get_l2()
|
||||||
|
if type(l2) is DMRPL2VoiceBurst:
|
||||||
|
return l2.get_emb_lc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_lc(self) -> bytes:
|
||||||
|
if ((lcdata := self.get_full_lc()) is not None or
|
||||||
|
(lcdata := self.get_emb_lc()) is not None):
|
||||||
|
return lcdata
|
||||||
|
return b""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.format()
|
return self.format()
|
||||||
|
|
||||||
@@ -391,7 +432,13 @@ class DMRPPacketData(DMRPBasePeerPacket):
|
|||||||
f"src:{self.src_id} dst:{self.dst_id} "
|
f"src:{self.src_id} dst:{self.dst_id} "
|
||||||
f"bits:{self.bits:08b}")
|
f"bits:{self.bits:08b}")
|
||||||
case 'ext':
|
case 'ext':
|
||||||
return str(self) + f" data:{self.dmr_data.hex()}"
|
return (self.format('') +
|
||||||
|
f" data:{self.dmr_data.hex()}")
|
||||||
|
|
||||||
|
case 'l2':
|
||||||
|
return (self.format('') +
|
||||||
|
f" lc:{self.get_lc().hex()}" +
|
||||||
|
f" data:{self.dmr_data.hex()}")
|
||||||
|
|
||||||
# default
|
# default
|
||||||
return (f"{self.pkt_type} "
|
return (f"{self.pkt_type} "
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
anyio==4.9.0
|
anyio==4.9.0
|
||||||
asyncio==3.4.3
|
asyncio==3.4.3
|
||||||
|
bitarray==3.4.1
|
||||||
|
bitstring==4.3.1
|
||||||
certifi==2025.4.26
|
certifi==2025.4.26
|
||||||
click==8.1.8
|
click==8.1.8
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
|
dmr-utils3==0.1.29
|
||||||
dnspython==2.7.0
|
dnspython==2.7.0
|
||||||
email_validator==2.2.0
|
email_validator==2.2.0
|
||||||
fastapi==0.115.12
|
fastapi==0.115.12
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user