0DMRMaster version 0.5
Этот коммит содержится в:
+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
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 *
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Executable
+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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Executable
Двоичные данные
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 138 KiB |
Ссылка в новой задаче
Block a user