ETSI Layer2 implementation; stage1: link control analysis

Этот коммит содержится в:
Alexander UR6LKW
2025-05-23 18:24:09 +03:00
родитель 9a34e930dd
Коммит da8e0cef8a
5 изменённых файлов: 115 добавлений и 7 удалений
+1
Просмотреть файл
@@ -1,2 +1,3 @@
from .mmdvm_l1 import *
from .etsi_l2 import *
from .exceptions import *
+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()
+9 -2
Просмотреть файл
@@ -18,8 +18,15 @@ class DMRPUnknownPacketTypeException(Exception):
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
+52 -5
Просмотреть файл
@@ -8,9 +8,10 @@ from hashlib import sha256
from typing import Type, Self
from .base_fields import DMRPFieldInt, DMRPFieldBytes, DMRPFieldStr
from .exceptions import DMRPBadPacket
from .exceptions import DMRPBadPacketException
from .exceptions import DMRPFieldOutOfRangeException
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.
Raises:
DMRPBadPacket: if size or type doesn't match
DMRPBadPacketException: if size or type doesn't match
"""
self._data = None
if not data.startswith(self.PKT_TYPE):
raise DMRPBadPacket(
raise DMRPBadPacketException(
f"Bad packet type: '{data[0:4]!r}', expected '{self.PKT_TYPE!r}'")
if len(data) != self.PKT_SIZE:
raise DMRPBadPacket(
raise DMRPBadPacketException(
f"Bad packet size: {len(data)}, expected {self.PKT_SIZE}")
self._data = bytearray(data)
@@ -296,6 +297,10 @@ class DMRPPacketData(DMRPBasePeerPacket):
ber = DMRPFieldInt('ber', 53, 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:
self.stream_id = int.from_bytes(random.randbytes(4), byteorder="big")
@@ -375,6 +380,42 @@ class DMRPPacketData(DMRPBasePeerPacket):
def is_voice_term(self) -> bool:
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:
return self.format()
@@ -391,7 +432,13 @@ class DMRPPacketData(DMRPBasePeerPacket):
f"src:{self.src_id} dst:{self.dst_id} "
f"bits:{self.bits:08b}")
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
return (f"{self.pkt_type} "
+3
Просмотреть файл
@@ -1,9 +1,12 @@
annotated-types==0.7.0
anyio==4.9.0
asyncio==3.4.3
bitarray==3.4.1
bitstring==4.3.1
certifi==2025.4.26
click==8.1.8
colorama==0.4.6
dmr-utils3==0.1.29
dnspython==2.7.0
email_validator==2.2.0
fastapi==0.115.12