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 .mmdvm_l1 import *
from .etsi_l2 import *
from .exceptions 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 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
+52 -5
Просмотреть файл
@@ -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} "
+3
Просмотреть файл
@@ -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