REFACT dmrproto
Этот коммит содержится в:
+9
-2
@@ -72,7 +72,8 @@ 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)
|
||||||
- 🥕 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)
|
- 🥕 only one call per ts for peer (per routing entity)
|
||||||
- 🥕 apps unit call routing (routing entity for app)
|
- 🥕 apps unit call routing (routing entity for app)
|
||||||
- 🥕 configuration
|
- 🥕 configuration
|
||||||
@@ -80,7 +81,6 @@ and exposes http API/dashboard on `8000/tcp` (may be changed with `--web-port` c
|
|||||||
- 🥕 resolve id to callsigns
|
- 🥕 resolve id to callsigns
|
||||||
- 🥕 routing 2: group subscriptions
|
- 🥕 routing 2: group subscriptions
|
||||||
- 🥕 data calls (messages)
|
- 🥕 data calls (messages)
|
||||||
- 🥕 dmr internal burst structure decoding (to fix rf fields and get ambe)
|
|
||||||
- 🥕 ambe decode/encode
|
- 🥕 ambe decode/encode
|
||||||
- 🥕 log voice calls as files
|
- 🥕 log voice calls as files
|
||||||
- 🥕 TA support 2 (inside DMRD packets)
|
- 🥕 TA support 2 (inside DMRD packets)
|
||||||
@@ -96,6 +96,13 @@ Digital Mobile Radio (DMR) Systems;
|
|||||||
Part 1: DMR Air Interface (AI) protocol
|
Part 1: DMR Air Interface (AI) protocol
|
||||||
https://www.etsi.org/deliver/etsi_ts/102300_102399/10236101/01.02.01_60/ts_10236101v010201p.pdf
|
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
|
- MMDVMHost by Jonathan Naylor, G4KLX
|
||||||
https://github.com/g4klx/MMDVMHost
|
https://github.com/g4klx/MMDVMHost
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
|
||||||
from .dispatcher import Dispatcher
|
from .dispatcher import Dispatcher
|
||||||
|
from .dmrmaster import DMRMaster
|
||||||
from .dmrproto import DMRPPacketFactory
|
from .dmrproto import DMRPPacketFactory
|
||||||
from .network import IDatagramSender, IDatagramReceiver
|
from .network import IDatagramSender, IDatagramReceiver
|
||||||
from .pphex import hexdump
|
from .pphex import hexdump
|
||||||
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
|
|
||||||
|
|||||||
Executable
@@ -0,0 +1,2 @@
|
|||||||
|
from .mmdvm_l1 import *
|
||||||
|
from .exceptions import *
|
||||||
Executable
+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"))
|
||||||
|
|
||||||
|
|
||||||
Executable
+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
|
||||||
@@ -5,37 +5,12 @@ import random
|
|||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Type, Self, Any
|
from typing import Type, Self
|
||||||
|
|
||||||
|
from .base_fields import DMRPFieldInt, DMRPFieldBytes, DMRPFieldStr
|
||||||
#############################
|
from .exceptions import DMRPBadPacket
|
||||||
# Local exception classes
|
from .exceptions import DMRPFieldOutOfRangeException
|
||||||
#############################
|
from .exceptions import DMRPUnknownPacketTypeException
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@@ -45,81 +20,6 @@ def calc_password_hash(salt: bytes, password: str) -> bytes:
|
|||||||
return sha256(salt + password.encode()).digest()
|
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
|
# Packet classes hierarchy
|
||||||
#############################
|
#############################
|
||||||
@@ -206,6 +106,25 @@ class DMRPBasePeerPacket(DMRPBasePacket, ABC):
|
|||||||
Abstract base class (more specific), which handles typical packets with
|
Abstract base class (more specific), which handles typical packets with
|
||||||
type header and peer_id field
|
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
|
||||||
peer_id: DMRPFieldInt = DMRPFieldPeerAuto('peer_id')
|
peer_id: DMRPFieldInt = DMRPFieldPeerAuto('peer_id')
|
||||||
|
|
||||||
@@ -494,7 +413,7 @@ class DMRPPacketFactory:
|
|||||||
and user-registered custom packet types.
|
and user-registered custom packet types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__instance: Self|None = None
|
__instance: DMRPPacketFactory|None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fd(cls, data: bytes) -> DMRPBasePacket:
|
def fd(cls, data: bytes) -> DMRPBasePacket:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from dmrmaster import DMRMaster
|
from dmrtools import DMRMaster
|
||||||
from dmrtools import AllowAllPeerAuth, ListPeerAuth
|
from dmrtools import AllowAllPeerAuth, ListPeerAuth
|
||||||
from dmrtools.parrot_app import ParrotApp
|
from dmrtools.parrot_app import ParrotApp
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user