Этот коммит содержится в:
Alexander UR6LKW
2025-05-23 16:07:52 +03:00
родитель 70ed9938b2
Коммит 9a34e930dd
8 изменённых файлов: 134 добавлений и 110 удалений
+9 -2
Просмотреть файл
@@ -72,7 +72,8 @@ and exposes http API/dashboard on `8000/tcp` (may be changed with `--web-port` c
- ✔️ unit call routing
- ✔️ allow single peer id check
- ✔️ TA support (DMRA packet)
- 🥕 routing entity (1 timeslot == 1 routing entity)
- 🥕 dmr internal burst structure decoding (to fix rf fields and get ambe)
- 🥕 routing (1 timeslot == 1 routing entity)
- 🥕 only one call per ts for peer (per routing entity)
- 🥕 apps unit call routing (routing entity for app)
- 🥕 configuration
@@ -80,7 +81,6 @@ and exposes http API/dashboard on `8000/tcp` (may be changed with `--web-port` c
- 🥕 resolve id to callsigns
- 🥕 routing 2: group subscriptions
- 🥕 data calls (messages)
- 🥕 dmr internal burst structure decoding (to fix rf fields and get ambe)
- 🥕 ambe decode/encode
- 🥕 log voice calls as files
- 🥕 TA support 2 (inside DMRD packets)
@@ -96,6 +96,13 @@ Digital Mobile Radio (DMR) Systems;
Part 1: DMR Air Interface (AI) protocol
https://www.etsi.org/deliver/etsi_ts/102300_102399/10236101/01.02.01_60/ts_10236101v010201p.pdf
- ETSI TS 102 361-2 V2.3.1 (2016-02)
Electromagnetic compatibility
and Radio spectrum Matters (ERM);
Digital Mobile Radio (DMR) Systems;
Part 2: DMR voice and generic services and facilities
https://www.etsi.org/deliver/etsi_ts/102300_102399/10236102/02.03.01_60/ts_10236102v020301p.pdf
- MMDVMHost by Jonathan Naylor, G4KLX
https://github.com/g4klx/MMDVMHost
+2 -1
Просмотреть файл
@@ -1,5 +1,6 @@
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
from .dispatcher import Dispatcher
from .dmrmaster import DMRMaster
from .dmrproto import DMRPPacketFactory
from .network import IDatagramSender, IDatagramReceiver
from .pphex import hexdump
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
Просмотреть файл
+2
Просмотреть файл
@@ -0,0 +1,2 @@
from .mmdvm_l1 import *
from .exceptions import *
+70
Просмотреть файл
@@ -0,0 +1,70 @@
from abc import ABC, abstractmethod
from typing import Any
from .exceptions import DMRPFieldOutOfRangeException
class DMRPFieldBase(ABC):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
self.name, self.offset, self.bytelen = name, offset, bytelen
self.eoffset = offset + bytelen
self.typename = f"bytes{self.bytelen}"
def get(self, obj) -> bytes:
return bytes(obj._data[self.offset:self.eoffset])
def set(self, obj, value: bytes):
if not isinstance(value, bytes) or self.bytelen != len(value):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
obj._data[self.offset:self.eoffset] = value
@abstractmethod
def __get__(self, obj, cls = None) -> Any:
pass
@abstractmethod
def __set__(self, obj, value: Any) -> None:
pass
class DMRPFieldBytes(DMRPFieldBase):
def __get__(self, obj, cls = None) -> bytes:
return self.get(obj)
def __set__(self, obj, value: bytes) -> None:
self.set(obj, value)
class DMRPFieldStr(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int,
pad_with: bytes = b'\x20') -> None:
super().__init__(name, offset, bytelen)
self.typename = 'str'
self.pad_with = pad_with
def __get__(self, obj, cls = None) -> str:
return (self.get(obj).strip(b'\x20\x00')
.decode(encoding='ascii', errors='ignore'))
def __set__(self, obj, value: str) -> None:
bvalue = value.encode()
if len(bvalue) > self.bytelen:
bvalue = bvalue[:self.bytelen]
bvalue = bvalue.ljust(self.bytelen, self.pad_with)
self.set(obj, bvalue)
class DMRPFieldInt(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
super().__init__(name, offset, bytelen)
self.typename = f"uint{str(bytelen * 8)}"
def __get__(self, obj, cls = None) -> int:
return int.from_bytes(self.get(obj), byteorder="big")
def __set__(self, obj, value: int) -> None:
if not (0 <= value < 1<<(self.bytelen * 8)):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
self.set(obj, value.to_bytes(self.bytelen, byteorder="big"))
+25
Просмотреть файл
@@ -0,0 +1,25 @@
class DMRPFieldOutOfRangeException(Exception):
"""
Exception raised when a packet field value is out of the allowed or expected range.
Attributes:
field: The name or identifier of the problematic field.
typename: The expected type or range description for the field.
"""
def __init__(self, field, typename) -> None:
super().__init__(f"Field out of range: {field} must be {typename}")
class DMRPUnknownPacketTypeException(Exception):
"""
Exception raised when the packet factory cannot recognize the packet type
from the given input data.
"""
pass
class DMRPBadPacket(Exception):
"""
Exception raised when a packet is structurally invalid or corrupted.
"""
pass
+25 -106
Просмотреть файл
@@ -5,37 +5,12 @@ import random
from abc import ABC
from hashlib import sha256
from typing import Type, Self, Any
from typing import Type, Self
#############################
# Local exception classes
#############################
class DMRPFieldOutOfRangeException(Exception):
"""
Exception raised when a packet field value is out of the allowed or expected range.
Attributes:
field: The name or identifier of the problematic field.
typename: The expected type or range description for the field.
"""
def __init__(self, field, typename) -> None:
super().__init__(f"Field out of range: {field} must be {typename}")
class DMRPUnknownPacketTypeException(Exception):
"""
Exception raised when the packet factory cannot recognize the packet type
from the given input data.
"""
pass
class DMRPBadPacket(Exception):
"""
Exception raised when a packet is structurally invalid or corrupted.
"""
pass
from .base_fields import DMRPFieldInt, DMRPFieldBytes, DMRPFieldStr
from .exceptions import DMRPBadPacket
from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPUnknownPacketTypeException
#############################
@@ -45,81 +20,6 @@ def calc_password_hash(salt: bytes, password: str) -> bytes:
return sha256(salt + password.encode()).digest()
#############################
# Field classes
#############################
class DMRPFieldBase(ABC):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
self.name, self.offset, self.bytelen = name, offset, bytelen
self.eoffset = offset + bytelen
self.typename = f"bytes{self.bytelen}"
def __get__(self, obj, cls = None) -> Any:
return bytes(obj._data[self.offset:self.eoffset])
def __set__(self, obj, value: Any) -> None:
if not isinstance(value, bytes) or self.bytelen != len(value):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
obj._data[self.offset:self.eoffset] = value
class DMRPFieldBytes(DMRPFieldBase):
def __get__(self, obj, cls = None) -> bytes:
return super().__get__(obj, cls)
def __set__(self, obj, value: bytes) -> None:
super().__set__(obj, value)
class DMRPFieldStr(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int,
pad_with: bytes = b'\x20') -> None:
super().__init__(name, offset, bytelen)
self.typename = 'str'
self.pad_with = pad_with
def __get__(self, obj, cls = None) -> str:
return (super().__get__(obj, cls).strip(b'\x20\x00')
.decode(encoding='ascii', errors='ignore'))
def __set__(self, obj, value: str) -> None:
bvalue = value.encode()
if len(bvalue) > self.bytelen:
bvalue = bvalue[:self.bytelen]
bvalue = bvalue.ljust(self.bytelen, self.pad_with)
super().__set__(obj, bvalue)
class DMRPFieldInt(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
super().__init__(name, offset, bytelen)
self.typename = f"uint{str(bytelen * 8)}"
def __get__(self, obj, cls = None) -> int:
return int.from_bytes(super().__get__(obj, cls), byteorder="big")
def __set__(self, obj, value: int) -> None:
if not (0 <= value < 1<<(self.bytelen * 8)):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
super().__set__(obj, value.to_bytes(self.bytelen, byteorder="big"))
class DMRPFieldPeerAuto(DMRPFieldInt):
def fixoffset(self, offset: int) -> None:
self.offset, self.eoffset = offset, offset + self.bytelen
def __init__(self, name: str) -> None:
super().__init__(name, 4, 4)
def __get__(self, obj, cls = None) -> int:
self.fixoffset(len(obj.PKT_TYPE))
return super().__get__(obj, cls)
def __set__(self, obj, value: int) -> None:
self.fixoffset(len(obj.PKT_TYPE))
super().__set__(obj, value)
#############################
# Packet classes hierarchy
#############################
@@ -206,6 +106,25 @@ class DMRPBasePeerPacket(DMRPBasePacket, ABC):
Abstract base class (more specific), which handles typical packets with
type header and peer_id field
"""
class DMRPFieldPeerAuto(DMRPFieldInt):
"""
Int field, which autodetects header length (PKT_TYPE) and
offsets peer_id for its len
"""
def fixoffset(self, offset: int) -> None:
self.offset, self.eoffset = offset, offset + self.bytelen
def __init__(self, name: str) -> None:
super().__init__(name, 4, 4)
def __get__(self, obj, cls = None) -> int:
self.fixoffset(len(obj.PKT_TYPE))
return super().__get__(obj, cls)
def __set__(self, obj, value: int) -> None:
self.fixoffset(len(obj.PKT_TYPE))
super().__set__(obj, value)
# peer_id
peer_id: DMRPFieldInt = DMRPFieldPeerAuto('peer_id')
@@ -494,7 +413,7 @@ class DMRPPacketFactory:
and user-registered custom packet types.
"""
__instance: Self|None = None
__instance: DMRPPacketFactory|None = None
@classmethod
def fd(cls, data: bytes) -> DMRPBasePacket:
+1 -1
Просмотреть файл
@@ -1,7 +1,7 @@
import logging
import time
from dmrmaster import DMRMaster
from dmrtools import DMRMaster
from dmrtools import AllowAllPeerAuth, ListPeerAuth
from dmrtools.parrot_app import ParrotApp