Этот коммит содержится в:
Alexander UR6LKW
2025-05-31 19:11:26 +03:00
родитель a3b486071d
Коммит 07aa4efb2d
16 изменённых файлов: 280 добавлений и 155 удалений
+12 -6
Просмотреть файл
@@ -1,8 +1,12 @@
# 0DMRMaster # 0DMRMaster
Private DMR master server. Version 0.4. Private DMR master server. Version 0.5.
Copyright ©2025 Alexander Mokrov, UR6LKW. Copyright ©2025 Alexander Mokrov, UR6LKW.
This project is currently in public alpha and is under active development.
Don't use it in production environment, interfaces may change.
## License
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
@@ -18,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
## Overview ## Overview
There is a few parts: This project contains:
1. An implementation of homebrew dmr protocol (which is used by brandmeister and hblink). Not yet complete, but sufficient for #2 and #3. 1. An implementation of homebrew dmr protocol (which is used by brandmeister and hblink). Not yet complete, but sufficient for #2 and #3.
1. Decoding UDP proxy 1. Decoding UDP proxy
1. Basic private dmr master server 1. Basic private dmr master server
@@ -26,9 +30,9 @@ There is a few parts:
### Installation ### Installation
1. Check basic requirements: 1. Check basic requirements:
- Linux or Windows - Linux or Windows
- Python 3.13+ - Python 3.11+
1. Clone/download the repo. 1. Clone/download the repo.
1. Execute (linux example): 1. Create environment and install requirements (linux example):
``` ```
$ python -m venv venv $ python -m venv venv
$ . venv/bin/activate $ . venv/bin/activate
@@ -50,6 +54,7 @@ The running server listens for `62031/udp` as DMR service (may be changed with `
and exposes http API/dashboard on `8000/tcp` (may be changed with `--web-port` command line argument): and exposes http API/dashboard on `8000/tcp` (may be changed with `--web-port` command line argument):
- API: http://YOUR-SERVER-IP:8000/api/dashboard - API: http://YOUR-SERVER-IP:8000/api/dashboard
- Web dashboard: http://YOUR-SERVER-IP:8000/dashboard/index.html - Web dashboard: http://YOUR-SERVER-IP:8000/dashboard/index.html
![web dashboard](https://github.com/olympy/0DMRMaster/blob/master/doc/dashboard-screenshot.png?raw=true)
@@ -72,9 +77,10 @@ and exposes http API/dashboard on `8000/tcp` (may be changed with `--web-port` c
- ✔️ unit call routing - ✔️ unit call routing
- ✔️ allow single peer id check - ✔️ allow single peer id check
- ✔️ TA support (DMRA packet) - ✔️ TA support (DMRA packet)
- 🥕 dmr internal burst structure decoding (to fix rf fields and get ambe) - ✔️ dmr internal structure decoding (get LC, voice data)
- 🥕 routing (1 timeslot == 1 routing entity) - 🥕 routing (1 timeslot == 1 routing entity)
- 🥕 only one call per ts for peer (per routing entity) - 🥕 only one call per ts for peer (per routing entity)
- 🥕 dmr internal structure encoding (set LC, generate voice data)
- 🥕 apps unit call routing (routing entity for app) - 🥕 apps unit call routing (routing entity for app)
- 🥕 configuration - 🥕 configuration
- 🥕 users configuration (allowed id and passes per id) - 🥕 users configuration (allowed id and passes per id)
+3 -6
Просмотреть файл
@@ -16,14 +16,12 @@ class IAppDispatcher(ABC):
Dispatcher interface for app Dispatcher interface for app
""" """
@abstractmethod @abstractmethod
def inject_packet(self, p: DMRPPacketData) -> None: def inject_packet(self, p: DMRPPacketData) -> None: ...
pass
class IAppCallInterceptor(ABC): class IAppCallInterceptor(ABC):
@abstractmethod @abstractmethod
def process_call_packet(self, call: Call, p: DMRPPacketData) -> None: def process_call_packet(self, call: Call, p: DMRPPacketData) -> None: ...
pass
class App(ABC): class App(ABC):
@@ -31,8 +29,7 @@ class App(ABC):
self.dispatcher: IAppDispatcher|None = None self.dispatcher: IAppDispatcher|None = None
@abstractmethod @abstractmethod
def get_name(self) -> str: def get_name(self) -> str: ...
pass
class AppKeeper: class AppKeeper:
+2 -4
Просмотреть файл
@@ -7,13 +7,11 @@ from .dmrproto import calc_password_hash
class IPeerAuth(ABC): class IPeerAuth(ABC):
@abstractmethod @abstractmethod
def allow_peer_id(self, peer_id: int) -> bool: def allow_peer_id(self, peer_id: int) -> bool: ...
pass
@abstractmethod @abstractmethod
def check_password(self, peer_id: int, salt: bytes, def check_password(self, peer_id: int, salt: bytes,
pass_hash: bytes) -> bool: pass_hash: bytes) -> bool: ...
pass
class AllowAllPeerAuth(IPeerAuth): class AllowAllPeerAuth(IPeerAuth):
+29 -1
Просмотреть файл
@@ -2,7 +2,10 @@ import logging
from time import time from time import time
from .dmrproto import CallLCDecoder
from .dmrproto import DMRPPacketData from .dmrproto import DMRPPacketData
from .dmrproto import LCBase, LCCall, LCLocation
from .dmrproto import CallType
from .peer import Peer from .peer import Peer
@@ -24,6 +27,10 @@ class Call:
self.end_time: float|None = None self.end_time: float|None = None
self.packets: int = 0 self.packets: int = 0
self.route_to: set[Peer]|None = None self.route_to: set[Peer]|None = None
self.ta: str|None = None
self.loc: LCLocation|None = None
self.rfcall: LCCall|None = None
self._lc_decoder: CallLCDecoder = CallLCDecoder(call_id)
logging.info(f"Voice call beg {str(self)}") logging.info(f"Voice call beg {str(self)}")
@property @property
@@ -56,9 +63,30 @@ class Call:
def to_be_cleaned_log(self) -> bool: def to_be_cleaned_log(self) -> bool:
return self.is_ended and not self.check_timeout(self.CLEAN_LOG_TIMEOUT) return self.is_ended and not self.check_timeout(self.CLEAN_LOG_TIMEOUT)
def packet_received(self) -> None: def packet_received(self, p: DMRPPacketData|None = None) -> None:
self.last_packet_time = time() self.last_packet_time = time()
self.packets += 1 self.packets += 1
if p is not None:
lc: LCBase|None = self._lc_decoder.process_voicedata(p)
if lc is not None:
logging.debug(f"New lc: {lc}")
self._update_lc_data()
def _update_lc_data(self) -> None:
if (self.ta is None
and (ta := self._lc_decoder.ta) is not None):
self.ta = ta
logging.info(f"Talker alias: '{ta}'")
if (self.loc is None
and (loc := self._lc_decoder.location) is not None):
self.loc = loc
logging.info(f"Location: {loc.lat} {loc.lon}")
if (self.rfcall is None
and (rfcall := self._lc_decoder.call) is not None):
self.rfcall = rfcall
dst: str = (('TG' if rfcall.call_type == CallType.GROUP else '')
+ str(rfcall.dst_id))
logging.info(f"RF call data: {rfcall.src_id}->{dst}")
def check_timeout(self, timeout: float) -> bool: def check_timeout(self, timeout: float) -> bool:
return time() - self.last_packet_time < timeout return time() - self.last_packet_time < timeout
+2 -2
Просмотреть файл
@@ -46,7 +46,7 @@ class Dispatcher(IDatagramReceiver, IAppDispatcher, IPCDispatcher):
controller.send_close() controller.send_close()
def dispatch_data_packet(self, p: DMRPPacketData, orig_addr: tuple) -> None: def dispatch_data_packet(self, p: DMRPPacketData, orig_addr: tuple) -> None:
logging.debug(f"Dispatching packet from {orig_addr}:\n{p}\n") # logging.debug(f"Dispatching packet from {orig_addr}:\n{p}\n")
call_id = p.stream_id call_id = p.stream_id
@@ -61,7 +61,7 @@ class Dispatcher(IDatagramReceiver, IAppDispatcher, IPCDispatcher):
if len(peers) > 0: if len(peers) > 0:
call.route_to = peers call.route_to = peers
call.packet_received() call.packet_received(p)
if p.is_voice_term: if p.is_voice_term:
call.end() call.end()
+3 -2
Просмотреть файл
@@ -1,4 +1,5 @@
from .mmdvm_l1 import * from .enums import *
from .etsi_l2 import * from .etsi_l2 import *
from .util import *
from .exceptions 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 obj._data[self.offset:self.eoffset] = value
@abstractmethod @abstractmethod
def __get__(self, obj, cls = None) -> Any: def __get__(self, obj, cls = None) -> Any: ...
pass
@abstractmethod @abstractmethod
def __set__(self, obj, value: Any) -> None: def __set__(self, obj, value: Any) -> None: ...
pass
class DMRPFieldBytes(DMRPFieldBase): class DMRPFieldBytes(DMRPFieldBase):
+22 -25
Просмотреть файл
@@ -12,20 +12,21 @@ from .enums import CallType
from .exceptions import DMRPFieldOutOfRangeException from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPL2BadDataException from .exceptions import DMRPL2BadDataException
from .exceptions import DMRPUnknownLCTypeException from .exceptions import DMRPUnknownLCTypeException
from .factory import BaseFactory, IFactoryProduced from .factory import AbstractFactory, IFactoryProduced
class DMRPL2Base(ABC): 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: def __init__(self, data: bytes) -> None:
self.set_data(data) self.set_data(data)
def set_data(self, data: bytes) -> None: def set_data(self, data: bytes) -> None:
if len(data) != 33: 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') self.bitdata: bitarray = bitarray(data, endian='big')
@@ -33,8 +34,7 @@ class DMRPL2Base(ABC):
return self.bitdata.tobytes() return self.bitdata.tobytes()
@abstractmethod @abstractmethod
def get_data_type(self) -> DMRPL2Base.DataTypes: def get_data_type(self) -> DMRPL2Base.DataTypes: ...
pass
class DMRPL2FullLC(DMRPL2Base): class DMRPL2FullLC(DMRPL2Base):
@@ -136,18 +136,12 @@ class LCTalkerAlias(LCBase):
_7BIT = 0b00 _7BIT = 0b00
ISO8 = 0b01 ISO8 = 0b01
UTF8 = 0b10 UTF8 = 0b10
UTF16BF = 0b11 UTF16BE = 0b11
def __init__(
self, data: bytes|None,
format: Format = Format.ISO8) -> None:
super().__init__(data)
self.__format: LCTalkerAlias.Format = format
# format # format
def get_format(self) -> Format: def get_format(self) -> Format|None:
if self.flco != 0x04: if self.flco != 0x04:
return self.__format return None
return LCTalkerAlias.Format((self._data[2] & 0xC0) >> 6) return LCTalkerAlias.Format((self._data[2] & 0xC0) >> 6)
def set_format(self, format: Format) -> None: def set_format(self, format: Format) -> None:
@@ -158,10 +152,10 @@ class LCTalkerAlias(LCBase):
format = property(get_format, set_format) format = property(get_format, set_format)
# len # len
def get_len(self) -> int: def get_len(self) -> int|None:
if self.flco != 0x04: if self.flco != 0x04:
return 0 return None
return (self._data[2] & 0x3E) >> 1 return int((self._data[2] & 0x3E) >> 1)
def set_len(self, len: int) -> None: def set_len(self, len: int) -> None:
if self.flco != 0x04: if self.flco != 0x04:
@@ -174,18 +168,21 @@ class LCTalkerAlias(LCBase):
len = property(get_len, set_len) len = property(get_len, set_len)
@property @property
def ta_str(self) -> str: def ta_data(self) -> bytes:
# TODO: decode different fmts
if self.flco == 0x04: if self.flco == 0x04:
return self._data[3:9].decode(encoding='ascii') if self.format == LCTalkerAlias.Format._7BIT:
return self._data[2:9].decode(encoding='ascii') 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: def __str__(self) -> str:
return (f"LC TA {self.flco} fmt:{self.format.name} " 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: def __init__(self) -> None:
""" """
Initializes the lc analyzer factory with a list of all lc 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): class DMRPFieldOutOfRangeException(Exception):
""" """
Exception raised when a packet field value is out of the allowed or expected range. 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 Exception raised when a packet is structurally invalid or corrupted
""" """
pass ...
class DMRPL2BadDataException(Exception): class DMRPL2BadDataException(Exception):
""" """
Exception raised when L2 data is invalid or corrupted. Exception raised when L2 data is invalid or corrupted.
""" """
pass ...
class FactoryException(Exception): class FactoryException(Exception):
pass ...
class DMRPUnknownPacketTypeException(FactoryException): class DMRPUnknownPacketTypeException(FactoryException):
@@ -33,7 +35,7 @@ class DMRPUnknownPacketTypeException(FactoryException):
Exception raised when the packet factory cannot recognize the packet type Exception raised when the packet factory cannot recognize the packet type
from the given input data. from the given input data.
""" """
pass ...
class DMRPUnknownLCTypeException(FactoryException): class DMRPUnknownLCTypeException(FactoryException):
@@ -41,6 +43,31 @@ class DMRPUnknownLCTypeException(FactoryException):
Exception raised when the lc analyzer factory cannot recognize the Exception raised when the lc analyzer factory cannot recognize the
lc type from the given input data. 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 abc import ABC, abstractmethod
from typing import Self, Type, NoReturn from typing import TypeVar, Generic, Type, ClassVar, NoReturn, Any
from .exceptions import FactoryException from .exceptions import FactoryException
class IFactoryProduced(ABC): class IFactoryProduced(ABC):
@abstractmethod @abstractmethod
def __init__(self, data: bytes) -> None: def __init__(self, data: bytes) -> None: ...
pass
@classmethod @classmethod
@abstractmethod @abstractmethod
def detect_by_data(cls, data: bytes) -> bool: def detect_by_data(cls, data: bytes) -> bool: ...
pass
class BaseFactory(ABC): TProduced = TypeVar("TProduced", bound=IFactoryProduced, covariant=True)
__instance: Self|None = None
class AbstractFactory(Generic[TProduced], ABC):
_instance: ClassVar[AbstractFactory[Any]|None] = None
@classmethod @classmethod
def fd(cls, data: bytes) -> IFactoryProduced: def fd(cls: Type[AbstractFactory[TProduced]],
data: bytes) -> TProduced:
""" """
Short singleton version of from_data method Short singleton version of from_data method
""" """
if cls.__instance is None: if cls._instance is None:
cls.__instance = cls() cls._instance = cls()
return cls.__instance.from_data(data) return cls._instance.from_data(data)
def __init__(self, def __init__(self,
classes: list[Type[IFactoryProduced]]|None = None) -> None: classes: list[Type[TProduced]]|None = None) -> None:
self._classes: list[Type[IFactoryProduced]] = ( self._classes: list[Type[TProduced]] = (
classes if classes is not None else []) 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) 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 Attempts to create an instance of corresponding
IFactoryProduced-implementing class based on data TProduced-implementing class based on data
""" """
for cls in self._classes: for cls in self._classes:
if cls.detect_by_data(data): 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 DMRPBadPacketException
from .exceptions import DMRPFieldOutOfRangeException from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPUnknownPacketTypeException 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'}") 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 A factory class responsible for creating instances of DMRP packet classes
based on packet data. This class supports both predefined packet types 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)
+3 -7
Просмотреть файл
@@ -3,16 +3,12 @@ from abc import ABC, abstractmethod
class IDatagramReceiver(ABC): class IDatagramReceiver(ABC):
@abstractmethod @abstractmethod
def recv_dg(self, data: bytes, addr: tuple) -> None: def recv_dg(self, data: bytes, addr: tuple) -> None: ...
pass
class IDatagramSender(ABC): class IDatagramSender(ABC):
@abstractmethod @abstractmethod
def send_dg(self, data: bytes, addr: tuple) -> None: def send_dg(self, data: bytes, addr: tuple) -> None: ...
pass
@abstractmethod @abstractmethod
def set_receiver(self, receiver: IDatagramReceiver) -> None: def set_receiver(self, receiver: IDatagramReceiver) -> None: ...
pass
+3 -6
Просмотреть файл
@@ -18,16 +18,13 @@ class IPCDispatcher(ABC):
Dispatcher interface to pass into controller to call back Dispatcher interface to pass into controller to call back
""" """
@abstractmethod @abstractmethod
def send_dg(self, data: bytes, addr: tuple) -> None: def send_dg(self, data: bytes, addr: tuple) -> None: ...
pass
@abstractmethod @abstractmethod
def get_peer_keeper(self) -> PeerKeeper: def get_peer_keeper(self) -> PeerKeeper: ...
pass
@abstractmethod @abstractmethod
def get_peer_auth(self) -> IPeerAuth: def get_peer_auth(self) -> IPeerAuth: ...
pass
class PeerController: class PeerController:
Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 138 KiB