diff --git a/README.md b/README.md index 89c0024..4e2c802 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dmrtools/__init__.py b/dmrtools/__init__.py index 75a6ab8..3f14933 100755 --- a/dmrtools/__init__.py +++ b/dmrtools/__init__.py @@ -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 diff --git a/dmrmaster.py b/dmrtools/dmrmaster.py similarity index 100% rename from dmrmaster.py rename to dmrtools/dmrmaster.py diff --git a/dmrtools/dmrproto/__init__.py b/dmrtools/dmrproto/__init__.py new file mode 100755 index 0000000..f59c46d --- /dev/null +++ b/dmrtools/dmrproto/__init__.py @@ -0,0 +1,2 @@ +from .mmdvm_l1 import * +from .exceptions import * diff --git a/dmrtools/dmrproto/base_fields.py b/dmrtools/dmrproto/base_fields.py new file mode 100755 index 0000000..ab2afdd --- /dev/null +++ b/dmrtools/dmrproto/base_fields.py @@ -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")) + + diff --git a/dmrtools/dmrproto/exceptions.py b/dmrtools/dmrproto/exceptions.py new file mode 100755 index 0000000..0f961fb --- /dev/null +++ b/dmrtools/dmrproto/exceptions.py @@ -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 diff --git a/dmrtools/dmrproto.py b/dmrtools/dmrproto/mmdvm_l1.py similarity index 79% rename from dmrtools/dmrproto.py rename to dmrtools/dmrproto/mmdvm_l1.py index 5aa72f1..f3b3df2 100755 --- a/dmrtools/dmrproto.py +++ b/dmrtools/dmrproto/mmdvm_l1.py @@ -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: diff --git a/localmaster_EXAMPLE.py b/localmaster_EXAMPLE.py index f938006..b6d77b0 100755 --- a/localmaster_EXAMPLE.py +++ b/localmaster_EXAMPLE.py @@ -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