Skip to content

Commit

Permalink
util to assemble IPSC packets, waking up repeater on first call or af…
Browse files Browse the repository at this point in the history
…ter long-inactivity, added some useful logging
  • Loading branch information
smarek committed Dec 29, 2020
1 parent 2b9dc50 commit dd18609
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 79 deletions.
6 changes: 3 additions & 3 deletions hytera_homebrew_bridge/kaitai/data_delivery_states.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

from pkg_resources import parse_version
import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
from enum import Enum

import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream
from pkg_resources import parse_version

if parse_version(kaitaistruct.__version__) < parse_version("0.9"):
raise Exception(
Expand Down
1 change: 1 addition & 0 deletions hytera_homebrew_bridge/kaitai/ip_site_connect_protocol.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ enums:
0xAAAA: slot_type_data_f
0xBBBB: slot_type_data_a
0xCCCC: slot_type_data_b
0xDDDD: slot_type_wakeup_request
0xEEEE: slot_type_ipsc_sync
0x0000: slot_type_unknown
packet_types:
Expand Down
1 change: 1 addition & 0 deletions hytera_homebrew_bridge/kaitai/ip_site_connect_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SlotTypes(Enum):
slot_type_data_f = 43690
slot_type_data_a = 48059
slot_type_data_b = 52428
slot_type_wakeup_request = 56797
slot_type_ipsc_sync = 61166

class PacketTypes(Enum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ IpSiteConnectProtocol.SlotTypes = enum.Enum {
slot_type_data_f = 43690,
slot_type_data_a = 48059,
slot_type_data_b = 52428,
slot_type_wakeup_request = 56797,
slot_type_ipsc_sync = 61166,
}

Expand Down
125 changes: 61 additions & 64 deletions hytera_homebrew_bridge/lib/hytera_mmdvm_translator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import asyncio
from asyncio import Queue
from time import time

from bitarray import bitarray
from kaitaistruct import KaitaiStruct
Expand All @@ -12,7 +13,12 @@
from hytera_homebrew_bridge.kaitai.mmdvm import Mmdvm
from hytera_homebrew_bridge.lib import settings as module_settings
from hytera_homebrew_bridge.lib.logging_trait import LoggingTrait
from hytera_homebrew_bridge.lib.utils import byteswap_bytes
from hytera_homebrew_bridge.lib.utils import (
byteswap_bytes,
assemble_hytera_ipsc_packet,
assemble_hytera_ipsc_wakeup_packet,
assemble_hytera_ipsc_sync_packet,
)


class HyteraMmdvmTranslator(LoggingTrait):
Expand All @@ -32,6 +38,7 @@ def __init__(
# translation / state-machine related variables
self.mmdvm_last_sequence: int = 0
self.mmdvm_sequence_number: int = 0
self.hytera_last_sent_timestamp: int = 0
self.hytera_last_sequence_out: int = 0
self.hytera_last_sequence_in: int = -1
self.hytera_last_started_stream_id_out: int = -1
Expand Down Expand Up @@ -90,7 +97,17 @@ async def translate_from_hytera(self):
packet.slot_type
== IpSiteConnectProtocol.SlotTypes.slot_type_ipsc_sync
):
# self.log("Received IPSC Sync packet, not translating")
self.log_debug(
"HYTERA->MMDVM Received IPSC Sync packet, not translating"
)
continue
if (
packet.slot_type
== IpSiteConnectProtocol.SlotTypes.slot_type_wakeup_request
):
self.log_debug(
"HYTERA->MMDVM Received IPSC Wakeup packet, not translating"
)
continue

if self.hytera_last_sequence_in == packet.sequence_number:
Expand Down Expand Up @@ -220,11 +237,15 @@ async def translate_from_mmdvm(self):

self.mmdvm_sequence_number = (self.mmdvm_sequence_number + 1) & 0xFF

slot_type: bytes
swapped_bytes: bytes = byteswap_bytes(packet.command_data.dmr_data)

slot_type: IpSiteConnectProtocol.SlotTypes
if packet.command_data.frame_type == 2:
if packet.command_data.data_type == 1:
# voice lc header
slot_type = b"\x11\x11"
slot_type = (
IpSiteConnectProtocol.SlotTypes.slot_type_voice_lc_header
)
self.mmdvm_sequence_number = 0
self.log_info(
"MMDVM->HYTERA *%s CALL START* FROM: %s TO: %s TS: %s"
Expand All @@ -237,9 +258,23 @@ async def translate_from_mmdvm(self):
"1" if packet.command_data.slot_no == 1 else "2",
)
)
if self.hytera_last_sent_timestamp < (time() - 60):
self.log_info("Waking up repeater, both timeslots")
for TS in (True, False):
# send wakeup packet
hytera_ipsc_packet: bytes = (
assemble_hytera_ipsc_wakeup_packet(
timeslot_is_ts1=TS,
target_id=packet.command_data.target_id,
source_id=packet.command_data.source_id,
)
)
await self.queue_hytera_output.put(hytera_ipsc_packet)
else:
# terminator with lc
slot_type = b"\x22\x22"
slot_type = (
IpSiteConnectProtocol.SlotTypes.slot_type_terminator_with_lc
)
self.log_info(
"MMDVM->HYTERA *%s CALL END * FROM: %s TO: %s TS: %s"
% (
Expand All @@ -256,66 +291,28 @@ async def translate_from_mmdvm(self):
packet.command_data.data_type
)

swapped_bytes: bytes = byteswap_bytes(packet.command_data.dmr_data)
# if slot_type == IpSiteConnectProtocol.SlotTypes.slot_type_data_a:
# # send sync packet
# hytera_ipsc_packet: bytes = assemble_hytera_ipsc_sync_packet(
# timeslot_is_ts1=(packet.command_data.slot_no == 0),
# is_private_call=(packet.command_data.call_type == 1),
# target_id=packet.command_data.target_id,
# source_id=packet.command_data.source_id,
# )
# self.log_info(f"Sending ipsc sync to repeater {hytera_ipsc_packet.hex()}")
# await self.queue_hytera_output.put(hytera_ipsc_packet)

data_string: bytes = (
# source port
self.settings.dmr_port.to_bytes(2, byteorder="little")
+
# magic fixed header
b"\x00\x50"
+
# sequence_number
self.mmdvm_sequence_number.to_bytes(1, byteorder="little")
+
# reserved_3
b"\xE0\x00\x00"
+
# packet type
b"\x01"
+
# reserved_7a
b"\x00\x05\x01"
+ (b"\x02" if packet.command_data.slot_no == 1 else b"\x01")
+ b"\x00\x00\x00"
+
# timeslot_raw
(b"\x22\x22" if packet.command_data.slot_no == 1 else b"\x11\x11")
+
# slot_type
self.mmdvm_to_hytera_slottype_str.get(slot_type, b"\x00\x00")
+
# delimiter
b"\x11\x11"
+
# frame_type
b"\xBB\xBB"
+
# reserved_2a
b"\x40\x5C"
+
# payload data
swapped_bytes
+
# two byte crc16 checksum
b"\x00\x00"
# reserved_2b
b"\x63\x02"
+
# call_type, mmdvm true = private, ipsc 00 = private
(b"\x11" if packet.command_data.call_type else b"\x00")
+
# destination id
int(packet.command_data.target_id).to_bytes(4, byteorder="little")
+
# source id
max(int(packet.command_data.source_id), 4294967295).to_bytes(
4, byteorder="little"
)
+
# reserved_1b
b"\x00"
hytera_ipsc_packet: bytes = assemble_hytera_ipsc_packet(
udp_port=self.settings.dmr_port,
sequence_number=self.mmdvm_sequence_number,
timeslot_is_ts1=(packet.command_data.slot_no == 0),
hytera_slot_type=int(slot_type.__getattribute__("value")),
dmr_payload=swapped_bytes,
is_private_call=(packet.command_data.call_type == 1),
target_id=packet.command_data.target_id,
source_id=packet.command_data.source_id,
)

await self.queue_hytera_output.put(data_string)
await self.queue_hytera_output.put(hytera_ipsc_packet)
self.hytera_last_sent_timestamp = time()
self.queue_mmdvm_to_translate.task_done()
5 changes: 3 additions & 2 deletions hytera_homebrew_bridge/lib/hytera_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def handle_registration(self, data: bytes, address: tuple) -> None:
def handle_rdac_request(self, data: bytes, address: tuple) -> None:
if not self.settings.hytera_is_registered:
self.log_debug("Rejecting RDAC request for not-registered repeater")
self.transport.sendto(bytes(0x01), address)
self.transport.sendto(bytes([0x00]), address)
return

response_address = (address[0], self.settings.p2p_port)
Expand Down Expand Up @@ -103,7 +103,7 @@ def get_redirect_packet(data: bytearray, target_port: int):
def handle_dmr_request(self, data: bytes, address: tuple) -> None:
if not self.settings.hytera_is_registered:
self.log_debug("Rejecting DMR request for not-registered repeater")
self.transport.sendto(bytes(0x01), address)
self.transport.sendto(bytes([0x00]), address)
return

response_address = (address[0], self.settings.p2p_port)
Expand Down Expand Up @@ -519,6 +519,7 @@ def connection_made(self, transport: transports.BaseTransport) -> None:

def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
try:
self.log_debug(f"Hytera incoming {data.hex()}")
self.queue_incoming.put_nowait(parse_hytera_data(data))
except EOFError as e:
self.log_error(f"Cannot parse IPSC DMR packet {hexlify(data)} from {addr}")
Expand Down
42 changes: 35 additions & 7 deletions hytera_homebrew_bridge/lib/mmdvm_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from asyncio import transports, Queue
from binascii import hexlify, a2b_hex
from hashlib import sha256
from socket import socket
from typing import Optional, Callable, Tuple

from hytera_homebrew_bridge.kaitai.mmdvm import Mmdvm
Expand Down Expand Up @@ -57,9 +58,18 @@ async def send_mmdvm_from_queue(self) -> None:

def connection_made(self, transport: transports.BaseTransport) -> None:
self.log_debug("MMDVM socket connected")
self.transport = transport
if self.connection_status is not self.CON_LOGIN_SUCCESSFULL:
self.send_login_request()
if not self.transport or self.transport.is_closing():
self.log_debug("Setting transport")
self.transport = transport
if self.connection_status is not self.CON_LOGIN_SUCCESSFULL:
self.send_login_request()
else:
self.log_debug("ignoring new transport")
hb_local_socket = transport.get_extra_info("socket")
if isinstance(hb_local_socket, socket):
self.log_warning(
f"Ignoring new transport {hb_local_socket.getsockname()}"
)

def connection_lost(self, exc: Optional[Exception]) -> None:
self.log_debug("MMDVM socket closed")
Expand All @@ -70,32 +80,49 @@ def connection_lost(self, exc: Optional[Exception]) -> None:

def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
packet = Mmdvm.from_bytes(data)
is_handled: bool = False
if isinstance(packet.command_data, Mmdvm.TypeMasterNotAccept):
if self.connection_status == self.CON_LOGIN_REQUEST_SENT:
self.connection_status = self.CON_NEW
self.log_error("Master did not accept our login request")
is_handled = True
elif self.connection_status == self.CON_LOGIN_RESPONSE_SENT:
self.connection_status = self.CON_NEW
self.log_error("Master did not accept our password challenge response")
is_handled = True
elif self.connection_status == self.CON_LOGIN_SUCCESSFULL:
self.connection_status = self.CON_NEW
self.log_info("Connection timed-out or was interrupted, do login again")
self.send_login_request()
is_handled = True
elif isinstance(packet.command_data, Mmdvm.TypeMasterRepeaterAck):
if self.connection_status == self.CON_LOGIN_REQUEST_SENT:
self.log_info("Sending Login Response")
self.send_login_response(packet.command_data.repeater_id_or_challenge)
is_handled = True
elif self.connection_status == self.CON_LOGIN_RESPONSE_SENT:
self.log_info("Master Login Accept")
self.connection_status = self.CON_LOGIN_SUCCESSFULL
self.send_configuration()
is_handled = True
elif self.connection_status == self.CON_LOGIN_SUCCESSFULL:
self.log_info("Master accepted our configuration")
is_handled = True
elif isinstance(packet.command_data, Mmdvm.TypeMasterPong):
# self.log("Master PONG received")
self.log_debug("Master PONG received")
is_handled = True
pass
elif isinstance(packet.command_data, Mmdvm.TypeMasterClosing):
self.log_info("Master Closing connection")
self.connection_status = self.CON_NEW
is_handled = True
elif isinstance(packet.command_data, Mmdvm.TypeDmrData):
self.log_debug(f"{hexlify(data)}")
self.queue_incoming.put_nowait(packet)
else:
self.log_error(f"UNHANDLED {packet.__class__.__name__} {hexlify(data)}")
is_handled = True
if not is_handled:
self.log_error(
f"UNHANDLED {packet.__class__.__name__} {packet.command_data.__class__.__name__} {hexlify(data)} status {self.connection_status}"
)

def send_login_request(self) -> None:
self.log_info("Sending Login Request")
Expand Down Expand Up @@ -154,6 +181,7 @@ def send_configuration(self) -> None:
log_mmdvm_configuration(logger=self.get_logger(), packet=config)

def send_ping(self) -> None:
self.log_debug("Sending PING")
packet = struct.pack(">7sI", b"RPTPING", self.settings.get_repeater_dmrid())
self.queue_outgoing.put_nowait(packet)

Expand Down
3 changes: 0 additions & 3 deletions hytera_homebrew_bridge/lib/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
#!/usr/bin/env python3

import configparser
import logging
import logging.config
from typing import Optional

from hytera_homebrew_bridge.lib.logging_trait import LoggingTrait

Expand Down
Loading

0 comments on commit dd18609

Please sign in to comment.